diff --git a/docs/src/docs/asciidoc/guides/boot-gemfire-with-gfsh-servers.adoc b/docs/src/docs/asciidoc/guides/boot-gemfire-with-gfsh-servers.adoc new file mode 100644 index 0000000..eac770f --- /dev/null +++ b/docs/src/docs/asciidoc/guides/boot-gemfire-with-gfsh-servers.adoc @@ -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"] +---- + + + + + + org.springframework.session + spring-session-data-geode + {spring-session-data-geode-version} + pom + + + + org.springframework.boot + spring-boot-starter-web + + + +---- + +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] +---- + + + + + + spring-snapshot + https://repo.spring.io/libs-snapshot + + + +---- +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] +---- + + + + + + spring-milestone + https://repo.spring.io/libs-milestone + + + +---- +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):1024 [Coordinator] +Server1 | 10.99.199.41(Server1:80669):1025 +Server2 | 10.99.199.41(Server2:80672):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]. diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 40c9fe6..fe410a8 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -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 <>. 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 diff --git a/samples/boot/gemfire-with-gfsh-servers/sample-boot-gemfire-with-gfsh-servers.png b/samples/boot/gemfire-with-gfsh-servers/sample-boot-gemfire-with-gfsh-servers.png new file mode 100644 index 0000000..9f9d690 Binary files /dev/null and b/samples/boot/gemfire-with-gfsh-servers/sample-boot-gemfire-with-gfsh-servers.png differ diff --git a/samples/boot/gemfire-with-gfsh-servers/spring-session-sample-boot-gemfire-with-gfsh-servers.gradle b/samples/boot/gemfire-with-gfsh-servers/spring-session-sample-boot-gemfire-with-gfsh-servers.gradle new file mode 100644 index 0000000..771be98 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/spring-session-sample-boot-gemfire-with-gfsh-servers.gradle @@ -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") + ] +} diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/java/sample/client/Application.java b/samples/boot/gemfire-with-gfsh-servers/src/main/java/sample/client/Application.java new file mode 100644 index 0000000..7894bc4 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/java/sample/client/Application.java @@ -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 attributes(HttpSession session) { + + Map sessionAttributes = new HashMap<>(); + + StreamSupport.stream(toIterable(session.getAttributeNames()).spliterator(), false) + .forEach(attributeName -> sessionAttributes.put(attributeName, + String.valueOf(session.getAttribute(attributeName)))); + + return sessionAttributes; + } + + Iterable toIterable(Enumeration enumeration) { + + return () -> Optional.ofNullable(enumeration) + .map(CollectionUtils::toIterator) + .orElseGet(Collections::emptyIterator); + } +} diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.gfsh b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.gfsh new file mode 100644 index 0000000..86abe78 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.gfsh @@ -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[] diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.sh b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.sh new file mode 100644 index 0000000..ba4df80 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/start-cluster.sh @@ -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" diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.gfsh b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.gfsh new file mode 100644 index 0000000..00fb8d4 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.gfsh @@ -0,0 +1,5 @@ +#!$GEODE_HOME/bin/gfsh + +stop server --name=Server2 +stop server --name=Server1 +stop locator --name=Locator1 diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.sh b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.sh new file mode 100644 index 0000000..5722283 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/bin/stop-cluster.sh @@ -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" diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/oql/queries.txt b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/oql/queries.txt new file mode 100644 index 0000000..bf5147f --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/geode/oql/queries.txt @@ -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[] diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/initializer-cache.xml b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/initializer-cache.xml new file mode 100644 index 0000000..6025cc2 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/initializer-cache.xml @@ -0,0 +1,15 @@ + + + + + + + org.springframework.session.data.gemfire.serialization.data.support.DataSerializableSessionSerializerInitializer + + + + + diff --git a/samples/boot/gemfire-with-gfsh-servers/src/main/resources/templates/index.html b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/templates/index.html new file mode 100644 index 0000000..e84fe93 --- /dev/null +++ b/samples/boot/gemfire-with-gfsh-servers/src/main/resources/templates/index.html @@ -0,0 +1,49 @@ + + + + Session Attributes + + + + +
+

Description

+

+ 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. +

+ +

Try it

+ +
+ + + + + +
+ +
+ + + + + + + + + + + + + + +
Attribute NameAttribute Value
namevalue
+
+ +