This guide describes how to build a Spring Boot application configured with Spring Session to transparently leverage Apache Geode in order to manage a Web application’s javax.servlet.http.HttpSession.

In this sample, we will use Apache Geode’s client/server topology with a Spring Boot application that is both a Web application and a Apache Geode client configured to manage HttpSession state stored in a cluster of Apache Geode servers, which are configured and started with Gfsh.

In addition, this sample configures and uses Apache Geode’s DataSerialization framework and Delta propagation functionality to serialize the HttpSession. Therefore, it is necessary to perform additional configuration steps to properly setup Apache Geode’s DataSerialization capabilities on the servers so Apache Geode properly recognizes the Spring Session types.

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.2.2.RELEASE</version>
		<type>pom</type>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>

</dependencies>

2. Spring Boot Configuration

After adding the required dependencies and repository declarations, we can create the Spring configuration for our Apache Geode client using Spring Boot. The Spring configuration is responsible for creating a Servlet Filter that replaces the Web container’s HttpSession with an implementation backed by Spring Session, which is then stored and managed in Apache Geode.

2.1. The Spring Boot, Apache Geode ClientCache, Web application

Let’s start by creating a Spring Boot, Web application to expose our Web Service using Spring Web MVC, running as an Apache Geode client, connected to our Apache Geode servers. The Web application will use Spring Session backed by Apache Geode to manage HttpSession state in a distributed and replicated manner.

@SpringBootApplication (1)
@ClientCacheApplication(subscriptionEnabled = true) (2)
@EnableGemFireHttpSession(regionName = "Sessions", poolName = "DEFAULT") (3)
@Controller (4)
public class Application {

	private static final String INDEX_TEMPLATE_VIEW_NAME = "index";
	private static final String PING_RESPONSE = "PONG";
	private static final String REQUEST_COUNT_ATTRIBUTE_NAME = "requestCount";

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

	@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);
				}
			};
		}
	}

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

	@GetMapping("/ping")
	@ResponseBody
	public String ping() {
		return PING_RESPONSE;
	}

	@PostMapping("/session")
	public String session(HttpSession session, ModelMap modelMap,
			@RequestParam(name = "attributeName", required = false) String name,
			@RequestParam(name = "attributeValue", required = false) String value) { (7)

		modelMap.addAttribute("sessionAttributes",
			attributes(setAttribute(updateRequestCount(session), name, value)));

		return INDEX_TEMPLATE_VIEW_NAME;
	}
1 We start by declaring our Web application to be a Spring Boot application simply by annotating our application class with @SpringBootApplication.
2 We also declare our Web application to be an Apache Geode client by annotating our application class with @ClientCacheApplication. Additionally, we set subscriptionEnabled to receive notifications for any updates to the HttpSession that may have originated from a different application client accessing the same HttpSession.
3 Next, we declare that the Web application will use Spring Session backed by Apache Geode by annotating the application class with @EnableGemFireHttpSession. This will create the necessary client-side PROXY Region, which we have explicitly named "Sessions". This name must correspond to a server-side Region with the same name. All HttpSession state will be sent from the client to the server through Region data access operations using the "DEFAULT" connection Pool.
4 @Controller is a Spring Web MVC annotation enabling our MVC handler mapping methods (i.e. methods annotated with @RequestMapping, etc) to process HTTP requests (e.g. <7>)
5 Then, we adjust the Spring Web MVC configuration to set the home page, and…​
6 Add an error handler to print out the Stack Trace of any Exception thrown by the server.
7 Finally, we declare the /session HTTP request handler method to set an HttpSession attribute and increment a count for the number of HTTP requests that have occurred during this HttpSession.

There are many other useful utility methods, so please refer to the actual source code for full details.

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 Topology in the Apache Geode User Guide.
For more information on configuring Spring Data for Apache Geode, refer to the Reference Guide.

2.1.1. Enabling 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 - Configures the data management policy on the client using the ClientRegionShortcut. Defaults to PROXY. Only applicable on the client.

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

  • maxInactiveIntervalInSeconds - Controls HttpSession Idle Expiration Timeout (TTI; defaults to 30 minutes).

  • poolName - Name of the dedicated connection Pool connecting the client to a cluster of servers. Defaults to gemfirePool. Only applicable on the client.

  • regionName - Declares the name of the Region used to store and manage HttpSession state. Defaults to "ClusteredSpringSessions".

  • serverRegionShortcut - Configures the data management policy on the server using the RegionShortcut Defaults to PARTITION. Only applicable on the server, or when the P2P topology is employed.

  • sessionExpirationPolicyBeanName - Configures the name of the bean declared in the Spring context implementing the Expiration Policy used by Apache Geode to expire stale HttpSessions. Defaults to unset.

  • sessionSerializerBeanName - Configures the name of the bean declared in the Spring context used to handle de/serialization of the HttpSession between client and server. Defaults to PDX.

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 is LOCAL. However, keep in mind that by using a client LOCAL Region, HttpSession state will not be propagated to the server and you lose all the benefits of using Apache Geode to store and manage HttpSession state on the servers in a distributed, replicated manner.

2.2. Starting Apache Geode Servers with Gfsh

Now, we must start a small Apache Geode cluster.

For this sample, we will start 1 Locator and 2 Servers. In addition, we will create the "Sessions" Region used to store and manage the HttpSession in the cluster as a PARTITION Region using an Idle Expiration (TTI) timeout of 15 seconds.

The following example shows the Gfsh shell script we will use to setup the cluster:

#!$GEODE_HOME/bin/gfsh

set variable --name=USER_HOME --value=${SYS_USER_HOME}
set variable --name=REPO_HOME --value=${USER_HOME}/.m2/repository
set variable --name=SPRING_VERSION [email protected]@
set variable --name=SPRING_DATA_VERSION [email protected]@
set variable --name=SPRING_SESSION_VERSION [email protected]@
set variable --name=SPRING_SESSION_DATA_GEODE_VERSION [email protected]@
set variable --name=MEMBER_TIMEOUT --value=5000
set variable --name=CACHE_XML_FILE --value=${USER_HOME}/spring-session-data-geode/samples/boot/gemfire-with-gfsh-servers/src/main/resources/initializer-cache.xml

#set variable --name=SERVER_CLASSPATH --value=${REPO_HOME}/org/springframework/spring-core/${SPRING_VERSION}/spring-core-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-aop/${SPRING_VERSION}/spring-aop-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-beans/${SPRING_VERSION}/spring-beans-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-context/${SPRING_VERSION}/spring-context-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-context-support/${SPRING_VERSION}/spring-context-support-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-expression/${SPRING_VERSION}/spring-expression-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-jcl/${SPRING_VERSION}/spring-jcl-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/spring-tx/${SPRING_VERSION}/spring-tx-${SPRING_VERSION}.jar\
#:${REPO_HOME}/org/springframework/data/spring-data-commons/${SPRING_DATA_VERSION}/spring-data-commons-${SPRING_DATA_VERSION}.jar\
#:${REPO_HOME}/org/springframework/data/spring-data-geode/${SPRING_DATA_VERSION}/spring-data-geode-${SPRING_DATA_VERSION}.jar\
#:${REPO_HOME}/org/springframework/session/spring-session-core/${SPRING_SESSION_VERSION}/spring-session-core-${SPRING_SESSION_VERSION}.jar\
#:${REPO_HOME}/org/springframework/session/spring-session-data-geode/${SPRING_SESSION_DATA_GEODE_VERSION}/spring-session-data-geode-${SPRING_SESSION_DATA_GEODE_VERSION}.jar\
#:${REPO_HOME}/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar

set variable --name=SERVER_CLASSPATH --value=${REPO_HOME}/org/springframework/spring-core/${SPRING_VERSION}/spring-core-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-aop/${SPRING_VERSION}/spring-aop-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-beans/${SPRING_VERSION}/spring-beans-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-context/${SPRING_VERSION}/spring-context-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-context-support/${SPRING_VERSION}/spring-context-support-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-expression/${SPRING_VERSION}/spring-expression-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-jcl/${SPRING_VERSION}/spring-jcl-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/spring-tx/${SPRING_VERSION}/spring-tx-${SPRING_VERSION}.jar:${REPO_HOME}/org/springframework/data/spring-data-commons/${SPRING_DATA_VERSION}/spring-data-commons-${SPRING_DATA_VERSION}.jar:${REPO_HOME}/org/springframework/data/spring-data-geode/${SPRING_DATA_VERSION}/spring-data-geode-${SPRING_DATA_VERSION}.jar:${REPO_HOME}/org/springframework/session/spring-session-core/${SPRING_SESSION_VERSION}/spring-session-core-${SPRING_SESSION_VERSION}.jar:${REPO_HOME}/org/springframework/session/spring-session-data-geode/${SPRING_SESSION_DATA_GEODE_VERSION}/spring-session-data-geode-${SPRING_SESSION_DATA_GEODE_VERSION}.jar:${REPO_HOME}/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar

start locator --name=Locator1 --log-level=config
#start locator --name=Locator1 --log-level=config --J=-Dgemfire.member-timeout=${MEMBER_TIMEOUT}

start server --name=Server1 --log-level=config --cache-xml-file=${CACHE_XML_FILE} --classpath=${SERVER_CLASSPATH}
#start server --name=Server1 --log-level=config --cache-xml-file=${CACHE_XML_FILE} --classpath=${SERVER_CLASSPATH} --J=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 --J=-Dgemfire.member-timeout=${MEMBER_TIMEOUT}

start server --name=Server2 --server-port=0 --log-level=config --cache-xml-file=${CACHE_XML_FILE} --classpath=${SERVER_CLASSPATH}
#start server --name=Server2 --server-port=0 --log-level=config --cache-xml-file=${CACHE_XML_FILE} --classpath=${SERVER_CLASSPATH} --J=-Dgemfire.member-timeout=${MEMBER_TIMEOUT}

create region --name=Sessions --type=PARTITION --skip-if-exists --enable-statistics=true --entry-idle-time-expiration=15 --entry-idle-time-expiration-action=INVALIDATE
You will minimally need to replace path to the CACHE_XML_FILE depending on where you cloned the Spring Session for Apache Geode to on your system.

This Gfsh shell script file contains two additional bits of key information.

First, the shell script configures the CLASSPATH of the servers to contain all the Spring JARs. If there were application domain classes being stored in the HttpSession (i.e. in Session attributes), then a JAR file containing the application types must also be on the CLASSPATH of the servers.

This is necessary since, when Apache Geode applies a delta (i.e. the client only sends HttpSession changes to the servers), it must deserialize the HttpSession object in order to apply the delta. Therefore, it is also necessary to have your application domain objects present on the CLASSPATH as well.

Second, we must include a small snippet of cache.xml to initialize the Apache Geode DataSerialization framework in order to register and enable Apache Geode to recognize the Spring Session types representing the HttpSession. Apache Geode is very precise and will only use DataSerialization for the types it knows about through registration.

But, as a user, you do not need to worry about which Spring Session types Apache Geode needs to know about. That is the job of the Spring Session for Apache Geode’s o.s..session.data.gemfire.serialization.data.support.DataSerializableSessionSerializerInitializer class.

You simply just need to declare the provided Initializer in cache.xml, as follows:

<cache xmlns="http://geode.apache.org/schema/cache"
	   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	   xsi:schemaLocation="http://geode.apache.org/schema/cache https://geode.apache.org/schema/cache/cache-1.0.xsd"
	   version="1.0">

	<initializer>
		<class-name>
			org.springframework.session.data.gemfire.serialization.data.support.DataSerializableSessionSerializerInitializer
		</class-name>
	</initializer>

</cache>

Then, you include the initializer-cache.xml in the configuration of the server on startup:

gfsh> start server --name=Server1 ... --cache-xml-file=/absolute/filesystem/path/to/initializer-cache.xml

Of course, the start-cluster.gfsh shell script shown above handles these details for us. You may recycle this Gfsh shell script for your own purposes.

3. Running the Sample

Now it is time to run our sample.

In order to run the sample, you must install a full installation of Apache Geode. You can download the bits from here.

After install, run Gfsh:

$ gfsh
    _________________________     __
   / _____/ ______/ ______/ /____/ /
  / /  __/ /___  /_____  / _____  /
 / /__/ / ____/  _____/ / /    / /
/______/_/      /______/_/    /_/    1.6.0

Monitor and Manage Apache Geode
gfsh>

3.1. Running the server-side cluster

We start the cluster by executing our start-cluster.gfsh shell script:

gfsh> run --file=${SYS_USER_HOME}/spring-session-data-geode/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.gfsh

In the shell, you will see each command listed out as it is executed by Gfsh and you will see the Locator and Servers startup, and the "Sessions" Region get created.

If all is well, you should be able to list members, describe region, and so on:

gfsh> list members
  Name   | Id
-------- | ---------------------------------------------------------------
Locator1 | 10.99.199.41(Locator1:80666:locator)<ec><v0>:1024 [Coordinator]
Server1  | 10.99.199.41(Server1:80669)<v1>:1025
Server2  | 10.99.199.41(Server2:80672)<v2>:1026

gfsh> list regions
List of regions
---------------
Sessions

gfsh> describe region --name=/Sessions
..........................................................
Name            : Sessions
Data Policy     : partition
Hosting Members : Server1
                  Server2

Non-Default Attributes Shared By Hosting Members

 Type  |          Name           | Value
------ | ----------------------- | ---------
Region | data-policy             | PARTITION
       | entry-idle-time.timeout | 15
       | size                    | 0
       | statistics-enabled      | true

The "Sessions" Region configuration shown above is exactly the same configuration that Spring would have created for you if you were to configure and bootstrap your Apache Geode servers using Spring Boot instead.

For instance, you can achieve a similar effect with the following Spring Boot application class, which can be used to configure and bootstrap an Apache Geode server:

@SpringBootApplication
@CacheServerApplication
@EnableGemFireHttpSession(regionName = "Sessions",
    maxInactiveIntervalInSeconds = 15)
public class ServerApplication {

    public static void main(String[] args) {

        new SpringApplicationBuilder(ServerApplication.class)
            .web(WebApplicationType.NONE)
            .build()
            .run(args);
    }
}

The nice thing about this approach is, whether you are launching the ServerApplication class from you IDE, or by using a Spring Boot JAR, since the Maven POM (or alternatively, Gradle build file) defines all your dependencies, you do not need to worry about the CLASSPATH (other than, perhaps your own application domain object types).

3.2. Running the client

Now, it is time to run the Spring Boot, Apache Geode client, Web application.

You can start the Web application from your IDE, or alternatively use the bootRun Gradle task to launch the application:

$ gradlew :spring-session-sample-boot-gemfire-with-gfsh-servers:bootRun

Once you have started the client, you can navigate your Web browser to http://localhost:8080/ping. You should see the "PONG" response.

Then navigate to http://localhost:8080. This will return the index.html page where you can submit HTTP requests and add session attributes. The page will refresh with a count of the number of HTTP requests for the current session. After 15 seconds, the HTTP session will expire and you will not longer see any session attributes. Additionally, your HTTP request count will reset to 0.

sample boot gemfire with gfsh servers

While the application is running and before the HTTP session times out (again, the TTI expiration timeout is set to 15 seconds), you can issue queries in Gfsh to see the contents of the "_Sessions)" Region.

For example:

gfsh>query --query="SELECT session.id FROM /Sessions session"
Result : true
Limit  : 100
Rows   : 1

Result
------------------------------------
9becc38f-7249-4bd0-94eb-acff70f92b87


gfsh>query --query="SELECT session.getClass().getName() FROM /Sessions session"
Result : true
Limit  : 100
Rows   : 1

Result
--------------------------------------------------------------------------------------------------------------
org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository$DeltaCapableGemFireSession


gfsh>query --query="SELECT attributes.key, attributes.value FROM /Sessions session, session.attributes attributes"
Result : true
Limit  : 100
Rows   : 3

    key      | value
------------ | -----
requestCount | 2
testTwo      | bar
testOne      | foo

The list of Apache Geode OQL queries used in this sample can be found in:

src/main/resources/geode/oql/queries.txt
SELECT session.id FROM /Sessions session
SELECT session.getClass().getName() FROM /Sessions session
SELECT attributes.key, attributes.value FROM /Sessions session, session.attributes attributes

4. Conclusion

In this sample, we saw how to specifically configure and bootstrap a Apache Geode cluster of servers with Gfsh and then connect to the cluster using a Spring Boot application enabled with Spring Session, configured with Apache Geode as the backing store used to manage the HttpSessions for the Web application.

Additionally, the application used Apache Geode’s DataSerialization framework to serialize the HttpSession state to the servers and also send deltas. The setup and configuration expressed in the Gfsh shell script was necessary in order for Apache Geode to properly identify the Spring Session types to handle.

Feel free to modify this sample and play around for your learning purposes.

Source code can be found here.