diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 941e1f3..6970573 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -284,7 +284,7 @@ Instead, developers should prefer interacting with `SessionRepository` and `Sess `RedisOperationsSessionRepository` is a `SessionRepository` that is implemented using Spring Data's `RedisOperations`. In a web environment, this is typically used in combination with `SessionRepositoryFilter`. -The implementation supports `SessionDeletedEvent` and `SessionExpiredEvent` through `SessionMessageListener`. +The implementation supports `SessionDestroyedEvent` and `SessionCreatedEvent` through `SessionMessageListener`. [[api-redisoperationssessionrepository-new]] ==== Instantiating a RedisOperationsSessionRepository @@ -453,6 +453,14 @@ XML Configuraiton can use the following: include::{docs-test-resources-dir}docs/HttpSessionConfigurationNoOpConfigureRedisActionXmlTests-context.xml[tags=configure-redis-action] ---- +[[api-redisoperationssessionrepository-sessioncreatedevent]] +==== SessionCreatedEvent + +When a session is created an event is sent to Redis with the channel of `spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe` +such that `33fdd1b6-b496-4b33-9f7d-df96679d32fe` is the session id. The body of the event will be the session that was created. + +If registered as a MessageListener (default), then `RedisOperationsSessionRepository` will then translate the Redis message into a `SessionCreatedEvent`. + [[api-redisoperationssessionrepository-cli]] ==== Viewing the Session in Redis 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 eb19951..d6b95d9 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 @@ -33,6 +33,8 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.session.Session; import org.springframework.session.SessionRepository; 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; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -46,14 +48,7 @@ public class RedisOperationsSessionRepositoryITests { private SessionRepository repository; @Autowired - private SessionDestroyedEventRegistry registry; - - private final Object lock = new Object(); - - @Before - public void setup() { - registry.setLock(lock); - } + private SessionEventRegistry registry; @Test public void saves() throws InterruptedException { @@ -65,22 +60,25 @@ public class RedisOperationsSessionRepositoryITests { SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext(); toSaveContext.setAuthentication(toSaveToken); toSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext); + registry.clear(); repository.save(toSave); + assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionCreatedEvent.class); + Session session = repository.getSession(toSave.getId()); assertThat(session.getId()).isEqualTo(toSave.getId()); assertThat(session.getAttributeNames()).isEqualTo(session.getAttributeNames()); assertThat(session.getAttribute(expectedAttributeName)).isEqualTo(toSave.getAttribute(expectedAttributeName)); + registry.clear(); + repository.delete(toSave.getId()); assertThat(repository.getSession(toSave.getId())).isNull(); - synchronized (lock) { - lock.wait(3000); - } - assertThat(registry.receivedEvent()).isTrue(); + assertThat(registry.getEvent()).isInstanceOf(SessionDestroyedEvent.class); assertThat(registry.getEvent().getSession().getAttribute(expectedAttributeName)).isEqualTo(expectedAttributeValue); @@ -105,27 +103,38 @@ public class RedisOperationsSessionRepositoryITests { assertThat(session.getAttribute("1")).isEqualTo("2"); } - static class SessionDestroyedEventRegistry implements ApplicationListener { - private SessionDestroyedEvent event; - private Object lock; + static class SessionEventRegistry implements ApplicationListener { + private AbstractSessionEvent event; + private final Object lock = new Object(); - public void onApplicationEvent(SessionDestroyedEvent event) { + public void onApplicationEvent(AbstractSessionEvent event) { this.event = event; synchronized (lock) { lock.notifyAll(); } } - public boolean receivedEvent() { - return this.event != null; + public void clear() { + this.event = null; } - public SessionDestroyedEvent getEvent() { - return event; + public boolean receivedEvent() throws InterruptedException { + return waitForEvent() != null; } - public void setLock(Object lock) { - this.lock = lock; + @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; } } @@ -140,8 +149,8 @@ public class RedisOperationsSessionRepositoryITests { } @Bean - public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() { - return new SessionDestroyedEventRegistry(); + public SessionEventRegistry sessionEventRegistry() { + return new SessionEventRegistry(); } } } \ No newline at end of file diff --git a/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java index 258b1c1..43e44d2 100644 --- a/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java @@ -30,13 +30,17 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.session.ExpiringSession; import org.springframework.session.MapSession; import org.springframework.session.Session; import org.springframework.session.SessionRepository; +import org.springframework.session.events.SessionCreatedEvent; import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionDestroyedEvent; import org.springframework.session.events.SessionExpiredEvent; import org.springframework.session.web.http.SessionRepositoryFilter; import org.springframework.util.Assert; @@ -126,6 +130,21 @@ import org.springframework.util.Assert; * HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue * * + *

SessionCreatedEvent

+ * + *

+ * When a session is created an event is sent to Redis with the channel of + * "spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe" such + * that "33fdd1b6-b496-4b33-9f7d-df96679d32fe" is the sesion id. The body of the + * event will be the session that was created. + *

+ * + *

+ * If registered as a {@link MessageListener}, then + * {@link RedisOperationsSessionRepository} will then translate the Redis + * message into a {@link SessionCreatedEvent}. + *

+ * *

Expiration

* *

@@ -156,11 +175,11 @@ import org.springframework.util.Assert; *

* Spring Session relies on the expired and delete * keyspace notifications - * from Redis to fire a SessionDestroyedEvent. It is the - * SessionDestroyedEvent that ensures resources associated with the Session - * are cleaned up. For example, when using Spring Session's WebSocket support - * the Redis expired or delete event is what triggers any WebSocket connections - * associated with the session to be closed. + * from Redis to fire a SessionDestroyedEvent. It is the SessionDestroyedEvent + * that ensures resources associated with the Session are cleaned up. For + * example, when using Spring Session's WebSocket support the Redis expired or + * delete event is what triggers any WebSocket connections associated with the + * session to be closed. *

* *

@@ -228,6 +247,11 @@ import org.springframework.util.Assert; * @author Rob Winch */ public class RedisOperationsSessionRepository implements SessionRepository, MessageListener { + /** + * The prefix for SessionCreated event channel. The suffix is the session id. + */ + private static final String SPRING_SESSION_CREATED_PREFIX = "spring:session:event:created:"; + private static final Log logger = LogFactory.getLog(SessionMessageListener.class); private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { @@ -318,6 +342,10 @@ public class RedisOperationsSessionRepository implements SessionRepository entries) { MapSession loaded = new MapSession(); loaded.setId(id); for(Map.Entry entry : entries.entrySet()) { @@ -357,13 +396,7 @@ public class RedisOperationsSessionRepository implements SessionRepository serializer = new JdkSerializationRedisSerializer(); + Map loaded = (Map) serializer.deserialize(message.getBody()); + handleCreated(loaded, channel); + return; + } + String body = new String(messageBody); if(!body.startsWith("spring:session:sessions:expires")) { return; @@ -430,6 +469,12 @@ public class RedisOperationsSessionRepository implements SessionRepository loaded, String channel) { + String id = channel.substring(channel.lastIndexOf(":")); + ExpiringSession session = loadSession(id, loaded); + publishEvent(new SessionCreatedEvent(this, session)); + } + private void handleDeleted(String sessionId, RedisSession session) { if(session == null) { publishEvent(new SessionDeletedEvent(this, sessionId)); @@ -508,6 +553,7 @@ public class RedisOperationsSessionRepository implements SessionRepository delta = new HashMap(); + private boolean isNew; /** * Creates a new instance ensuring to mark all of the new attributes to be persisted in the next save operation. @@ -517,6 +563,7 @@ public class RedisOperationsSessionRepository implements SessionRepository S getSession() { + return (S) session; + } + + public String getSessionId() { + return sessionId; + } +} diff --git a/spring-session/src/main/java/org/springframework/session/events/SessionCreatedEvent.java b/spring-session/src/main/java/org/springframework/session/events/SessionCreatedEvent.java new file mode 100644 index 0000000..77fba9d --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/events/SessionCreatedEvent.java @@ -0,0 +1,45 @@ +/* + * 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.events; + +import org.springframework.session.Session; +import org.springframework.session.SessionRepository; + +/** + * For {@link SessionRepository} implementations that support it, this event is + * fired when a {@link Session} is destroyed either explicitly or via + * expiration. + * + * @author Rob Winch + * @since 1.0 + * + */ +@SuppressWarnings("serial") +public class SessionCreatedEvent extends AbstractSessionEvent { + + public SessionCreatedEvent(Object source, String sessionId) { + super(source, sessionId); + } + + /** + * @param source + * @param session + */ + public SessionCreatedEvent(Object source, Session session) { + super(source, session); + } + +} \ No newline at end of file diff --git a/spring-session/src/main/java/org/springframework/session/events/SessionDestroyedEvent.java b/spring-session/src/main/java/org/springframework/session/events/SessionDestroyedEvent.java index d0e7dbc..c70ae58 100644 --- a/spring-session/src/main/java/org/springframework/session/events/SessionDestroyedEvent.java +++ b/spring-session/src/main/java/org/springframework/session/events/SessionDestroyedEvent.java @@ -15,7 +15,6 @@ */ package org.springframework.session.events; -import org.springframework.context.ApplicationEvent; import org.springframework.session.Session; /** @@ -26,36 +25,17 @@ import org.springframework.session.Session; * */ @SuppressWarnings("serial") -public class SessionDestroyedEvent extends ApplicationEvent { - private final String sessionId; - - private final Session session; +public class SessionDestroyedEvent extends AbstractSessionEvent { public SessionDestroyedEvent(Object source, String sessionId) { - super(source); - this.sessionId = sessionId; - this.session = null; - } - - public SessionDestroyedEvent(Object source, Session session) { - super(source); - this.session = session; - this.sessionId = session.getId(); + super(source, sessionId); } /** - * Gets the {@link Session} that was destroyed. For some - * {@link SessionRepository} implementations it may not be possible to get - * the original session in which case this may be null. - * - * @return the expired {@link Session} or null if the data store does not support obtaining it + * @param source + * @param session */ - @SuppressWarnings("unchecked") - public S getSession() { - return (S) session; - } - - public String getSessionId() { - return sessionId; + public SessionDestroyedEvent(Object source, Session session) { + super(source, session); } } \ No newline at end of file