Add sample demonstrating the use of Spring Session with Gfsh started Apache Geode servers configured with DataSerialization.

Resolve gh-11.
This commit is contained in:
John Blum
2018-11-13 13:58:51 -08:00
parent ff0ae7cd5f
commit 680c6d4a61
12 changed files with 757 additions and 16 deletions

View File

@@ -0,0 +1,411 @@
= Spring Session - HttpSession with Apache Geode Client/Server using Spring Boot and Gfsh Started Servers
John Blum
:toc:
:data-store-name: Apache Geode
:data-store-version: 16
:data-store-docs: http://geode.apache.org/docs/guide/{data-store-version}
:data-store-javadoc: http://geode.apache.org/releases/latest/javadoc
:data-store-website: http://geode.apache.org
:sdg-name: Spring Data for {data-store-name}
:sdg-docs: https://docs.spring.io/spring-data/geode/docs/current/reference/html
:sdg-javadoc: https://docs.spring.io/spring-data/geode/docs/current/api
This guide describes how to build a Spring Boot application configured with Spring Session to transparently
leverage {data-store-name} in order to manage a Web application's `javax.servlet.http.HttpSession`.
In this sample, we will use {data-store-name}'s client/server topology with a Spring Boot application that is both a
Web application and a {data-store-name} client configured to manage `HttpSession` state stored in a cluster of
{data-store-name} servers, which are configured and started with
{data-store-docs}/tools_modules/gfsh/chapter_overview.html[_Gfsh_].
In addition, this sample configures and uses {data-store-name}'s
{data-store-docs}/developing/data_serialization/gemfire_data_serialization.html[_DataSerialization_] framework
and {data-store-docs}/developing/delta_propagation/chapter_overview.html[Delta propagation] functionality
to serialize the `HttpSession`. Therefore, it is necessary to perform additional configuration steps to properly setup
{data-store-name}'s _DataSerialization_ capabilities on the servers so {data-store-name} properly recognizes
the Spring Session types.
== 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
[source,xml]
[subs="verbatim,attributes"]
----
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-geode</artifactId>
<version>{spring-session-data-geode-version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
----
ifeval::["{version-snapshot}" == "true"]
Since we are using a SNAPSHOT version, we need to add the Spring Snapshot Maven Repository.
If you are using _Maven_, include the following `repository` declaration in your `pom.xml`:
.pom.xml
[source,xml]
----
<repositories>
<!-- ... -->
<repository>
<id>spring-snapshot</id>
<url>https://repo.spring.io/libs-snapshot</url>
</repository>
</repositories>
----
endif::[]
ifeval::["{version-milestone}" == "true"]
Since we are using a Milestone version, we need to add the Spring Milestone Maven Repository.
If you are using _Maven_, include the following `repository` declaration in your `pom.xml`:
.pom.xml
[source,xml]
----
<repositories>
<!-- ... -->
<repository>
<id>spring-milestone</id>
<url>https://repo.spring.io/libs-milestone</url>
</repository>
</repositories>
----
endif::[]
// tag::config[]
[[httpsession-spring-configuration]]
== Spring Boot Configuration
After adding the required dependencies and repository declarations, we can create the Spring configuration for our
{data-store-name} 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 {data-store-name}.
[[httpsession-springboot-apachegeode-client-web-application]]
=== The Spring Boot, {data-store-name} `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 {data-store-name} client, connected to our {data-store-name} servers. The Web application will use Spring Session
backed by {data-store-name} to manage `HttpSession` state in a distributed and replicated manner.
[source,java]
----
include::{samples-dir}boot/gemfire-with-gfsh-servers/src/main/java/sample/client/Application.java[tags=class]
----
<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 {data-store-name} 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.
TIP: In typical {data-store-name} 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 {data-store-name} 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
{data-store-docs}/topologies_and_comm/cs_configuration/standard_client_server_deployment.html[Client/Server Topology]
in the {data-store-name} User Guide.
NOTE: For more information on configuring {sdg-name}, refer to the {sdg-docs}[Reference Guide].
[[httpsession-springsession-configuration]]
==== Enabling `HttpSession` Management
The `@EnableGemFireHttpSession` annotation enables developers to configure certain aspects of both Spring Session
and {data-store-name} out-of-the-box, using the following attributes:
* `clientRegionShortcut` - Configures the {data-store-docs}/developing/region_options/region_types.html[data management policy]
on the client using the {data-store-javadoc}/org/apache/geode/cache/client/ClientRegionShortcut.html[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-store-docs}/developing/region_options/region_types.html[data management policy]
on the server using the {data-store-javadoc}/org/apache/geode/cache/RegionShortcut.html[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 {data-store-name} 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.
NOTE: It is important to remember that the {data-store-name} 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 {data-store-name}
to store and manage `HttpSession` state on the servers in a distributed, replicated manner.
[[httpsession-apachegeode-servers-gfsh]]
=== Starting {data-store-name} Servers with Gfsh
Now, we must start a small {data-store-name} 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:
[source,txt]
----
include::{samples-dir}boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.gfsh[tags=shell-script]
----
NOTE: You will minimally need to replace path to the `CACHE_XML_FILE` depending on where you cloned the
{gh-url}[Spring Session for {data-store-name}] 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 {data-store-name} 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 {data-store-name} _DataSerialization_
framework in order to register and enable {data-store-name} to recognize the Spring Session types representing
the `HttpSession`. {data-store-name} 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 {data-store-name} needs to know about.
That is the job of the Spring Session for {data-store-name}'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:
[source,xml]
----
include::{samples-dir}boot/gemfire-with-gfsh-servers/src/main/resources/initializer-cache.xml[tags=cache-xml]
----
Then, you include the `initializer-cache.xml` in the configuration of the server on startup:
[source,txt]
----
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.
[[httpsession-sample-boot-with-gfsh-servers-run]]
== Running the Sample
Now it is time to run our sample.
In order to run the sample, you must install a full installation of {data-store-name}. You can download the bits
from {data-store-website}/releases[here].
After install, run Gfsh:
[source,txt]
----
$ gfsh
_________________________ __
/ _____/ ______/ ______/ /____/ /
/ / __/ /___ /_____ / _____ /
/ /__/ / ____/ _____/ / / / /
/______/_/ /______/_/ /_/ 1.6.0
Monitor and Manage Apache Geode
gfsh>
----
=== Running the server-side cluster
We start the cluster by executing our `start-cluster.gfsh` shell script:
[source,txt]
----
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:
[source,txt]
----
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 {data-store-name} 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 {data-store-name} server:
[source,java]
----
@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
link:guides/boot-gemfire.html[HttpSession with Spring Boot and {data-store-name} Sample Guide].
=== Running the client
Now, it is time to run the Spring Boot, {data-store-name} client, Web application.
You can start the Web application from your IDE, or alternatively use the `bootRun` Gradle task
to launch the application:
[source,txt]
----
$ 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.
image::{samples-dir}/boot/gemfire-with-gfsh-servers/sample-boot-gemfire-with-gfsh-servers.png[]
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:
[source,txt]
----
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 {data-store-name} OQL queries used in this sample can be found in:
.src/main/resources/geode/oql/queries.txt
[source,txt]
----
include::{samples-dir}boot/gemfire-with-gfsh-servers/src/main/resources/geode/oql/queries.txt[tags=queries]
----
== Conclusion
In this sample, we saw how to specifically configure and bootstrap a {data-store-name} cluster of servers with Gfsh
and then connect to the cluster using a Spring Boot application enabled with Spring Session, configured with
{data-store-name} as the backing store used to manage the `HttpSessions` for the Web application.
Additionally, the application used {data-store-name}'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 {data-store-name} 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 {gh-samples-url}boot/gemfire-with-gfsh-servers[here].

View File

@@ -45,6 +45,11 @@ is with our Sample Applications.
using a Client/Server topology.
| link:guides/boot-gemfire.html[HttpSession with Spring Boot and {data-store-name} Guide]
| {gh-samples-url}boot/gemfire-with-gfsh-servers[HttpSession with Spring Boot and {data-store-name} using Gfsh]
| Demonstrates how to use Spring Session to manage the `HttpSession` with {data-store-name} in a Spring Boot application
using a Client/Server topology. Additionally configures and uses {data-store-name}'s _DataSerialization_ framework.
| link:guides/boot-gemfire-with-gfsh-servers.html[HttpSession with Spring Boot and {data-store-name} using Gfsh Guide]
| {gh-samples-url}boot/gemfire-with-scoped-proxies[HttpSession with Spring Boot and {data-store-name} using Scoped Proxies]
| Demonstrates how to use Spring Session to manage the `HttpSession` with {data-store-name} in a Spring Boot application
using a Client/Server topology. The application also makes use of Spring Request and Session Scoped Proxy beans.
@@ -836,7 +841,7 @@ does not cause PDX serialized objects to be deserialized. But, nothing will pre
causing a deserialization, so be careful.
[[httpsession-gemfire-serialization-java-data-pdx]]
===== PDX + _Data Serialization_ + _Java Serialization_
===== _Data Serialization_ + PDX + _Java Serialization_
It is possible for {data-store-name} to support all 3 serialization formats simultaneously.
@@ -1071,48 +1076,54 @@ using Spring (Boot) as generally, the information that follows will not apply.
declared dependencies and your Spring configuration. However, if you are using *_Gfsh_* to start the servers
in your cluster, then definitely read on.
When using {data-store-name}'s _DataSerialization_ framework, especially from the client to serialize (HTTP) Session
====== Background
When using {data-store-name}'s _DataSerialization_ framework, especially from the client when serializing (HTTP) Session
state to the servers in the cluster, you must take care to configure the {data-store-name} servers in your cluster
with the appropriate dependencies. This is especially true when leveraging deltas as explained in the earlier section
on <<httpsession-gemfire-serialization-data>>.
When using the _DataSerialization_ framework as your serialization strategy to serialize (HTTP) Session state from
your Web application clients to the servers, then the servers must be properly configured with the Spring Session
for {data-store-name} class types are used to represent the (HTTP) Session and its contents. This means including
for {data-store-name} class types used to represent the (HTTP) Session and its contents. This means including
the Spring JARs on the servers classpath.
Additionally, using _DataSerialization_ may also require you to include the JARs containing your application domain
classes that are used by your Web application and put into the (HTTP) Session as Session Attribute values,
particularly if:
1. The types implement the `org.apache.geode.DataSerializable` interface.
2. The type implement the `org.apache.geode.Delta` interface.
1. Your types implement the `org.apache.geode.DataSerializable` interface.
2. Your types implement the `org.apache.geode.Delta` interface.
3. You have registered a `org.apache.geode.DataSerializer` that identifies and serializes the types.
4. The types implement the `java.io.Serializable` interface.
4. Your types implement the `java.io.Serializable` interface.
Of course, you must ensure your application domain object types put in the (HTTP) Session are serializable in some
form or another. However, you are not strictly required to use _DataSerialization_ nor are you necessarily
required to have your application domain object types on the servers classpath if:
1. The types implement the `org.apache.geode.pdx.PdxSerializable` interface.
2. Or, you have registered an `org.apache.geode.pdx.PdxSerializer` that properly identifies and serializes the types.
1. Your types implement the `org.apache.geode.pdx.PdxSerializable` interface.
2. Or, you have registered an `org.apache.geode.pdx.PdxSerializer` that properly identifies and serializes
your application domain object types.
{data-store-name} will apply the following order of precedence when determining the serialization strategy to use
to serialize an object graph:
1. First, `DataSerializable` objects and/or any registered `DataSerializers` identifying the objects to serialize.
2. Then `PdxSerializable` objects and/or any registered `PdxSerializer` identifying th objects to serialize.
3. And finally, all `java.io.Serialiable` types.
2. Then `PdxSerializable` objects and/or any registered `PdxSerializer` identifying the objects to serialize.
3. And finally, all `java.io.Serializable` types.
This also means that if a particular application domain object type (e.g. A) implements `java.io.Serializable`,
This also means that if a particular application domain object type (e.g. `A`) implements `java.io.Serializable`,
however, a (custom) `PdxSerializer` has been registered with {data-store-name} identifying the same application
domain object type (i.e. A), the {data-store-name} will use PDX to serialize "A" and *not* Java Serialization.
domain object type (i.e. `A`), then {data-store-name} will use PDX to serialize "A" and *not* Java Serialization,
in this case.
This is especially useful since then you can use _DataSerialization_ to serialize the (HTTP) Session object, leveraging
Deltas and all the powerful features of _DataSerialization_ but then use PDX to serialize your application domain object
Deltas and all the powerful features of _DataSerialization_, but then use PDX to serialize your application domain object
types, which greatly simplifies the configuration and/or effort involved.
Now, that we have a general understanding of why this support exists, how do you enable it?
Now that we have a general understanding of why this support exists, how do you enable it?
====== Configuration
First, create an {data-store-name} `cache.xml`, as follows:
@@ -1135,7 +1146,7 @@ First, create an {data-store-name} `cache.xml`, as follows:
----
Then when starting your servers using _*Gfsh*_, you specify:
Then, start your servers in _*Gfsh*_ using:
.Starting Server with Gfsh
[source,txt]
@@ -1171,7 +1182,7 @@ ${REPO_HOME}/org/springframework/spring-core/{spring-version}/spring-core-{sprin
Keep in mind, you may need to add your application domain object JAR files to the server classpath as well.
To get a complete picture of how this works, see the {gh-samples-url}[sample].
To get a complete picture of how this works, see the {gh-samples-url}boot/gemfire-with-gfsh-servers[sample].
[[httpsession-gemfire-serialization-framework-session-representation]]
===== Changing the Session Representation

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -0,0 +1,39 @@
import org.apache.tools.ant.filters.ReplaceTokens
apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-session-data-geode')
compile("org.springframework.boot:spring-boot-starter-thymeleaf") {
exclude group: "org.springframework.boot", module: "spring-boot-starter-logging"
}
compile("org.springframework.boot:spring-boot-starter-web") {
exclude group: "org.springframework.boot", module: "spring-boot-starter-logging"
}
compile "org.springframework.data:spring-data-geode-test"
compile "org.webjars:bootstrap"
compile "org.webjars:webjars-locator"
runtime "org.springframework.shell:spring-shell"
testCompile("org.springframework.boot:spring-boot-starter-test") {
exclude group: "org.springframework.boot", module: "spring-boot-starter-logging"
}
}
bootJar {
mainClassName = 'sample.client.Application'
}
processResources {
filter ReplaceTokens, tokens: [
'spring.version' : project.property("springVersion"),
'spring-data.version' : project.property("springDataGeodeVersion"),
'spring-session.version' : project.property("springSessionVersion"),
'spring-session-data-geode.version' : project.property("version")
]
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.client;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.StreamSupport;
import javax.servlet.http.HttpSession;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.gemfire.config.annotation.ClientCacheApplication;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SuppressWarnings("unused")
// tag::class[]
@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;
}
// end::class[]
HttpSession updateRequestCount(HttpSession session) {
synchronized (session) {
Integer currentRequestCount = (Integer) session.getAttribute(REQUEST_COUNT_ATTRIBUTE_NAME);
session.setAttribute(REQUEST_COUNT_ATTRIBUTE_NAME, nullSafeIncrement(currentRequestCount));
return session;
}
}
Integer nullSafeIncrement(Integer value) {
return nullSafeIntValue(value) + 1;
}
int nullSafeIntValue(Number value) {
return Optional.ofNullable(value).map(Number::intValue).orElse(0);
}
HttpSession setAttribute(HttpSession session, String attributeName, String attributeValue) {
if (isSet(attributeName, attributeValue)) {
session.setAttribute(attributeName, attributeValue);
}
return session;
}
boolean isSet(String... values) {
boolean set = true;
for (String value : values) {
set &= StringUtils.hasText(value);
}
return set;
}
Map<String, String> attributes(HttpSession session) {
Map<String, String> sessionAttributes = new HashMap<>();
StreamSupport.stream(toIterable(session.getAttributeNames()).spliterator(), false)
.forEach(attributeName -> sessionAttributes.put(attributeName,
String.valueOf(session.getAttribute(attributeName))));
return sessionAttributes;
}
<T> Iterable<T> toIterable(Enumeration<T> enumeration) {
return () -> Optional.ofNullable(enumeration)
.map(CollectionUtils::toIterator)
.orElseGet(Collections::emptyIterator);
}
}

View File

@@ -0,0 +1,40 @@
#tag::shell-script[]
#!$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 --value=@spring.version@
set variable --name=SPRING_DATA_VERSION --value=@spring-data.version@
set variable --name=SPRING_SESSION_VERSION --value=@spring-session.version@
set variable --name=SPRING_SESSION_DATA_GEODE_VERSION --value=@spring-session-data-geode.version@
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
#end::shell-script[]

View File

@@ -0,0 +1,3 @@
#!/bin/bash
gfsh -e "run --file=${SYS_USER_HOME}/spring-session-data-geode/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.gfsh"

View File

@@ -0,0 +1,5 @@
#!$GEODE_HOME/bin/gfsh
stop server --name=Server2
stop server --name=Server1
stop locator --name=Locator1

View File

@@ -0,0 +1,3 @@
#!/bin/bash
gfsh -e "run --file=${SYS_USER_HOME}/spring-session-data-geode/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.gfsh"

View File

@@ -0,0 +1,5 @@
# tag::queries[]
SELECT session.id FROM /Sessions session
SELECT session.getClass().getName() FROM /Sessions session
SELECT attributes.key, attributes.value FROM /Sessions session, session.attributes attributes
# end::queries[]

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- tag::cache-xml[] -->
<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 http://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>
<!-- end::cache-xml[] -->

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-3.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Session Attributes</title>
<link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" href="/webjars/bootstrap/css/bootstrap.min.css"/>
<style type="text/css">
body {
padding: 1em;
}
</style>
</head>
<body>
<div class="container">
<h1>Description</h1>
<p>
This application demonstrates how to use a GemFire instance to back your session. Notice that there
is no JSESSIONID cookie. We are also able to customize the way of identifying what the requested
session id is.
</p>
<h1>Try it</h1>
<form class="form-inline" role="form" action="./session" method="post">
<label for="attributeName">Attribute Name</label>
<input id="attributeName" type="text" name="attributeName"/>
<label for="attributeValue">Attribute Value</label>
<input id="attributeValue" type="text" name="attributeValue"/>
<input type="submit" value="Set Attribute"/>
</form>
<hr/>
<table class="table table-striped">
<thead>
<tr>
<th>Attribute Name</th>
<th>Attribute Value</th>
</tr>
</thead>
<tbody>
<tr th:each="attribute: ${sessionAttributes}">
<td th:text="${attribute.key}">name</td>
<td th:text="${attribute.value}">value</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>