This guide describes how to build a Spring Boot application configured with Spring Session to transparently leverage Pivotal GemFire to back a web application’s HttpSession.

In this sample, GemFire’s client/server topology is employed using a pair of Spring Boot applications, one to configure and run a GemFire Server and another to configure and run the client, Spring MVC-based web application making use of the HttpSession.

The completed guide can be found in the HttpSession with GemFire using Spring Boot Sample Application.

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-gemfire</artifactId>
                <version>1.2.1.RELEASE</version>
                <type>pom</type>
        </dependency>
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
</dependencies>

Spring Boot Configuration

After adding the required dependencies and repository declarations, we can create our Spring configuration for both the GemFire 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 GemFire.

Spring Boot-based GemFire Server

We start with the Spring Boot application for configuring and bootstrapping a GemFire Server process…​

@SpringBootApplication
@EnableGemFireHttpSession(maxInactiveIntervalInSeconds = 20) (1)
public class GemFireServer {

        static final String DEFAULT_GEMFIRE_LOG_LEVEL = "config";

        public static void main(String[] args) {
                SpringApplication springApplication = new SpringApplication(GemFireServer.class);
                springApplication.setWebEnvironment(false);
                springApplication.run(args);
        }

        @Bean
        static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
                return new PropertySourcesPlaceholderConfigurer();
        }

        String applicationName() {
                return "samples:httpsession-gemfire-boot-"
                                .concat(GemFireServer.class.getSimpleName());
        }

        String gemfireLogLevel() {
                return System.getProperty("gemfire.log-level", DEFAULT_GEMFIRE_LOG_LEVEL);
        }

        Properties gemfireProperties() { (2)
                Properties gemfireProperties = new Properties();

                gemfireProperties.setProperty("name", applicationName());
                gemfireProperties.setProperty("mcast-port", "0");
                gemfireProperties.setProperty("log-level", gemfireLogLevel());

                return gemfireProperties;
        }

        @Bean
        CacheFactoryBean gemfireCache() { (3)
                CacheFactoryBean gemfireCache = new CacheFactoryBean();

                gemfireCache.setClose(true);
                gemfireCache.setProperties(gemfireProperties());

                return gemfireCache;
        }

        @Bean
        CacheServerFactoryBean gemfireCacheServer(Cache gemfireCache,
                        @Value("${gemfire.cache.server.bind-address:localhost}") String bindAddress,
                        @Value("${gemfire.cache.server.hostname-for-clients:localhost}") String hostnameForClients,
                        @Value("${gemfire.cache.server.port:12480}") int port) { (4)

                CacheServerFactoryBean gemfireCacheServer = new CacheServerFactoryBean();

                gemfireCacheServer.setAutoStartup(true);
                gemfireCacheServer.setCache(gemfireCache);
                gemfireCacheServer.setBindAddress(bindAddress);
                gemfireCacheServer.setHostNameForClients(hostnameForClients);
                gemfireCacheServer.setMaxTimeBetweenPings(
                                Long.valueOf(TimeUnit.MINUTES.toMillis(1)).intValue());
                gemfireCacheServer.setNotifyBySubscription(true);
                gemfireCacheServer.setPort(port);

                return gemfireCacheServer;
        }
}
1 The @EnableGemFireHttpSession annotation is used on the GemFire Server mainly to define the corresponding Region (e.g. ClusteredSpringSessions, the default) in which Session state information will be stored and managed by GemFire. As well, we have specified an arbitrary expiration attribute (i.e. maxInactiveIntervalInSeconds) for when the Session will timeout, which is triggered by a GemFire Region entry expiration event that also invalidates the Session object in the Region.
2 Next, we define a few Properties allowing us to configure certain aspects of the GemFire Server using GemFire’s System properties.
3 Then, we create an instance of the GemFire Cache using our defined Properties.
4 Finally, we setup a Cache Server instance running in the GemFire Server to listen for connections from cache clients. The Cache Server Socket will be used to connect our GemFire client cache, Spring Boot web application to the server.

The sample also makes use of a PropertySourcesPlaceholderConfigurer bean in order to externalize the sample application configuration to affect GemFire and application configuration/behavior from the command-line (e.g. such as GemFire’s log-level using the gemfire.log.level System property; more details below).

Spring Boot-based GemFire cache client Web application

Now, we create the Spring Boot web application (a GemFire cache client), exposing our Web service using Spring MVC along with Spring Session backed by GemFire, connected to our Spring Boot-based GemFire Server, in order to manage Session state in a cluster/replicated-enabled fashion.

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

        static final int MAX_CONNECTIONS = 50;

        static final long DEFAULT_WAIT_DURATION = TimeUnit.SECONDS.toMillis(20);
        static final long DEFAULT_WAIT_INTERVAL = 500L;

        static final CountDownLatch latch = new CountDownLatch(1);

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

        static { (6)
                ClientMembership
                                .registerClientMembershipListener(new ClientMembershipListenerAdapter() {
                                        @Override
                                        public void memberJoined(ClientMembershipEvent event) {
                                                if (!event.isClient()) {
                                                        latch.countDown();
                                                }
                                        }
                                });
        }

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

        protected final Logger logger = LoggerFactory.getLogger(getClass());

        @Bean
        static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
                return new PropertySourcesPlaceholderConfigurer();
        }

        String applicationName() {
                return "samples:httpsession-gemfire-boot-"
                                .concat(Application.class.getSimpleName());
        }

        String gemfireLogLevel() {
                return System.getProperty("gemfire.log-level", DEFAULT_GEMFIRE_LOG_LEVEL);
        }

        ConnectionEndpoint newConnectionEndpoint(String host, int port) {
                return new ConnectionEndpoint(host, port);
        }

        Properties gemfireProperties() { (2)
                Properties gemfireProperties = new Properties();

                gemfireProperties.setProperty("name", applicationName());
                gemfireProperties.setProperty("log-level", gemfireLogLevel());

                return gemfireProperties;
        }

        @Bean
        ClientCacheFactoryBean gemfireCache() { (3)
                ClientCacheFactoryBean gemfireCache = new ClientCacheFactoryBean();

                gemfireCache.setClose(true);
                gemfireCache.setProperties(gemfireProperties());

                return gemfireCache;
        }

        @Bean
        PoolFactoryBean gemfirePool(
                        @Value("${gemfire.cache.server.host:localhost}") String host,
                        @Value("${gemfire.cache.server.port:12480}") int port) { (4)

                PoolFactoryBean gemfirePool = new PoolFactoryBean();

                gemfirePool.setMaxConnections(MAX_CONNECTIONS);
                gemfirePool.setPingInterval(TimeUnit.SECONDS.toMillis(15));
                gemfirePool.setRetryAttempts(1);
                gemfirePool.setSubscriptionEnabled(true);
                gemfirePool.setServerEndpoints(
                                Collections.singleton(newConnectionEndpoint(host, port)));

                return gemfirePool;
        }

        @Bean
        BeanPostProcessor gemfireCacheServerAvailabilityBeanPostProcessor(
                        @Value("${gemfire.cache.server.host:localhost}") final String host,
                        @Value("${gemfire.cache.server.port:12480}") final int port) { (5)

                return new BeanPostProcessor() {

                        public Object postProcessBeforeInitialization(Object bean, String beanName)
                                        throws BeansException {
                                if (bean instanceof PoolFactoryBean || bean instanceof Pool) {
                                        if (!waitForCacheServerToStart(host, port)) {
                                                Application.this.logger.warn(
                                                                "No GemFire Cache Server found on [host: {}, port: {}]",
                                                                host, port);
                                        }
                                }

                                return bean;
                        }

                        public Object postProcessAfterInitialization(Object bean, String beanName)
                                        throws BeansException {
                                if (bean instanceof PoolFactoryBean || bean instanceof Pool) {
                                        try {
                                                Assert.state(
                                                                latch.await(DEFAULT_WAIT_DURATION, TimeUnit.MILLISECONDS),
                                                                String.format(
                                                                                "GemFire Cache Server failed to start on [host: %1$s, port: %2$d]",
                                                                                host, port));
                                        }
                                        catch (InterruptedException e) {
                                                Thread.currentThread().interrupt();
                                        }
                                }

                                return bean;
                        }
                };
        }

        @RequestMapping("/")
        public String index() { (7)
                return INDEX_TEMPLATE_VIEW_NAME;
        }

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

        @RequestMapping(method = RequestMethod.POST, path = "/session")
        public String session(HttpSession session, ModelMap modelMap,
                        @RequestParam(name = "attributeName", required = false) String name,
                        @RequestParam(name = "attributeValue", required = false) String value) { (9)

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

                return INDEX_TEMPLATE_VIEW_NAME;
        }
1 Here, again, we use the @EnableGemFireHttpSession annotation to not only configure the GemFire cache client, but also to override the (HTTP) Web application container’s HttpSession and replace it with a Session implementation backed by Spring Session in conjunction with GemFire. Also notice, we did not define any Session expiration timeout with the maxInactiveIntervalInSeconds attribute this time. That is because the Session expiration is managed by GemFire, which will appropriately notify the cache client when the Session times out. Again, we have just resorted to using the default Region, ClusteredSpringSessions. Of course, we can change the Region name, but we must do so on both the client and the server. That is a GemFire requirement, not a Spring Session Data GemFire requirement.
2 Similary to the server configuration, we set a few basic GemFire System Properties on the client.
3 Although, this time, an instance of ClientCache is created with the ClientCacheFactoryBean from Spring Data GemFire.
4 However, in order to connect to the GemFire Server we must define a GemFire Pool bean containing pre-populated connections to the server. Whenever a client Region entry operation corresponding to a Session update occurs, the client-side Region will use an existing, pooled connection to route the operation to the server.
5 The following Spring BeanPostProcessor (along with some utility methods) are only needed for testing purposes and is not required by any production code. Specifically, the BeanPostProcessor along with the code referenced in 6 is useful in integration test cases where the client and server processes are forked by the test framework. It is pretty easy to figure out that a race condition is imminent without proper coordination between the client and the server, therefore, the BPP and ClientMembershipListener help sync the interaction between the client and the server on startup during automated testing.
6 See <5> above.
7 Navigates the Web application to the home page (index.html), which uses Thymeleaf templates for server-side pages.
8 Heartbeat web service endpoint (useful for manual testing purposes).
9 Web service endpoint for adding a Session attributes defined by the user using the Web application UI. In addition, the webapp stores an additional Session attribute (requestCount) to keep track of how many HTTP requests the user has sent during the current "session".

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

In typical GemFire deployments, where the cluster includes potentially hundreds or thousands of GemFire data nodes (servers), it is more common for clients to connect to one or more GemFire Locators running in the cluster. A Locator passes meta-data to clients about the servers available, their load and which servers have the client’s data of interest, which is particularly important in direct, single-hop data access and latency-sensitive operations. See more details about the Client/Server Topology in GemFire’s User Guide.
For more information on configuring Spring Data GemFire, refer to the reference guide.

The @EnableGemFireHttpSession annotation enables a developer to configure certain aspects of both Spring Session and GemFire out-of-the-box using the following attributes:

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

  • regionName - specifies the name of the GemFire Region used to store HttpSession state (defaults is "ClusteredSpringSessions").

  • clientRegionShort - specifies GemFire’s data management policy with a GemFire ClientRegionShortcut (default is PROXY). This attribute is only used when configuring client Region.

  • serverRegionShort - specifies GemFire’s data management policy using a GemFire RegionShortcut (default is PARTITION). This attribute is only used when configuring server Regions, or when a p2p topology is employed.

It is important to note that the GemFire 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 Spring Sessions is LOCAL. However, keep in mind that your session state will not be propagated to the server and you lose all the benefits of using GemFire to store and manage distributed, replicated session state information in a cluster.

HttpSession with GemFire using Spring Boot Sample Application

Running the httpsession-gemfire-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 :samples:httpsession-gemfire-boot:run [-Dgemfire.log-level=config]

Then, in a separate terminal, run the client:

$ ./gradlew :samples:httpsession-gemfire-boot:bootRun [-Dgemfire.log-level=config]

You should now be able to access the application at http://localhost:8080/. In this sample, the web application is the client cache and the server is standalone.

Exploring the httpsession-gemfire-boot Sample Application

Try using the application. Fill out the form with the following information:

  • Attribute Name: username

  • Attribute Value: test

Now click the Set Attribute button. You should now see the attribute name and value displayed in the table along with an additional attribute (requestCount) indicating the number of Session interactions (via HTTP requests).

How does it work?

We interact with the standard HttpSession in the the Spring MVC web service endpoint, shown here for convenience:

src/main/java/sample/SessionServlet.java
@RequestMapping(method = RequestMethod.POST, path = "/session")
public String session(HttpSession session, ModelMap modelMap,
                @RequestParam(name = "attributeName", required = false) String name,
                @RequestParam(name = "attributeValue", required = false) String value) {

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

        return INDEX_TEMPLATE_VIEW_NAME;
}

Instead of using the embedded HTTP server’s HttpSession, we are actually persisting the Session state in GemFire. Spring Session creates a cookie named SESSION in your browser that contains the id of your session. Go ahead and view the cookies (click for help with Chrome or Firefox).

The following instructions assume you have a local GemFire installation. For more information on installation, see Installing Pivotal GemFire.

If you like, you can easily remove the session using gfsh. For example, on a Linux-based system type the following at the command-line:

$ gfsh

Then, enter the following commands in Gfsh ensuring to replace 70002719-3c54-4c20-82c3-e7faa6b718f3 with the value of your SESSION cookie, or the session ID returned by the GemFire OQL query (which should match):

gfsh>connect --jmx-manager=localhost[1099]

gfsh>query --query='SELECT * FROM /ClusteredSpringSessions.keySet'

Result     : true
startCount : 0
endCount   : 20
Rows       : 1

Result
------------------------------------
70002719-3c54-4c20-82c3-e7faa6b718f3

NEXT_STEP_NAME : END

gfsh>remove --region=/ClusteredSpringSessions --key="70002719-3c54-4c20-82c3-e7faa6b718f3"
The GemFire User Guide has more detailed instructions on using gfsh.

Now visit the application at http://localhost:8080/ again and observe that the attribute we added is no longer displayed.

Alternatively, you can wait 20 seconds for the session to expire and timeout, and then refresh the page. The attribute we added should no longer be displayed in the table.