This guide describes how to build a Spring Boot Web application configured with Spring Session to transparently manage a Web application’s javax.servlet.http.HttpSession using Apache Geode in a clustered (distributed) and replicated.

In addition, this samples explores the effects of using Spring Session and Apache Geode to manage the HttpSession when the Spring Boot Web application also declares both "session" and "request" scoped bean definitions to process client HTTP requests.

This sample is based on a StackOverflow post, which posed the following question…​

Can session scope beans be used with Spring Session and VMware Tanzu GemFire?

The poster of the question when on to state and ask…​

When using Spring Session for "session" scope beans, Spring creates an extra HttpSession for this bean. Is this an existing issue? What is the solution for this?

The answer to the first question is most definitely, yes. And, the second statement/question is not correct, nor even valid, as explained in the answer.

This sample uses Apache Geode’s client/server topology with a pair of Spring Boot applications, one to configure and run an Apache Geode server, and another to configure and run an Apache Geode client, which is also a Spring Web MVC application making use of an HttpSession.

1. Updating Dependencies

Before using Spring Session, you must ensure that the required dependencies are included. If you are using Maven, include the following dependencies in your pom.xml:

pom.xml
<dependencies>
	<!-- ... -->

	<dependency>
		<groupId>org.springframework.session</groupId>
		<artifactId>spring-session-data-geode</artifactId>
		<version>2.7.2</version>
		<type>pom</type>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
</dependencies>
If you are using VMware Tanzu GemFire instead of Apache Geode, you may substitute the spring-session-data-gemfire artifact for spring-session-data-geode.

2. Spring Boot Configuration

After adding the required dependencies and repository declarations, we can create the Spring configuration for both our Apache Geode client and server using Spring Boot. The Spring configuration is responsible for creating a Servlet Filter that replaces the HttpSession with an implementation backed by Spring Session and Apace Geode.

2.1. Spring Boot, Apache Geode Cache Server

We start with a Spring Boot application to configure and bootstrap the Apache Geode server:

@SpringBootApplication (1)
@CacheServerApplication(name = "SpringSessionDataGeodeBootSampleWithScopedProxiesServer", logLevel = "error") (2)
@EnableGemFireHttpSession(maxInactiveIntervalInSeconds = 10) (3)
public class GemFireServer {

	public static void main(String[] args) {

		new SpringApplicationBuilder(GemFireServer.class)
			.web(WebApplicationType.NONE)
			.build()
			.run(args);
	}
}
1 First, we annotate the GemFireServer class with @SpringBootApplication to declare that this is a Spring Boot application, allowing us to leverage all of Spring Boot’s features (e.g. auto-configuration).
2 Next, we use the Spring Data for Apache Geode configuration annotation @CacheServerApplication to simplify the creation of a peer cache instance containing a CacheServer for cache clients to connect.
3 (Optional) Then, the @EnableGemFireHttpSession annotation is used to create the necessary server-side Region (by default, "ClusteredSpringSessions") to store the HttpSessions state. This step is optional since the Session Region could be created manually, perhaps even using external means. Using @EnableGemFireHttpSession is convenient and quick.

2.2. Spring Boot, Apache Geode Cache Client Web application

Now, we create a Spring Boot Web application exposing our Web service with Spring Web MVC, running as an Apache Geode cache client connected to our Spring Boot, Apache Geode server. The Web application will use Spring Session backed by Apache Geode to manage HttpSession state in a clustered (distributed) and replicated manner.

@SpringBootApplication (1)
@Controller (2)
public class Application {

	static final String INDEX_TEMPLATE_VIEW_NAME = "index";
	static final String PING_RESPONSE = "PONG";

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

	@ClientCacheApplication(name = "SpringSessionDataGeodeBootSampleWithScopedProxiesClient", logLevel = "error",
		readTimeout = 15000, retryAttempts = 1, subscriptionEnabled = true)  (3)
	@EnableGemFireHttpSession(poolName = "DEFAULT") (4)
	static class ClientCacheConfiguration { }

	@Configuration
	static class SpringWebMvcConfiguration {  (5)

		@Bean
		public WebMvcConfigurer webMvcConfig() {

			return new WebMvcConfigurer() {

				@Override
				public void addViewControllers(ViewControllerRegistry registry) {
					registry.addViewController("/").setViewName(INDEX_TEMPLATE_VIEW_NAME);
				}
			};
		}
	}

	@Autowired
	private RequestScopedProxyBean requestBean;

	@Autowired
	private SessionScopedProxyBean sessionBean;

	@ExceptionHandler
	@ResponseBody
	public String errorHandler(Throwable error) {
		StringWriter writer = new StringWriter();
		error.printStackTrace(new PrintWriter(writer));
		return writer.toString();
	}

	@RequestMapping(method = RequestMethod.GET, path = "/ping")
	@ResponseBody
	public String ping() {
		return PING_RESPONSE;
	}

	@RequestMapping(method = RequestMethod.GET, path = "/counts")
	public String requestAndSessionInstanceCount(HttpServletRequest request, HttpSession session, Model model) { (6)

		model.addAttribute("sessionId", session.getId());
		model.addAttribute("requestCount", this.requestBean.getCount());
		model.addAttribute("sessionCount", this.sessionBean.getCount());

		return INDEX_TEMPLATE_VIEW_NAME;
	}
}
1 Like the server, we declare our Web application to be a Spring Boot application by annotating our Application class with @SpringBootApplication.
2 @Controller is a Spring Web MVC annotation enabling our MVC handler mapping methods (i.e. methods annotated with @RequestMapping) to process HTTP requests (e.g. <6>)
3 We also declare our Web application to be an Apache Geode cache client by annotating our Application class with @ClientCacheApplication. Additionally, we adjust a few basic, "DEFAULT" Pool settings (e.g. readTimeout).
4 Next, we declare that the Web application will use Spring Session backed by Apache Geode to manage the HttpSession’s state by annotating the nested ClientCacheConfiguration class with @EnableGemFireHttpSession. This will create the necessary client-side PROXY Region (by default, "ClusteredSpringSessions") corresponding to the same server Region by name. All session state will be sent from the client to the server through Region data access operations. The client-side Region uses the "DEFAULT" Pool.
5 We adjust the Spring Web MVC configuration to set the home page, and finally…​
6 We declare the /counts HTTP request mapping handler method to keep track of the number of instances created by the Spring container for both "request" and "session" scoped proxy beans, of types RequestScopedProxyBean and SessionScopedProxyBean, respectively, each and every time a request is processed by the handler method.
In typical Apache Geode production deployments, where the cluster includes potentially hundreds or thousands of servers (a.k.a. data nodes), it is more common for clients to connect to 1 or more Apache Geode Locators running in the same cluster. A Locator passes meta-data to clients about the servers available in the cluster, the individual server load and which servers have the client’s data of interest, which is particularly important for direct, single-hop data access and latency-sensitive applications. See more details about the Client/Server Deployment in the Apache Geode User Guide.
For more information on configuring Spring Data for Apache Geode, refer to the Reference Guide.

2.2.1. Enabling VMware Tanzu GemFire HttpSession Management

The @EnableGemFireHttpSession annotation enables developers to configure certain aspects of both Spring Session and Apache Geode out-of-the-box using the following attributes:

  • clientRegionShortcut - specifies Apache Geode data management policy on the client with the ClientRegionShortcut (default is PROXY). This attribute is only used when configuring the client Region.

  • indexableSessionAttributes - Identifies the Session attributes by name that should be indexed for querying purposes. Only Session attributes explicitly identified by name will be indexed.

  • maxInactiveIntervalInSeconds - controls HttpSession idle-timeout expiration (defaults to 30 minutes).

  • poolName - name of the dedicated Apache Geode Pool used to connect a client to the cluster of servers. This attribute is only used when the application is a cache client. Defaults to gemfirePool.

  • regionName - specifies the name of the Apache Geode Region used to store and manage HttpSession state (default is "ClusteredSpringSessions").

  • serverRegionShortcut - specifies Apache Geode data management policy on the server with the RegionShortcut (default is PARTITION). This attribute is only used when configuring server Regions, or when a P2P topology is employed.

It is important to remember that the Apache Geode client Region name must match a server Region by the same name if the client Region is a PROXY or CACHING_PROXY. Client and server Region names are not required to match if the client Region used to store session state is LOCAL. However, keep in mind that Session state will not be propagated to the server and you lose all the benefits of using Apache Geode to store and manage distributed, replicated session state information on the servers in a distributed, replicated manner.

2.3. Session-scoped Proxy Bean

The Spring Boot Apache Geode cache client Web application defines the SessionScopedProxyBean domain class.

@Component (1)
@SessionScope(proxyMode = ScopedProxyMode.TARGET_CLASS) (2)
public class SessionScopedProxyBean implements Serializable {

	private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0);

	private final int count;

	public SessionScopedProxyBean() {
		this.count = INSTANCE_COUNTER.incrementAndGet(); (3)
	}

	public int getCount() {
		return count;
	}

	@Override
	public String toString() {
		return String.format("{ @type = '%s', count = %d }", getClass().getName(), getCount());
	}
}
1 First, the SessionScopedProxyBean domain class is stereotyped as a Spring @Component picked up by Spring’s classpath component-scan.
2 Additionally, instances of this class are scoped to the HttpSession. Therefore, each time a client request results in creating a new HttpSession (such as during a login event), a single instance of this class is created and will last for the duration of the HttpSession. When the HttpSession expires or is invalidated, this instance is destroyed by the Spring container. If the client re-establishes a new HttpSession, then another, new instance of this class will be provided to the application’s beans. However only ever 1 instance of this class exists for the duration of the HttpSession; no more!
3 Finally, this class keeps track of how many instances of this type are created by the Spring container throughout the entire application lifecycle.
More information on Spring’s @SessionScope (i.e. "session" scope proxy beans) can be found in the Reference Documentation, along with this.

2.4. Request-scoped Proxy Bean

The Spring Boot Apache Geode cache client Web application additionally defines the RequestScopedProxyBean domain class.

@Component (1)
@RequestScope(proxyMode = ScopedProxyMode.TARGET_CLASS) (2)
public class RequestScopedProxyBean {

	private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0);

	private final int count;

	public RequestScopedProxyBean() {
		this.count = INSTANCE_COUNTER.incrementAndGet(); (3)
	}

	public int getCount() {
		return count;
	}

	@Override
	public String toString() {
		return String.format("{ @type = '%s', count = %d }", getClass().getName(), getCount());
	}
}
1 First, the RequestScopedProxyBean domain class is stereotyped as a Spring @Component picked up by Spring’s classpath component-scan.
2 Additionally, instances of this class are scoped to the HttpServletRequest. Therefore, each time a client HTTP request is sent (e.g. to process a Thread-scoped transaction), a single instance of this class will be created and will last for the duration of the HttpServletRequest. When the request ends, this instance is destroyed by the Spring container. Any subsequent client HttpServletRequests results in another, new instance of this class, which will be provided to the application’s beans. However, only ever 1 instance of this class exists for the duration of the HttpServletRequest; no more!
3 Finally, this class keeps track of how many instances of this class are created by the Spring container throughout the entire application lifecycle.
More information on Spring’s @RequestScope (i.e. "request" scope proxy beans) can be found in the Reference Documentation, along with this.

3. Sample Spring Boot Web Application using Apache Geode-managed HttpSessions with Request and Session Scoped Proxy Beans

3.1. Running the Boot Sample Application

You can run the sample by obtaining the source code and invoking the following commands.

First, you must run the server:

$ ./gradlew :spring-session-sample-boot-gemfire-with-scoped-proxies:run

Then, in a separate terminal, run the client:

$ ./gradlew :spring-session-sample-boot-gemfire-with-scoped-proxies:bootRun

You should now be able to access the application at http://localhost:8080/counts.

3.2. Exploring the Sample Application

When you access the Web application @ http://localhost:8080/counts, you will see a screen similar to…​

sample boot gemfire with scoped proxies

The table shows 1 row with 3 columns of information.

The Session ID and Session Count columns show current HttpSession information including the current HttpSession’s ID and the number of HttpSessions created during the application’s current run.

Additionally, the current Request Count is shown to indicate how many requests have been made by the client, which in this case is your web browser.

You can use your web browser’s refresh button to increase both the session and request count. However, the session count only increases after the current session expires and a new session has been created for the client.

The session will time out after 10 seconds, which was configured on the server using the @EnableGemFireHttpSession annotation as we saw before (#3)…​

src/main/java/sample/server/GemFireServer.java
@SpringBootApplication
@CacheServerApplication(name = "SpringSessionDataGeodeServerWithScopedProxiesBootSample")
@EnableGemFireHttpSession(maxInactiveIntervalInSeconds = 10) (3)
public class GemFireServer {
  // ...
}

Here, you see that maxInactiveIntervalInSeconds is set to 10 seconds. After 10 seconds, Apache Geode will expire the HttpSession, and upon refreshing your web browser, a new session will be created and the session count will be incremented.

Naturally, every request results in incrementing the request count.

3.3. How does it work?

Well, from our defined Web service endpoint in our Spring MVC @Controller class on the client…​

src/main/java/sample/client/Application.java
@Controller
class Controller {

  @RequestMapping(method = RequestMethod.GET, path = "/counts")
  public String requestAndSessionInstanceCount(HttpServletRequest request, HttpSession session, Model model) { (7)

    model.addAttribute("sessionId", session.getId());
    model.addAttribute("requestCount", this.requestBean.getCount());
    model.addAttribute("sessionCount", this.sessionBean.getCount());

    return INDEX_TEMPLATE_VIEW_NAME;
  }
}

We see that we have injected a reference to the HttpSession as a request mapping handler method parameter. This will result in a new HttpSession on the client’s first HTTP request. Subsequent requests from the same client within the duration of the existing, current HttpSession will result in the same HttpSession being injected.

Of course, an HttpSession is identified by the session’s identifier, which is stored in a Cookie sent between the client and the server during HTTP request processing.

Additionally, we also see that we have injected references to the SessionScopedProxyBean and RequestScopedProxyBean in our @Controller class…​

src/main/java/sample/client/Application.java
@Autowired
private RequestScopedProxyBean requestBean;

@Autowired
private SessionScopedProxyBean sessionBean;

Based on the class definitions of these two types, as previously shown, these bean instances are scoped according to Spring’s "request" and "session" scopes, respectively. The 2 scopes can only be used in Web applications.

For each and every HTTP request sent by the client (i.e. on each web browser refresh), Spring will create a new instance of the RequestScopedProxyBean. This is why the request count increases with every refresh, which effectively is sending another HTTP request to the server to access and pull the content.

Furthermore, after each new HttpSession, a new instance of SessionScopedProxyBean is created. This instance persists for the duration of the session. If the HttpSession remains inactive (i.e. no request has been made) for longer than 10 seconds, the client’s current HttpSession will expire. Therefore, on any subsequent client HTTP request, a new HttpSession will be created by the Web container (e.g. Tomcat), which is replaced by Spring Session and backed with Apache Geode.

Additionally, this "session" scope bean is stored in the HttpSession, referenced by a session attribute. Therefore, you will also notice that the SessionScopedProxyBean class, unlike the RequestScopedProxyBean class, is also java.io.Serializable…​

src/main/java/sample/client/model/SessionScopedProxyBean.java
@Component
@SessionScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedProxyBean implements Serializable {
  // ...
}

This class is Serializable since it is stored in the HttpSession, which will be transferred as part of the HttpSession when sent to the Apache Geode cluster to be managed. Therefore, the type must be Serializable.

Any RequestScopedProxyBeans are not stored in the HttpSession and therefore will not be sent to the server, and as such, do not need to implement java.io.Serializable.