Add SessionCreatedEvent

Fixes gh-261
This commit is contained in:
Rob Winch
2015-08-11 22:24:49 -05:00
parent 6e1ecadcc4
commit d27d9a22bf
7 changed files with 231 additions and 69 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 `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

View File

@@ -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<S extends Session> {
private SessionRepository<S> 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<S extends Session> {
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<S extends Session> {
assertThat(session.getAttribute("1")).isEqualTo("2");
}
static class SessionDestroyedEventRegistry implements ApplicationListener<SessionDestroyedEvent> {
private SessionDestroyedEvent event;
private Object lock;
static class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
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 extends AbstractSessionEvent> E getEvent() throws InterruptedException {
return (E) waitForEvent();
}
@SuppressWarnings("unchecked")
private <E extends AbstractSessionEvent> E waitForEvent() throws InterruptedException {
synchronized(lock) {
if(event == null) {
lock.wait(3000);
}
}
return (E) event;
}
}
@@ -140,8 +149,8 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
}
@Bean
public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
return new SessionDestroyedEventRegistry();
public SessionEventRegistry sessionEventRegistry() {
return new SessionEventRegistry();
}
}
}

View File

@@ -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
* </pre>
*
* <h3>SessionCreatedEvent</h3>
*
* <p>
* 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.
* </p>
*
* <p>
* If registered as a {@link MessageListener}, then
* {@link RedisOperationsSessionRepository} will then translate the Redis
* message into a {@link SessionCreatedEvent}.
* </p>
*
* <h3>Expiration</h3>
*
* <p>
@@ -156,11 +175,11 @@ import org.springframework.util.Assert;
* <p>
* Spring Session relies on the expired and delete
* <a href="http://redis.io/topics/notifications">keyspace notifications</a>
* 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.
* </p>
*
* <p>
@@ -228,6 +247,11 @@ import org.springframework.util.Assert;
* @author Rob Winch
*/
public class RedisOperationsSessionRepository implements SessionRepository<RedisOperationsSessionRepository.RedisSession>, 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<Redis
public void save(RedisSession session) {
session.saveDelta();
if(session.isNew()) {
this.sessionRedisOperations.convertAndSend(SPRING_SESSION_CREATED_PREFIX + session.getId(), session.delta);
session.setNew(false);
}
}
@Scheduled(cron="0 * * * * *")
@@ -343,6 +371,17 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
if(entries.isEmpty()) {
return null;
}
MapSession loaded = loadSession(id, entries);
if(!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime() + TimeUnit.SECONDS.toMillis(loaded.getMaxInactiveIntervalInSeconds());
result.setLastAccessedTime(System.currentTimeMillis());
return result;
}
private MapSession loadSession(String id, Map<Object, Object> entries) {
MapSession loaded = new MapSession();
loaded.setId(id);
for(Map.Entry<Object,Object> entry : entries.entrySet()) {
@@ -357,13 +396,7 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue());
}
}
if(!allowExpired && loaded.isExpired()) {
return null;
}
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime() + TimeUnit.SECONDS.toMillis(loaded.getMaxInactiveIntervalInSeconds());
result.setLastAccessedTime(System.currentTimeMillis());
return result;
return loaded;
}
public void delete(String sessionId) {
@@ -392,8 +425,6 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
}
return redisSession;
}
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
@@ -403,6 +434,14 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
String channel = new String(messageChannel);
if(channel.startsWith(SPRING_SESSION_CREATED_PREFIX)) {
RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
Map<Object,Object> loaded = (Map<Object, Object>) 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<Redis
}
}
public void handleCreated(Map<Object,Object> 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<Redis
private final MapSession cached;
private Long originalLastAccessTime;
private Map<String, Object> delta = new HashMap<String,Object>();
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<Redis
delta.put(CREATION_TIME_ATTR, getCreationTime());
delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
this.isNew = true;
}
@@ -531,6 +578,10 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
this.cached = cached;
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
public void setLastAccessedTime(long lastAccessedTime) {
cached.setLastAccessedTime(lastAccessedTime);
delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
@@ -540,6 +591,10 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
return cached.isExpired();
}
public boolean isNew() {
return isNew;
}
public long getCreationTime() {
return cached.getCreationTime();
}

View File

@@ -31,18 +31,19 @@ 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.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.SessionMessageListener;
import org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.web.http.HttpSessionStrategy;
@@ -73,11 +74,13 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory, RedisOperationsSessionRepository redisSessionMessageListener) {
RedisConnectionFactory connectionFactory, RedisOperationsSessionRepository messageListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(redisSessionMessageListener,
Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));
container.addMessageListener(messageListener,
Arrays.asList(new PatternTopic("__keyevent@*:del"), new PatternTopic("__keyevent@*:expired")));
container.addMessageListener(messageListener, Arrays.asList(new PatternTopic("spring:session:event:created:*")));
return container;
}

View File

@@ -0,0 +1,62 @@
/*
* 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.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 updated.
*
* @author Rob Winch
* @since 1.1
*/
@SuppressWarnings("serial")
public abstract class AbstractSessionEvent extends ApplicationEvent {
private final String sessionId;
private final Session session;
protected AbstractSessionEvent(Object source, String sessionId) {
super(source);
this.sessionId = sessionId;
this.session = null;
}
AbstractSessionEvent(Object source, Session session) {
super(source);
this.session = session;
this.sessionId = session.getId();
}
/**
* 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
*/
@SuppressWarnings("unchecked")
public <S extends Session> S getSession() {
return (S) session;
}
public String getSessionId() {
return sessionId;
}
}

View File

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

View File

@@ -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 extends Session> S getSession() {
return (S) session;
}
public String getSessionId() {
return sessionId;
public SessionDestroyedEvent(Object source, Session session) {
super(source, session);
}
}