Add SessionExpiredEvent and SessionDeletedEvent
Fixes gh-258
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user