Add SessionExpiredEvent and SessionDeletedEvent

Fixes gh-258
This commit is contained in:
Mark Anderson
2015-08-09 22:46:25 +01:00
committed by Rob Winch
parent 249361bb7f
commit 7518306975
11 changed files with 156 additions and 49 deletions

View File

@@ -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:<session-id> 1800
Spring Session relies on the expired and delete http://redis.io/topics/notifications[keyspace notifications] from Redis to fire a <<SessionDestroyedEvent>>.
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 <<api-redisoperationssessionrepository-sessiondestroyedevent,SessionDeletedEvent>> and <<api-redisoperationssessionrepository-sessiondestroyedevent,SessionExpiredEvent>> 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:<session-id> sessionAttr:<attrName2> newValue
EXPIRE spring:session:sessions:<session-id> 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:

View File

@@ -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<S extends Session> {
private SessionRepository<S> repository;
@Autowired
private SessionDestroyedEventRegistry registry;
private SessionDeletedEventRegistry registry;
private final Object lock = new Object();
@@ -100,11 +100,11 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
assertThat(session.getAttribute("1")).isEqualTo("2");
}
static class SessionDestroyedEventRegistry implements ApplicationListener<SessionDestroyedEvent> {
static class SessionDeletedEventRegistry implements ApplicationListener<SessionDeletedEvent> {
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<S extends Session> {
}
@Bean
public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
return new SessionDestroyedEventRegistry();
public SessionDeletedEventRegistry sessionDestroyedEventRegistry() {
return new SessionDeletedEventRegistry();
}
}
}

View File

@@ -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<S extends Expirin
private SessionRepository<S> repository;
@Autowired
private SessionDestroyedEventRegistry registry;
private SessionExpiredEventRegistry registry;
private final Object lock = new Object();
@@ -56,7 +56,7 @@ public class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Expirin
}
@Test
public void expireFiresSessionDestroyedEvent() throws InterruptedException {
public void expireFiresSessionExpiredEvent() throws InterruptedException {
S toSave = repository.createSession();
toSave.setAttribute("a", "b");
Authentication toSaveToken = new UsernamePasswordAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
@@ -83,11 +83,11 @@ public class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Expirin
assertThat(registry.receivedEvent()).isTrue();
}
static class SessionDestroyedEventRegistry implements ApplicationListener<SessionDestroyedEvent> {
static class SessionExpiredEventRegistry implements ApplicationListener<SessionExpiredEvent> {
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<S extends Expirin
}
@Bean
public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
return new SessionDestroyedEventRegistry();
public SessionExpiredEventRegistry sessionDestroyedEventRegistry() {
return new SessionExpiredEventRegistry();
}
}
}

View File

@@ -15,7 +15,8 @@
*/
package org.springframework.session;
import org.springframework.session.events.SessionDestroyedEvent;
import org.springframework.session.events.SessionDeletedEvent;
import org.springframework.session.events.SessionExpiredEvent;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -26,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
* distributed maps provided by NoSQL stores like Redis and Hazelcast.
*
* <p>
* The implementation does NOT support firing {@link SessionDestroyedEvent}.
* The implementation does NOT support firing {@link SessionDeletedEvent} or {@link SessionExpiredEvent}.
* </p>
*
* @author Rob Winch

View File

@@ -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}.
* </p>
*
* <h2>Creating a new instance</h2>

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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<SessionDestroyedEvent> event;
ArgumentCaptor<SessionDestroyedEvent> deletedEvent;
@Captor
ArgumentCaptor<SessionExpiredEvent> 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

View File

@@ -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<String, Object> 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));