diff --git a/docs/build.gradle b/docs/build.gradle index d2af63c..1b9c343 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -32,6 +32,7 @@ dependencies { 'org.mockito:mockito-core:1.9.5', "org.springframework:spring-test:$springVersion", 'org.easytesting:fest-assert:1.4', + "com.hazelcast:hazelcast:$hazelcastVersion", "redis.clients:jedis:2.4.1", "javax.servlet:javax.servlet-api:$servletApiVersion" } @@ -47,6 +48,7 @@ asciidoctor { 'download-url' : "https://github.com/spring-projects/spring-session/archive/${ghTag}.zip", 'spring-session-version' : version, 'spring-version' : springVersion, + 'hazelcast-version' : hazelcastVersion, 'docs-test-dir' : rootProject.projectDir.path + '/docs/src/test/java/', 'docs-test-resources-dir' : rootProject.projectDir.path + '/docs/src/test/resources/', 'samples-dir' : rootProject.projectDir.path + '/samples/', diff --git a/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc b/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc new file mode 100644 index 0000000..a78d055 --- /dev/null +++ b/docs/src/docs/asciidoc/guides/hazelcast-spring.adoc @@ -0,0 +1,191 @@ += Spring Session and Spring Security with Hazelcast +Tommy Ludwig; Rob Winch +:toc: + +This guide describes how to use Spring Session along with Spring Security using Hazelcast as your data store. +It assumes you have already applied Spring Security to your application. + +NOTE: The completed guide can be found in the <>. + +== Updating Dependencies +Before you use Spring Session, you must ensure to update your dependencies. +If you are using Maven, ensure to add the following dependencies: + +.pom.xml +[source,xml] +[subs="verbatim,attributes"] +---- + + + + + com.hazelcast + hazelcast + {hazelcast-version} + + + org.springframework + spring-web + {spring-version} + + +---- + +ifeval::["{version-snapshot}" == "true"] +Since We are using a SNAPSHOT version, we need to ensure to add the Spring Snapshot Maven Repository. +Ensure you have the following 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 ensure to add the Spring Milestone Maven Repository. +Ensure you have the following in your pom.xml: + +.pom.xml +[source,xml] +---- + +spring-milestone +https://repo.spring.io/libs-milestone + +---- +endif::[] + +[[security-spring-configuration]] +== Spring Configuration + +After adding the required dependencies, we can create our Spring configuration. +The Spring configuration is responsible for creating a Servlet Filter that replaces the `HttpSession` implementation with an implementation backed by Spring Session. +Add the following Spring Configuration: + +[source,java] +---- +include::{docs-test-dir}docs/http/HazelcastHttpSessionConfig.java[tags=config] +---- + +<1> The `@EnableHazelcastHttpSession` annotation creates a Spring Bean with the name of `springSessionRepositoryFilter` that implements Filter. +The filter is what is in charge of replacing the `HttpSession` implementation to be backed by Spring Session. +In this instance Spring Session is backed by Hazelcast. +<2> We create a `HazelcastInstance` that connects Spring Session to Hazelcast. +By default, an embedded instance of Hazelcast is started and connected to by the application. +For more information on configuring Hazelcast, refer to the http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-configuration[reference documentation]. + +== Servlet Container Initialization + +Our <> created a Spring Bean named `springSessionRepositoryFilter` that implements `Filter`. +The `springSessionRepositoryFilter` bean is responsible for replacing the `HttpSession` with a custom implementation that is backed by Spring Session. + +In order for our `Filter` to do its magic, Spring needs to load our `Config` class. +Since our application is already loading Spring configuration using our `SecurityInitializer` class, we can simply add our Config class to it. + +.src/main/java/sample/SecurityInitializer.java +[source,java] +---- +include::{samples-dir}hazelcast-spring/src/main/java/sample/SecurityInitializer.java[tags=class] +---- + +Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our `springSessionRepositoryFilter` for every request. +It is extremely important that Spring Session's `springSessionRepositoryFilter` is invoked before Spring Security's `springSecurityFilterChain`. +This ensures that the `HttpSession` that Spring Security uses is backed by Spring Session. +Fortunately, Spring Session provides a utility class named `AbstractHttpSessionApplicationInitializer` that makes this extremely easy. +You can find an example below: + +.src/main/java/sample/Initializer.java +[source,java] +---- +include::{samples-dir}hazelcast-spring/src/main/java/sample/Initializer.java[tags=class] +---- + +NOTE: The name of our class (Initializer) does not matter. What is important is that we extend `AbstractHttpSessionApplicationInitializer`. + +By extending `AbstractHttpSessionApplicationInitializer` we ensure that the Spring Bean by the name `springSessionRepositoryFilter` is registered with our Servlet Container for every request before Spring Security's `springSecurityFilterChain`. + + + +[[hazelcast-spring-security-sample]] +== Hazelcast Spring Security Sample Application + +=== Running the Sample Application + +You can run the sample by obtaining the {download-url}[source code] and invoking the following command: + +[NOTE] +==== +Hazelcast will run in embedded mode with your application by default, but if you want to connect +to a stand alone instance instead, you can configure it by following the instructions in the +http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-configuration[reference documentation]. +==== + +---- +$ ./gradlew :samples:hazelcast-spring:tomcatRun +---- + +You should now be able to access the application at http://localhost:8080/ + +=== Exploring the security Sample Application + +Try using the application. Enter the following to log in: + +* **Username** _user_ +* **Password** _password_ + +Now click the **Login** button. +You should now see a message indicating your are logged in with the user entered previously. +The user's information is stored in Hazelcast rather than Tomcat's `HttpSession` implementation. + +=== How does it work? + +Instead of using Tomcat's `HttpSession`, we are actually persisting the values in Hazelcast. +Spring Session replaces the `HttpSession` with an implementation that is backed by a `Map` in Hazelcast. +When Spring Security's `SecurityContextPersistenceFilter` saves the `SecurityContext` to the `HttpSession` it is then persisted into Hazelcast. + +When a new `HttpSession` is created, 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 https://developer.chrome.com/devtools/docs/resources#cookies[Chrome] or https://getfirebug.com/wiki/index.php/Cookies_Panel#Cookies_List[Firefox]). + +=== Interact with the data store + +If you like, you can remove the session using http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-java-client[a Java client], +http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#other-client-implementations[one of the other clients], or the +http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#management-center[management center]. + +==== Using the console + +For example, using the management center console after connecting to your Hazelcast node: + + default> ns spring:session:sessions + spring:session:sessions> m.clear + +TIP: The Hazelcast documentation has instructions for http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#console[the console]. + +Alternatively, you can also delete the explicit key. Enter the following into the console ensuring to replace `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie: + + spring:session:sessions> m.remove 7e8383a4-082c-4ffe-a4bc-c40fd3363c5e + +Now visit the application at http://localhost:8080/ and observe that we are no longer authenticated. + +==== Using the REST API + +As described in the other clients section of the documentation, there is a +http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#rest-client[REST API] +provided by the Hazelcast node(s). + +For example, you could delete an individual key as follows (replacing `7e8383a4-082c-4ffe-a4bc-c40fd3363c5e` with the value of your SESSION cookie): + + $ curl -v -X DELETE http://localhost:xxxxx/hazelcast/rest/maps/spring:session:sessions/7e8383a4-082c-4ffe-a4bc-c40fd3363c5e + +TIP: The port number of the Hazelcast node will be printed to the console on startup. Replace `xxxxx` above with the port number. + +Now observe that you are no longer authenticated with this session. \ No newline at end of file diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 68f24da..21dc37e 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -71,7 +71,7 @@ If you are looking to get started with Spring Session, the best place to start i [[samples-hazelcast-spring]] | {gh-samples-url}hazelcast-spring[Hazelcast Spring] | Demonstrates how to use Spring Session and Hazelcast with an existing Spring Security application. -| TBD +| link:guides/hazelcast-spring.html[Hazelcast Spring Guide] |=== @@ -342,6 +342,43 @@ It is important to note that no infrastructure for session expirations is config This is because things like session expiration are highly implementation dependent. This means if you require cleaning up expired sessions, you are responsible for cleaning up the expired sessions. +[[api-enablehazelcasthttpsession]] +=== EnableHazelcastHttpSession + +If you wish to use http://hazelcast.org/[Hazelcast] as your backing source for the `SessionRepository`, then the `@EnableHazelcastHttpSession` annotation +can be added to an `@Configuration` class. This extends the functionality provided by the `@EnableSpringHttpSession` annotation but makes the `SessionRepository` for you in Hazelcast. +You must provide a single `HazelcastInstance` bean for the configuration to work. +For example: + +[source,java,indent=0] +---- +include::{docs-test-dir}docs/http/HazelcastHttpSessionConfig.java[tags=config] +---- + +This will configure Hazelcast in embedded mode with default configuration. +See the http://docs.hazelcast.org/docs/latest/manual/html-single/hazelcast-documentation.html#hazelcast-configuration[Hazelcast documentation] for +detailed information on configuration options for Hazelcast. + +[[api-enablehazelcasthttpsession-storage]] +==== Storage Details + +Sessions will be stored in a distributed `Map` in Hazelcast using a <>. +The `Map` interface methods will be used to `get()` and `put()` Sessions. +The expiration of a session in the `Map` is handled by Hazelcast's native support for configuring a map's `max-idle-seconds` setting. +Entries (sessions) that have been idle longer than the max idle setting will be automatically removed from the `Map`. + +[[api-enablehazelcasthttpsession-customize]] +==== Basic Customization +You can use the following attributes on `@EnableHazelcastHttpSession` to customize the configuration: + +* **maxInactiveIntervalInSeconds** - the amount of time before the session will expire in seconds. Default is 1800 seconds (30 minutes) +* **sessionMapName** - the name of the distributed `Map` that will be used in Hazelcast to store the session data. + +[[api-enablehazelcasthttpsession-events]] +==== Session Events +Using a `MapListener` to respond to entries being added, evicted, and removed from the distributed `Map`, these events will trigger +publishing SessionCreatedEvent, SessionExpiredEvent, and SessionDeletedEvent events respectively using the `ApplicationEventPublisher`. + [[api-redisoperationssessionrepository]] === RedisOperationsSessionRepository diff --git a/docs/src/test/java/docs/http/HazelcastHttpSessionConfig.java b/docs/src/test/java/docs/http/HazelcastHttpSessionConfig.java new file mode 100644 index 0000000..bea7913 --- /dev/null +++ b/docs/src/test/java/docs/http/HazelcastHttpSessionConfig.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2015 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 docs.http; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.data.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession; + +import com.hazelcast.config.Config; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; + +//tag::config[] +@EnableHazelcastHttpSession // <1> +@Configuration +public class HazelcastHttpSessionConfig { + @Bean + public HazelcastInstance embeddedHazelcast() { + Config hazelcastConfig = new Config(); + return Hazelcast.newHazelcastInstance(hazelcastConfig); // <2> + } +} +// end::config[] \ No newline at end of file diff --git a/samples/hazelcast-spring/src/main/java/sample/Config.java b/samples/hazelcast-spring/src/main/java/sample/Config.java index 1757aab..64f229a 100644 --- a/samples/hazelcast-spring/src/main/java/sample/Config.java +++ b/samples/hazelcast-spring/src/main/java/sample/Config.java @@ -15,74 +15,34 @@ */ package sample; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.session.MapSessionRepository; -import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; -import org.springframework.session.ExpiringSession; +import org.springframework.session.data.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession; import org.springframework.util.SocketUtils; -import com.hazelcast.config.MapConfig; import com.hazelcast.config.NetworkConfig; import com.hazelcast.config.SerializerConfig; import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.core.IMap; // tag::class[] -@EnableSpringHttpSession +@EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = "300") @Configuration public class Config { - private String sessionMapName = "spring:session:sessions"; - - @Autowired - private ApplicationEventPublisher eventPublisher; - @Bean(destroyMethod = "shutdown") public HazelcastInstance hazelcastInstance() { com.hazelcast.config.Config cfg = new com.hazelcast.config.Config(); NetworkConfig netConfig = new NetworkConfig(); netConfig.setPort(SocketUtils.findAvailableTcpPort()); + System.out.println("Hazelcast port #: " + netConfig.getPort()); cfg.setNetworkConfig(netConfig); SerializerConfig serializer = new SerializerConfig().setTypeClass( Object.class).setImplementation(new ObjectStreamSerializer()); cfg.getSerializationConfig().addSerializerConfig(serializer); - MapConfig mc = new MapConfig(); - mc.setName(sessionMapName); - - mc.setMaxIdleSeconds(60); - cfg.addMapConfig(mc); return Hazelcast.newHazelcastInstance(cfg); } - @Bean - public SessionRemovedListener removeListener() { - return new SessionRemovedListener(eventPublisher); - } - - @Bean - public SessionEvictedListener evictListener() { - return new SessionEvictedListener(eventPublisher); - } - - @Bean - public SessionCreatedListener addListener() { - return new SessionCreatedListener(eventPublisher); - } - - @Bean - public MapSessionRepository sessionRepository(HazelcastInstance instance, - SessionRemovedListener removeListener, SessionEvictedListener evictListener, - SessionCreatedListener addListener) { - IMap sessions = instance.getMap(sessionMapName); - sessions.addEntryListener(removeListener, true); - sessions.addEntryListener(evictListener, true); - sessions.addEntryListener(addListener, true); - return new MapSessionRepository(sessions); - } } // end::class[] \ No newline at end of file diff --git a/samples/hazelcast-spring/src/main/java/sample/SessionCreatedListener.java b/samples/hazelcast-spring/src/main/java/sample/SessionCreatedListener.java deleted file mode 100644 index 02e4bc9..0000000 --- a/samples/hazelcast-spring/src/main/java/sample/SessionCreatedListener.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2002-2015 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; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.session.ExpiringSession; -import org.springframework.session.events.SessionCreatedEvent; - -import com.hazelcast.core.EntryEvent; -import com.hazelcast.map.listener.EntryAddedListener; - -/** -* -* @author Mark Anderson -* -*/ -public class SessionCreatedListener - implements EntryAddedListener { - - private ApplicationEventPublisher eventPublisher; - - public SessionCreatedListener(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - public void entryAdded(EntryEvent event) { - System.out.println("Session added: " + event); - eventPublisher.publishEvent(new SessionCreatedEvent(this, event.getValue())); - } -} diff --git a/samples/hazelcast-spring/src/main/java/sample/SessionEvictedListener.java b/samples/hazelcast-spring/src/main/java/sample/SessionEvictedListener.java deleted file mode 100644 index 1a76ed3..0000000 --- a/samples/hazelcast-spring/src/main/java/sample/SessionEvictedListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2002-2015 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; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.session.ExpiringSession; -import org.springframework.session.events.SessionExpiredEvent; - -import com.hazelcast.core.EntryEvent; -import com.hazelcast.map.listener.EntryEvictedListener; - -/** -* -* @author Mark Anderson -* -*/ -public class SessionEvictedListener - implements EntryEvictedListener { - - private ApplicationEventPublisher eventPublisher; - - public SessionEvictedListener(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - public void entryEvicted(EntryEvent event) { - System.out.println("Session removed: " + event); - eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue())); - } - -} diff --git a/samples/hazelcast-spring/src/main/java/sample/SessionRemovedListener.java b/samples/hazelcast-spring/src/main/java/sample/SessionRemovedListener.java deleted file mode 100644 index da334e1..0000000 --- a/samples/hazelcast-spring/src/main/java/sample/SessionRemovedListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2002-2015 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; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.session.ExpiringSession; -import org.springframework.session.events.SessionDeletedEvent; - -import com.hazelcast.core.EntryEvent; -import com.hazelcast.map.listener.EntryRemovedListener; - -/** - * - * @author Mark Anderson - * - */ -public class SessionRemovedListener - implements EntryRemovedListener { - - private ApplicationEventPublisher eventPublisher; - - public SessionRemovedListener(ApplicationEventPublisher eventPublisher) { - this.eventPublisher = eventPublisher; - } - - public void entryRemoved(EntryEvent event) { - System.out.println("Session removed: " + event); - eventPublisher.publishEvent(new SessionDeletedEvent(this, event.getOldValue())); - } - -} diff --git a/spring-session/build.gradle b/spring-session/build.gradle index aaf75ec..43fdc19 100644 --- a/spring-session/build.gradle +++ b/spring-session/build.gradle @@ -16,6 +16,7 @@ configurations { dependencies { optional "org.springframework.data:spring-data-redis:$springDataRedisVersion", + "com.hazelcast:hazelcast:$hazelcastVersion", "org.springframework:spring-context:$springVersion", "org.springframework:spring-web:$springVersion", "org.springframework:spring-messaging:$springVersion", diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/SessionEventRegistry.java b/spring-session/src/integration-test/java/org/springframework/session/data/SessionEventRegistry.java new file mode 100644 index 0000000..b9c4eb8 --- /dev/null +++ b/spring-session/src/integration-test/java/org/springframework/session/data/SessionEventRegistry.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2015 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 org.springframework.session.data; + +import org.springframework.context.ApplicationListener; +import org.springframework.session.events.AbstractSessionEvent; + +public class SessionEventRegistry implements ApplicationListener { + private AbstractSessionEvent event; + private Object lock = new Object(); + + public void onApplicationEvent(AbstractSessionEvent event) { + this.event = event; + synchronized (lock) { + lock.notifyAll(); + } + } + + public void setLock(Object lock) { + this.lock = lock; + } + + public void clear() { + this.event = null; + } + + public boolean receivedEvent() throws InterruptedException { + return waitForEvent() != null; + } + + @SuppressWarnings("unchecked") + public E getEvent() throws InterruptedException { + return (E) waitForEvent(); + } + + @SuppressWarnings("unchecked") + private E waitForEvent() throws InterruptedException { + synchronized(lock) { + if(event == null) { + lock.wait(3000); + } + } + return (E) event; + } +} diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/hazelcast/HazelcastRepositoryITests.java b/spring-session/src/integration-test/java/org/springframework/session/data/hazelcast/HazelcastRepositoryITests.java new file mode 100644 index 0000000..ad64c13 --- /dev/null +++ b/spring-session/src/integration-test/java/org/springframework/session/data/hazelcast/HazelcastRepositoryITests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2015 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 org.springframework.session.data.hazelcast; + +import static org.fest.assertions.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.ExpiringSession; +import org.springframework.session.SessionRepository; +import org.springframework.session.data.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.util.SocketUtils; + +import com.hazelcast.config.Config; +import com.hazelcast.config.NetworkConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.IMap; + +/** + * Integration tests that check the underlying data source - in this case + * Hazelcast. + * + * @author Tommy Ludwig + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class HazelcastRepositoryITests { + + @Autowired + private HazelcastInstance hazelcast; + + @Autowired + private SessionRepository repository; + + @Test + public void createAndDestorySession() { + S sessionToSave = repository.createSession(); + String sessionId = sessionToSave.getId(); + + IMap hazelcastMap = hazelcast.getMap("spring:session:sessions"); + + assertThat(hazelcastMap.size()).isEqualTo(0); + + repository.save(sessionToSave); + + assertThat(hazelcastMap.size()).isEqualTo(1); + assertThat(hazelcastMap.get(sessionId)).isEqualTo(sessionToSave); + + repository.delete(sessionId); + + assertThat(hazelcastMap.size()).isEqualTo(0); + } + + @EnableHazelcastHttpSession + @Configuration + static class HazelcastSessionConfig { + + @Bean + public HazelcastInstance embeddedHazelcast() { + Config hazelcastConfig = new Config(); + NetworkConfig netConfig = new NetworkConfig(); + netConfig.setPort(SocketUtils.findAvailableTcpPort()); + hazelcastConfig.setNetworkConfig(netConfig); + return Hazelcast.newHazelcastInstance(hazelcastConfig); + } + } + +} diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java b/spring-session/src/integration-test/java/org/springframework/session/data/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java new file mode 100644 index 0000000..053ff75 --- /dev/null +++ b/spring-session/src/integration-test/java/org/springframework/session/data/hazelcast/config/annotation/web/http/EnableHazelcastHttpSessionEventsTests.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2015 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 org.springframework.session.data.hazelcast.config.annotation.web.http; + +import static org.fest.assertions.Assertions.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.session.ExpiringSession; +import org.springframework.session.Session; +import org.springframework.session.SessionRepository; +import org.springframework.session.data.SessionEventRegistry; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionExpiredEvent; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.util.SocketUtils; + +import com.hazelcast.config.Config; +import com.hazelcast.config.NetworkConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; + +/** + * Ensure that the appropriate SessionEvents are fired at the expected times. + * Additionally ensure that the interactions with the {@link SessionRepository} + * abstraction behave as expected after each SessionEvent. + * + * @author Tommy Ludwig + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@WebAppConfiguration +public class EnableHazelcastHttpSessionEventsTests { + + private final static String MAX_INACTIVE_INTERVAL_IN_SECONDS_STR = "1"; + private final static int MAX_INACTIVE_INTERVAL_IN_SECONDS = Integer.valueOf(MAX_INACTIVE_INTERVAL_IN_SECONDS_STR); + + @Autowired + private SessionRepository repository; + + @Autowired + private SessionEventRegistry registry; + + private final Object lock = new Object(); + + @Before + public void setup() { + registry.clear(); + registry.setLock(lock); + } + + @Test + public void saveSessionTest() throws InterruptedException { + String username = "saves-"+System.currentTimeMillis(); + + S sessionToSave = repository.createSession(); + + String expectedAttributeName = "a"; + String expectedAttributeValue = "b"; + sessionToSave.setAttribute(expectedAttributeName, expectedAttributeValue); + Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username,"password", AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext(); + toSaveContext.setAuthentication(toSaveToken); + sessionToSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext); + sessionToSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, username); + + repository.save(sessionToSave); + + assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionCreatedEvent.class); + + Session session = repository.getSession(sessionToSave.getId()); + + assertThat(session.getId()).isEqualTo(sessionToSave.getId()); + assertThat(session.getAttributeNames()).isEqualTo(sessionToSave.getAttributeNames()); + assertThat(session.getAttribute(expectedAttributeName)).isEqualTo(sessionToSave.getAttribute(expectedAttributeName)); + } + + @Test + public void expiredSessionTest() throws InterruptedException { + S sessionToSave = repository.createSession(); + + repository.save(sessionToSave); + + assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionCreatedEvent.class); + registry.clear(); + + assertThat(sessionToSave.getMaxInactiveIntervalInSeconds()).isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS); + + synchronized (lock) { + lock.wait((sessionToSave.getMaxInactiveIntervalInSeconds() * 1000) + 1); + } + + assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionExpiredEvent.class); + + assertThat(repository.getSession(sessionToSave.getId())).isNull(); + } + + @Test + public void deletedSessionTest() throws InterruptedException { + S sessionToSave = repository.createSession(); + + repository.save(sessionToSave); + + assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionCreatedEvent.class); + registry.clear(); + + repository.delete(sessionToSave.getId()); + + assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionDeletedEvent.class); + + assertThat(repository.getSession(sessionToSave.getId())).isNull(); + } + + + @Configuration + @EnableHazelcastHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS_STR) + static class HazelcastSessionConfig { + + @Bean + public HazelcastInstance embeddedHazelcast() { + Config hazelcastConfig = new Config(); + NetworkConfig netConfig = new NetworkConfig(); + netConfig.setPort(SocketUtils.findAvailableTcpPort()); + hazelcastConfig.setNetworkConfig(netConfig); + return Hazelcast.newHazelcastInstance(hazelcastConfig); + } + + @Bean + public SessionEventRegistry sessionEventRegistry() { + return new SessionEventRegistry(); + } + } + +} diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/redis/RedisOperationsSessionRepositoryITests.java b/spring-session/src/integration-test/java/org/springframework/session/data/redis/RedisOperationsSessionRepositoryITests.java index 2509075..347a5a0 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/data/redis/RedisOperationsSessionRepositoryITests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/data/redis/RedisOperationsSessionRepositoryITests.java @@ -24,7 +24,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; @@ -35,9 +34,9 @@ import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.session.Session; +import org.springframework.session.data.SessionEventRegistry; import org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; -import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionDestroyedEvent; import org.springframework.test.context.ContextConfiguration; @@ -88,7 +87,7 @@ public class RedisOperationsSessionRepositoryITests { Session session = repository.getSession(toSave.getId()); assertThat(session.getId()).isEqualTo(toSave.getId()); - assertThat(session.getAttributeNames()).isEqualTo(session.getAttributeNames()); + assertThat(session.getAttributeNames()).isEqualTo(toSave.getAttributeNames()); assertThat(session.getAttribute(expectedAttributeName)).isEqualTo(toSave.getAttribute(expectedAttributeName)); registry.clear(); @@ -146,41 +145,6 @@ public class RedisOperationsSessionRepositoryITests { assertThat(findByPrincipalName.keySet()).excludes(toSave.getId()); } - static class SessionEventRegistry implements ApplicationListener { - private AbstractSessionEvent event; - private final Object lock = new Object(); - - public void onApplicationEvent(AbstractSessionEvent event) { - this.event = event; - synchronized (lock) { - lock.notifyAll(); - } - } - - public void clear() { - this.event = null; - } - - public boolean receivedEvent() throws InterruptedException { - return waitForEvent() != null; - } - - @SuppressWarnings("unchecked") - public E getEvent() throws InterruptedException { - return (E) waitForEvent(); - } - - @SuppressWarnings("unchecked") - private E waitForEvent() throws InterruptedException { - synchronized(lock) { - if(event == null) { - lock.wait(3000); - } - } - return (E) event; - } - } - @Configuration @EnableRedisHttpSession(redisNamespace = "RedisOperationsSessionRepositoryITests") static class Config { diff --git a/spring-session/src/main/java/org/springframework/session/data/hazelcast/SessionEntryListener.java b/spring-session/src/main/java/org/springframework/session/data/hazelcast/SessionEntryListener.java new file mode 100644 index 0000000..ade32b7 --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/data/hazelcast/SessionEntryListener.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2015 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 org.springframework.session.data.hazelcast; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.session.ExpiringSession; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionExpiredEvent; +import org.springframework.util.Assert; + +import com.hazelcast.core.EntryEvent; +import com.hazelcast.map.listener.EntryAddedListener; +import com.hazelcast.map.listener.EntryEvictedListener; +import com.hazelcast.map.listener.EntryRemovedListener; + +/** + * Listen for events on the Hazelcast-backed SessionRepository and + * translate those events into the corresponding Spring Session events. + * Publish the Spring Session events with the given {@link ApplicationEventPublisher}. + *
    + *
  • entryAdded --> {@link SessionCreatedEvent}
  • + *
  • entryEvicted --> {@link SessionExpiredEvent}
  • + *
  • entryRemoved --> {@link SessionDeletedEvent}
  • + *
+ * + * @author Tommy Ludwig + * @author Mark Anderson + * @since 1.1 + */ +public class SessionEntryListener implements EntryAddedListener, + EntryEvictedListener, EntryRemovedListener { + private static final Log logger = LogFactory.getLog(SessionEntryListener.class); + + private ApplicationEventPublisher eventPublisher; + + public SessionEntryListener(ApplicationEventPublisher eventPublisher) { + Assert.notNull(eventPublisher, "eventPublisher cannot be null"); + this.eventPublisher = eventPublisher; + } + + public void entryAdded(EntryEvent event) { + logger.debug("Session created with id: " + event.getValue().getId()); + this.eventPublisher.publishEvent(new SessionCreatedEvent(this, event.getValue())); + } + + public void entryEvicted(EntryEvent event) { + logger.debug("Session expired with id: " + event.getOldValue().getId()); + this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue())); + } + + public void entryRemoved(EntryEvent event) { + logger.debug("Session deleted with id: " + event.getOldValue().getId()); + this.eventPublisher.publishEvent(new SessionDeletedEvent(this, event.getOldValue())); + } + +} diff --git a/spring-session/src/main/java/org/springframework/session/data/hazelcast/config/annotation/web/http/EnableHazelcastHttpSession.java b/spring-session/src/main/java/org/springframework/session/data/hazelcast/config/annotation/web/http/EnableHazelcastHttpSession.java new file mode 100644 index 0000000..ac5ce57 --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/data/hazelcast/config/annotation/web/http/EnableHazelcastHttpSession.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2015 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 org.springframework.session.data.hazelcast.config.annotation.web.http; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; + +/** + * Add this annotation to a {@code @Configuration} class to expose the + * SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and + * backed by Hazelcast. In order to leverage the annotation, a single {@link HazelcastInstance} + * must be provided. For example: + *
+ * 
+ * {@literal @Configuration}
+ * {@literal @EnableHazelcastHttpSession}
+ * public class HazelcastHttpSessionConfig {
+ *
+ *     {@literal @Bean}
+ *     public HazelcastInstance embeddedHazelcast() {
+ *         Config hazelcastConfig = new Config();
+ *         return Hazelcast.newHazelcastInstance(hazelcastConfig);
+ *     }
+ *
+ * }
+ * 
+ * 
+ * + * More advanced configurations can extend {@link HazelcastHttpSessionConfiguration} instead. + * + * @author Tommy Ludwig + * @since 1.1 + * @see EnableSpringHttpSession + */ +@Retention(value=java.lang.annotation.RetentionPolicy.RUNTIME) +@Target(value={java.lang.annotation.ElementType.TYPE}) +@Documented +@Import(HazelcastHttpSessionConfiguration.class) +@Configuration +public @interface EnableHazelcastHttpSession { + /** + * This is the session timeout in seconds. By default, it is set to 1800 seconds (30 minutes). + * This should be a non-negative integer. + *

If you wish to use external configuration (outside of this annotation) to set this value, you can + * set this to "" (an empty String), which will prevent this configuration from overriding + * the external configuration for this value.

+ * + * @return the seconds a session can be inactive before expiring + */ + String maxInactiveIntervalInSeconds() default "1800"; + + /** + * This is the name of the Map that will be used in Hazelcast to store the session data. + * Default is "spring:session:sessions". + * @return the name of the Map to store the sessions in Hazelcast + */ + String sessionMapName() default "spring:session:sessions"; + +} diff --git a/spring-session/src/main/java/org/springframework/session/data/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java b/spring-session/src/main/java/org/springframework/session/data/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java new file mode 100644 index 0000000..728c69d --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/data/hazelcast/config/annotation/web/http/HazelcastHttpSessionConfiguration.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2015 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 org.springframework.session.data.hazelcast.config.annotation.web.http; + +import java.util.Map; + +import javax.annotation.PreDestroy; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.session.ExpiringSession; +import org.springframework.session.MapSessionRepository; +import org.springframework.session.SessionRepository; +import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; +import org.springframework.session.data.hazelcast.SessionEntryListener; +import org.springframework.session.web.http.SessionRepositoryFilter; +import org.springframework.util.ClassUtils; + +import com.hazelcast.config.MapConfig; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.IMap; + +/** + * Exposes the {@link SessionRepositoryFilter} as a bean named + * "springSessionRepositoryFilter". In order to use this a single + * {@link HazelcastInstance} must be exposed as a Bean. + * + * @author Tommy Ludwig + * @since 1.1 + * @see EnableHazelcastHttpSession + */ +@Configuration +public class HazelcastHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware { + + /** This is the magic value to use if you do not want this configuration + * overriding the maxIdleSeconds value for the Map backing the session data. */ + private static final String DO_NOT_CONFIGURE_INACTIVE_INTERVAL_STRING = ""; + + private ClassLoader beanClassLoader; + + private Integer maxInactiveIntervalInSeconds = 1800; + + private String sessionMapName = "spring:session:sessions"; + + private String sessionListenerUid; + + private IMap sessionsMap; + + @Bean + public SessionRepository sessionRepository(HazelcastInstance hazelcastInstance, SessionEntryListener sessionListener) { + configureSessionMap(hazelcastInstance); + this.sessionsMap = hazelcastInstance.getMap(sessionMapName); + this.sessionListenerUid = this.sessionsMap.addEntryListener(sessionListener, true); + + MapSessionRepository sessionRepository = new MapSessionRepository(this.sessionsMap); + sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds); + + return sessionRepository; + } + + @PreDestroy + private void removeSessionListener() { + this.sessionsMap.removeEntryListener(this.sessionListenerUid); + } + + @Bean + public SessionEntryListener sessionListener(ApplicationEventPublisher eventPublisher) { + return new SessionEntryListener(eventPublisher); + } + + /** + * Make a {@link MapConfig} for the given sessionMapName if one does not exist. + * Ensure that maxIdleSeconds is set to maxInactiveIntervalInSeconds for proper session expiration. + * + * @param hazelcastInstance the {@link HazelcastInstance} to configure + */ + private void configureSessionMap(HazelcastInstance hazelcastInstance) { + MapConfig sessionMapConfig = hazelcastInstance.getConfig().getMapConfig(sessionMapName); + if (this.maxInactiveIntervalInSeconds != null) { + sessionMapConfig.setMaxIdleSeconds(this.maxInactiveIntervalInSeconds); + } + } + + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map enableAttrMap = importMetadata.getAnnotationAttributes(EnableHazelcastHttpSession.class.getName()); + AnnotationAttributes enableAttrs = AnnotationAttributes.fromMap(enableAttrMap); + if (enableAttrs == null) { + // search parent classes + Class currentClass = ClassUtils.resolveClassName(importMetadata.getClassName(), beanClassLoader); + for (Class classToInspect = currentClass; classToInspect != null; classToInspect = classToInspect.getSuperclass()) { + EnableHazelcastHttpSession enableHazelcastHttpSessionAnnotation = AnnotationUtils.findAnnotation(classToInspect, EnableHazelcastHttpSession.class); + if (enableHazelcastHttpSessionAnnotation == null) { + continue; + } + enableAttrMap = AnnotationUtils + .getAnnotationAttributes(enableHazelcastHttpSessionAnnotation); + enableAttrs = AnnotationAttributes.fromMap(enableAttrMap); + } + } + + transferAnnotationAttributes(enableAttrs); + } + + private void transferAnnotationAttributes(AnnotationAttributes enableAttrs) { + String maxInactiveIntervalString = enableAttrs.getString("maxInactiveIntervalInSeconds"); + + if (DO_NOT_CONFIGURE_INACTIVE_INTERVAL_STRING.equals(maxInactiveIntervalString)) { + this.maxInactiveIntervalInSeconds = null; + } else { + try { + this.maxInactiveIntervalInSeconds = Integer.parseInt(maxInactiveIntervalString); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException( + "@EnableHazelcastHttpSession's maxInactiveIntervalInSeconds expects an int format String but was '" + + maxInactiveIntervalString + "' instead.", nfe); + } + } + this.sessionMapName = enableAttrs.getString("sessionMapName"); + } + + public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) { + this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds; + } + + public String getSessionMapName() { + return this.sessionMapName; + } + + public void setSessionMapName(String sessionMapName) { + this.sessionMapName = sessionMapName; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + +}