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
:
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-geode</artifactId>
<version>2.6.1</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 toPROXY
. Only applicable on the client. -
indexableSessionAttributes
- Identifies theHttpSession
attributes by name that should be indexed for queries. Only Session attributes explicitly identified by name will be indexed. -
maxInactiveIntervalInSeconds
- ControlsHttpSession
Idle Expiration Timeout (TTI; defaults to 30 minutes). -
poolName
- Name of the dedicated connectionPool
connecting the client to a cluster of servers. Defaults togemfirePool
. Only applicable on the client. -
regionName
- Declares the name of theRegion
used to store and manageHttpSession
state. Defaults to "ClusteredSpringSessions". -
serverRegionShortcut
- Configures the data management policy on the server using the RegionShortcut Defaults toPARTITION
. 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 staleHttpSessions
. Defaults to unset. -
sessionSerializerBeanName
- Configures the name of the bean declared in the Spring context used to handle de/serialization of theHttpSession
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).
This approach is shown in the HttpSession with Spring Boot and Apache Geode Sample Guide.
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.
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:
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.