diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc
index 857f6b8..757d071 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 `SessionDestroyedEvent` through `SessionMessageListener`.
+The implementation supports `SessionDeletedEvent` and `SessionExpiredEvent` through `SessionMessageListener`.
[[api-redisoperationssessionrepository-new]]
==== Instantiating a RedisOperationsSessionRepository
@@ -317,8 +317,8 @@ For example:
EXPIRE spring:session:sessions: 1800
-Spring Session relies on the expired and delete http://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <>.
-It is the `SessionDestroyedEvent` that ensures resources associated with the Session are cleaned up.
+Spring Session relies on the expired and delete http://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <> and <> respectively.
+It is the `SessionDeletedEvent` or `SessionExpiredEvent` 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.
One problem with this approach is that Redis makes no guarantee of when the expired event will be fired if they key has not been accessed.
@@ -354,13 +354,16 @@ HMSET spring:session:sessions: sessionAttr: newValue
EXPIRE spring:session:sessions: 1800
[[api-redisoperationssessionrepository-sessiondestroyedevent]]
-==== SessionDestroyedEvent
+==== SessionDeletedEvent and SessionExpiredEvent
-`RedisOperationsSessionRepository` supports firing a `SessionDestroyedEvent` whenever a `Session` is deleted or when it expires.
+`SessionDeletedEvent` and `SessionExpiredEvent` are both types of `SessionDestroyedEvent`.
+
+`RedisOperationsSessionRepository` supports firing a `SessionDeletedEvent` whenever a `Session` is deleted or a `SessionExpiredEvent` when it expires.
This is necessary to ensure resources associated with the `Session` are properly cleaned up.
+
For example, when integrating with WebSockets the `SessionDestroyedEvent` is in charge of closing any active WebSocket connections.
-Firing a `SessionDestroyedEvent` is made available through the `SessionMessageListener` which listens to http://redis.io/topics/notifications[Redis Keyspace events].
+Firing `SessionDeletedEvent` or `SessionExpiredEvent` is made available through the `SessionMessageListener` which listens to http://redis.io/topics/notifications[Redis Keyspace events].
In order for this to work, Redis Keyspace events for Generic commands and Expired events needs to be enabled.
For example:
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 6a185c6..4fb3388 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,7 +33,7 @@ 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.SessionDestroyedEvent;
+import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@@ -46,7 +46,7 @@ public class RedisOperationsSessionRepositoryITests {
private SessionRepository repository;
@Autowired
- private SessionDestroyedEventRegistry registry;
+ private SessionDeletedEventRegistry registry;
private final Object lock = new Object();
@@ -100,11 +100,11 @@ public class RedisOperationsSessionRepositoryITests {
assertThat(session.getAttribute("1")).isEqualTo("2");
}
- static class SessionDestroyedEventRegistry implements ApplicationListener {
+ static class SessionDeletedEventRegistry implements ApplicationListener {
private boolean receivedEvent;
private Object lock;
- public void onApplicationEvent(SessionDestroyedEvent event) {
+ public void onApplicationEvent(SessionDeletedEvent event) {
receivedEvent = true;
synchronized (lock) {
lock.notifyAll();
@@ -131,8 +131,8 @@ public class RedisOperationsSessionRepositoryITests {
}
@Bean
- public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
- return new SessionDestroyedEventRegistry();
+ public SessionDeletedEventRegistry sessionDestroyedEventRegistry() {
+ return new SessionDeletedEventRegistry();
}
}
}
\ No newline at end of file
diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java b/spring-session/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java
index 074188b..2097ae6 100644
--- a/spring-session/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java
+++ b/spring-session/src/integration-test/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSessionExpireSessionDestroyedTests.java
@@ -33,7 +33,7 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
-import org.springframework.session.events.SessionDestroyedEvent;
+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;
@@ -46,7 +46,7 @@ public class EnableRedisHttpSessionExpireSessionDestroyedTests repository;
@Autowired
- private SessionDestroyedEventRegistry registry;
+ private SessionExpiredEventRegistry registry;
private final Object lock = new Object();
@@ -56,7 +56,7 @@ public class EnableRedisHttpSessionExpireSessionDestroyedTests {
+ static class SessionExpiredEventRegistry implements ApplicationListener {
private boolean receivedEvent;
private Object lock;
- public void onApplicationEvent(SessionDestroyedEvent event) {
+ public void onApplicationEvent(SessionExpiredEvent event) {
synchronized (lock) {
receivedEvent = true;
lock.notifyAll();
@@ -114,8 +114,8 @@ public class EnableRedisHttpSessionExpireSessionDestroyedTests
- * The implementation does NOT support firing {@link SessionDestroyedEvent}.
+ * The implementation does NOT support firing {@link SessionDeletedEvent} or {@link SessionExpiredEvent}.
*
*
* @author Rob Winch
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 7d33747..23c5b2d 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,7 +30,8 @@ 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.SessionDestroyedEvent;
+import org.springframework.session.events.SessionDeletedEvent;
+import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.Assert;
@@ -41,7 +42,7 @@ import org.springframework.util.Assert;
* {@link org.springframework.data.redis.core.RedisOperations}. In a web
* environment, this is typically used in combination with
* {@link SessionRepositoryFilter}. This implementation supports
- * {@link SessionDestroyedEvent} through {@link SessionMessageListener}.
+ * {@link SessionDeletedEvent} and {@link SessionExpiredEvent} through {@link SessionMessageListener}.
*
*
* Creating a new instance
diff --git a/spring-session/src/main/java/org/springframework/session/data/redis/SessionMessageListener.java b/spring-session/src/main/java/org/springframework/session/data/redis/SessionMessageListener.java
index a019272..0d98a0e 100644
--- a/spring-session/src/main/java/org/springframework/session/data/redis/SessionMessageListener.java
+++ b/spring-session/src/main/java/org/springframework/session/data/redis/SessionMessageListener.java
@@ -21,12 +21,14 @@ import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
-import org.springframework.session.events.SessionDestroyedEvent;
+import org.springframework.session.events.SessionDeletedEvent;
+import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.util.Assert;
/**
- * Listen for Redis {@link Message} notifications. If it is a "del" or "expired"
- * translate into a {@link SessionDestroyedEvent}.
+ * Listen for Redis {@link Message} notifications. If it is a "del"
+ * translate into a {@link SessionDeletedEvent}. If it is an "expired"
+ * translate into a {@link SessionExpiredEvent}.
*
* @author Rob Winch
* @since 1.0
@@ -69,7 +71,11 @@ public class SessionMessageListener implements MessageListener {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
- publishEvent(new SessionDestroyedEvent(this, sessionId));
+ if(channel.endsWith(":del")) {
+ publishEvent(new SessionDeletedEvent(this, sessionId));
+ } else {
+ publishEvent(new SessionExpiredEvent(this, sessionId));
+ }
}
private void publishEvent(ApplicationEvent event) {
diff --git a/spring-session/src/main/java/org/springframework/session/events/SessionDeletedEvent.java b/spring-session/src/main/java/org/springframework/session/events/SessionDeletedEvent.java
new file mode 100644
index 0000000..7ced0dd
--- /dev/null
+++ b/spring-session/src/main/java/org/springframework/session/events/SessionDeletedEvent.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 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 via deletion.
+ *
+ * @author Rob Winch
+ * @since 1.1
+ *
+ */
+@SuppressWarnings("serial")
+public class SessionDeletedEvent extends SessionDestroyedEvent {
+
+ public SessionDeletedEvent(Object source, String sessionId) {
+ super(source, sessionId);
+ }
+
+}
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 e634d0f..6534551 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
@@ -17,19 +17,16 @@ package org.springframework.session.events;
import org.springframework.context.ApplicationEvent;
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.
+ * Base class for events fired when a {@link Session} is destroyed explicitly.
*
* @author Rob Winch
* @since 1.0
*
*/
@SuppressWarnings("serial")
-public class SessionDestroyedEvent extends ApplicationEvent {
+public abstract class SessionDestroyedEvent extends ApplicationEvent {
private final String sessionId;
public SessionDestroyedEvent(Object source, String sessionId) {
diff --git a/spring-session/src/main/java/org/springframework/session/events/SessionExpiredEvent.java b/spring-session/src/main/java/org/springframework/session/events/SessionExpiredEvent.java
new file mode 100644
index 0000000..38310e4
--- /dev/null
+++ b/spring-session/src/main/java/org/springframework/session/events/SessionExpiredEvent.java
@@ -0,0 +1,35 @@
+/*
+ * 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 via expiration.
+ *
+ * @author Rob Winch
+ * @since 1.1
+ *
+ */
+@SuppressWarnings("serial")
+public class SessionExpiredEvent extends SessionDestroyedEvent {
+
+ public SessionExpiredEvent(Object source, String sessionId) {
+ super(source, sessionId);
+ }
+}
\ No newline at end of file
diff --git a/spring-session/src/test/java/org/springframework/session/data/redis/SessionMessageListenerTests.java b/spring-session/src/test/java/org/springframework/session/data/redis/SessionMessageListenerTests.java
index d2b7a4a..de575eb 100644
--- a/spring-session/src/test/java/org/springframework/session/data/redis/SessionMessageListenerTests.java
+++ b/spring-session/src/test/java/org/springframework/session/data/redis/SessionMessageListenerTests.java
@@ -31,6 +31,7 @@ import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.session.events.SessionDestroyedEvent;
+import org.springframework.session.events.SessionExpiredEvent;
/**
@@ -47,7 +48,10 @@ public class SessionMessageListenerTests {
Message message;
@Captor
- ArgumentCaptor event;
+ ArgumentCaptor deletedEvent;
+
+ @Captor
+ ArgumentCaptor expiredEvent;
byte[] pattern;
@@ -76,18 +80,28 @@ public class SessionMessageListenerTests {
listener.onMessage(message, pattern);
- verify(eventPublisher).publishEvent(event.capture());
- assertThat(event.getValue().getSessionId()).isEqualTo("123");
+ verify(eventPublisher).publishEvent(deletedEvent.capture());
+ assertThat(deletedEvent.getValue().getSessionId()).isEqualTo("123");
}
@Test
- public void onMessageSource() throws Exception {
+ public void onMessageDelSource() throws Exception {
mockMessage("__keyevent@0__:del","spring:session:sessions:123");
listener.onMessage(message, pattern);
- verify(eventPublisher).publishEvent(event.capture());
- assertThat(event.getValue().getSource()).isEqualTo(listener);
+ verify(eventPublisher).publishEvent(deletedEvent.capture());
+ assertThat(deletedEvent.getValue().getSource()).isEqualTo(listener);
+ }
+
+ @Test
+ public void onMessageExpiredSource() throws Exception {
+ mockMessage("__keyevent@0__:expired","spring:session:sessions:123");
+
+ listener.onMessage(message, pattern);
+
+ verify(eventPublisher).publishEvent(expiredEvent.capture());
+ assertThat(expiredEvent.getValue().getSource()).isEqualTo(listener);
}
@Test
@@ -96,8 +110,8 @@ public class SessionMessageListenerTests {
listener.onMessage(message, pattern);
- verify(eventPublisher).publishEvent(event.capture());
- assertThat(event.getValue().getSessionId()).isEqualTo("543");
+ verify(eventPublisher).publishEvent(expiredEvent.capture());
+ assertThat(expiredEvent.getValue().getSessionId()).isEqualTo("543");
}
@Test
diff --git a/spring-session/src/test/java/org/springframework/session/web/socket/handler/WebSocketRegistryListenerTests.java b/spring-session/src/test/java/org/springframework/session/web/socket/handler/WebSocketRegistryListenerTests.java
index 36e0bc1..bde02b2 100644
--- a/spring-session/src/test/java/org/springframework/session/web/socket/handler/WebSocketRegistryListenerTests.java
+++ b/spring-session/src/test/java/org/springframework/session/web/socket/handler/WebSocketRegistryListenerTests.java
@@ -33,7 +33,8 @@ import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
-import org.springframework.session.events.SessionDestroyedEvent;
+import org.springframework.session.events.SessionDeletedEvent;
+import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.session.web.socket.events.SessionConnectEvent;
import org.springframework.session.web.socket.server.SessionRepositoryMessageInterceptor;
import org.springframework.test.util.ReflectionTestUtils;
@@ -58,7 +59,9 @@ public class WebSocketRegistryListenerTests {
SessionDisconnectEvent disconnect;
- SessionDestroyedEvent destroyed;
+ SessionDeletedEvent deleted;
+
+ SessionExpiredEvent expired;
Map attributes;
@@ -89,24 +92,35 @@ public class WebSocketRegistryListenerTests {
connect = new SessionConnectEvent(listener,wsSession);
connect2 = new SessionConnectEvent(listener,wsSession2);
disconnect = new SessionDisconnectEvent(listener, message, wsSession.getId(), CloseStatus.NORMAL);
- destroyed = new SessionDestroyedEvent(listener, sessionId);
+ deleted = new SessionDeletedEvent(listener, sessionId);
+ expired = new SessionExpiredEvent(listener, sessionId);
}
@Test
- public void onApplicationEventConnectSessionDestroyed() throws Exception {
+ public void onApplicationEventConnectSessionDeleted() throws Exception {
listener.onApplicationEvent(connect);
- listener.onApplicationEvent(destroyed);
+ listener.onApplicationEvent(deleted);
+
+ verify(wsSession).close(WebSocketRegistryListener.SESSION_EXPIRED_STATUS);
+ }
+
+ @Test
+ public void onApplicationEventConnectSessionExpired() throws Exception {
+ listener.onApplicationEvent(connect);
+
+ listener.onApplicationEvent(expired);
verify(wsSession).close(WebSocketRegistryListener.SESSION_EXPIRED_STATUS);
}
+
@Test
- public void onApplicationEventConnectSessionDestroyedNullPrincipal() throws Exception {
+ public void onApplicationEventConnectSessionDeletedNullPrincipal() throws Exception {
when(wsSession.getPrincipal()).thenReturn(null);
listener.onApplicationEvent(connect);
- listener.onApplicationEvent(destroyed);
+ listener.onApplicationEvent(deleted);
verify(wsSession,times(0)).close(any(CloseStatus.class));
}
@@ -116,7 +130,7 @@ public class WebSocketRegistryListenerTests {
listener.onApplicationEvent(connect);
listener.onApplicationEvent(disconnect);
- listener.onApplicationEvent(destroyed);
+ listener.onApplicationEvent(deleted);
verify(wsSession,times(0)).close(any(CloseStatus.class));
}
@@ -150,7 +164,7 @@ public class WebSocketRegistryListenerTests {
listener.onApplicationEvent(connect2);
listener.onApplicationEvent(disconnect);
- listener.onApplicationEvent(destroyed);
+ listener.onApplicationEvent(deleted);
verify(wsSession2).close(WebSocketRegistryListener.SESSION_EXPIRED_STATUS);
verify(wsSession,times(0)).close(any(CloseStatus.class));