上一篇文章中介绍了Spring-Session的核心原理,Filter,Session,Repository等等,传送门:spring-session(一)揭秘。html
这篇继上一篇的原理逐渐深刻Spring-Session中的事件机制原理的探索。众所周知,Servlet规范中有对HttpSession的事件的处理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,能够查看Package javax.servletjava
在Spring-Session中也有相应的Session事件机制实现,包括Session建立/过时/删除事件。git
本文主要从如下方面探索Spring-Session中事件机制github
Note:
这里的事件触发机制只介绍基于RedissSession的实现。基于内存Map实现的MapSession不支持Session事件机制。其余的Session实现这里也不作关注。web
先来看下Session事件抽象UML类图,总体掌握事件之间的依赖关系。redis
Session Event最顶层是ApplicationEvent,即Spring上下文事件对象。由此能够看出Spring-Session的事件机制是基于Spring上下文事件实现。spring
抽象的AbstractSessionEvent事件对象提供了获取Session(这里的是指Spring Session的对象)和SessionId。api
基于事件的类型,分类为:session
Tips:
Session销毁事件只是删除和过时事件的统一,并没有实际含义。oracle
事件对象只是对事件自己的抽象,描述事件的属性,如:
下面再深刻探索以上的Session事件是如何触发,从事件源到事件监听器的链路分析事件流转过程。
阅读本节前,读者应该了解Redis的Pub/Sub和KeySpace Notification,若是还不是很了解,传送门Redis Keyspace Notifications和Pub/Sub。
上节中也介绍Session Event事件基于Spring的ApplicationEvent实现。先简单认识spring上下文事件机制:
那么在Spring-Session中必然包含事件发布者ApplicationEventPublisher发布Session事件和ApplicationListener监听Session事件。
能够看出ApplicationEventPublisher发布一个事件:
@FunctionalInterface public interface ApplicationEventPublisher { /** * Notify all <strong>matching</strong> listeners registered with this * application of an application event. Events may be framework events * (such as RequestHandledEvent) or application-specific events. * @param event the event to publish * @see org.springframework.web.context.support.RequestHandledEvent */ default void publishEvent(ApplicationEvent event) { publishEvent((Object) event); } /** * Notify all <strong>matching</strong> listeners registered with this * application of an event. * <p>If the specified {@code event} is not an {@link ApplicationEvent}, * it is wrapped in a {@link PayloadApplicationEvent}. * @param event the event to publish * @since 4.2 * @see PayloadApplicationEvent */ void publishEvent(Object event); }
ApplicationListener用于监听相应的事件:
@FunctionalInterface public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { /** * Handle an application event. * @param event the event to respond to */ void onApplicationEvent(E event); }
Tips:
这里使用到了发布/订阅模式,事件监听器能够监听感兴趣的事件,发布者能够发布各类事件。不过这是内部的发布订阅,即观察者模式。
Session事件的流程实现以下:
上图展现了Spring-Session事件流程图,事件源来自于Redis键空间通知,在spring-data-redis项目中抽象MessageListener监听Redis事件源,而后将其传播至spring应用上下文发布者,由发布者发布事件。在spring上下文中的监听器Listener便可监听到Session事件。
由于二者是Spring框架提供的对Spring的ApplicationEvent的支持。Session Event基于ApplicationEvent实现,必然也有其相应发布者和监听器的的实现。
Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。全部关于RedisSession的管理操做都是由其实现,因此Session的产生源是RedisOperationSessionRepository。
在RedisOperationSessionRepository中持有ApplicationEventPublisher对象用于发布Session事件。
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { @Override public void publishEvent(ApplicationEvent event) { } @Override public void publishEvent(Object event) { } };
可是该ApplicationEventPublisher是空实现,实际实现是在应用启动时由Spring-Session自动配置。在spring-session-data-redis模块中RedisHttpSessionConfiguration中有关于建立RedisOperationSessionRepository Bean时将调用set方法将ApplicationEventPublisher配置。
@Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer { private ApplicationEventPublisher applicationEventPublisher; @Bean public RedisOperationsSessionRepository sessionRepository() { RedisTemplate<Object, Object> redisTemplate = createRedisTemplate(); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository( redisTemplate); // 注入依赖 sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher); if (this.defaultRedisSerializer != null) { sessionRepository.setDefaultSerializer(this.defaultRedisSerializer); } sessionRepository .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds); if (StringUtils.hasText(this.redisNamespace)) { sessionRepository.setRedisKeyNamespace(this.redisNamespace); } sessionRepository.setRedisFlushMode(this.redisFlushMode); return sessionRepository; } // 注入上下文中的ApplicationEventPublisher Bean @Autowired public void setApplicationEventPublisher( ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } }
在进行自动配置时,将上下文中的ApplicationEventPublisher的注入,实际上即ApplicationContext对象。
Note:
考虑篇幅缘由,以上的RedisHttpSessionConfiguration至展现片断。
对于ApplicationListener是由应用开发者自行实现,注册成Bean便可。当有Session Event发布时,便可监听。
/** * session事件监听器 * * @author huaijin */ @Component public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> { private static final String CURRENT_USER = "currentUser"; @Override public void onApplicationEvent(SessionDeletedEvent event) { Session session = event.getSession(); UserVo userVo = session.getAttribute(CURRENT_USER); System.out.println("Current session's user:" + userVo.toString()); } }
以上部分探索了Session事件的发布者和监听者,可是核心事件的触发发布则是由Redis的键空间通知机制触发,当有Session建立/删除/过时时,Redis键空间会通知Spring-Session应用。
RedisOperationsSessionRepository实现spring-data-redis中的MessageListener接口。
/** * Listener of messages published in Redis. * * @author Costin Leau * @author Christoph Strobl */ public interface MessageListener { /** * Callback for processing received objects through Redis. * * @param message message must not be {@literal null}. * @param pattern pattern matching the channel (if specified) - can be {@literal null}. */ void onMessage(Message message, @Nullable byte[] pattern); }
该监听器即用来监听redis发布的消息。RedisOperationsSessionRepositorys实现了该Redis键空间消息通知监听器接口,实现以下:
public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener { @Override @SuppressWarnings("unchecked") public void onMessage(Message message, byte[] pattern) { // 获取该消息发布的redis通道channel byte[] messageChannel = message.getChannel(); // 获取消息体内容 byte[] messageBody = message.getBody(); String channel = new String(messageChannel); // 若是是由Session建立通道发布的消息,则是Session建立事件 if (channel.startsWith(getSessionCreatedChannelPrefix())) { // 从消息体中载入Session Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer .deserialize(message.getBody()); // 发布建立事件 handleCreated(loaded, channel); return; } // 若是消息体不是以过时键前缀,直接返回。由于spring-session在redis中的key命名规则: // "${namespace}:sessions:expires:${sessionId}",如: // session.example:sessions:expires:a5236a19-7325-4783-b1f0-db9d4442db9a // 因此判断过时或者删除的键是否为spring-session的过时键。若是不是,多是应用中其余的键的操做,因此直接return String body = new String(messageBody); if (!body.startsWith(getExpiredKeyPrefix())) { return; } // 根据channel判断键空间的事件类型del或者expire时间 boolean isDeleted = channel.endsWith(":del"); if (isDeleted || channel.endsWith(":expired")) { int beginIndex = body.lastIndexOf(":") + 1; int endIndex = body.length(); // Redis键空间消息通知内容即操做的键,spring-session键中命名规则: // "${namespace}:sessions:expires:${sessionId}",如下是根据规则解析sessionId String sessionId = body.substring(beginIndex, endIndex); // 根据sessionId加载session RedisSession session = getSession(sessionId, true); if (session == null) { logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId); return; } if (logger.isDebugEnabled()) { logger.debug("Publishing SessionDestroyedEvent for session " + sessionId); } cleanupPrincipalIndex(session); // 发布Session delete事件 if (isDeleted) { handleDeleted(session); } else { // 不然发布Session expire事件 handleExpired(session); } } } }
下续再深刻每种事件产生的前世此生。
1.Session建立事件的触发
RedisOperationSessionRepository中保存一个Session时,判断Session是否新建立。
若是新建立,则向
@Override public void save(RedisSession session) { session.saveDelta(); // 判断是否为新建立的session if (session.isNew()) { // 获取redis指定的channel:${namespace}:event:created:${sessionId}, // 如:session.example:event:created:82sdd-4123-o244-ps123 String sessionCreatedKey = getSessionCreatedChannel(session.getId()); // 向该通道发布session数据 this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); // 设置session为非新建立 session.setNew(false); } }
该save方法的调用是由HttpServletResponse提交时——即返回客户端响应调用,上篇文章已经详解,这里再也不赘述。关于RedisOperationSessionRepository实现MessageListener上述已经介绍,这里一样再也不赘述。
Note:
这里有点绕。我的认为RedisOperationSessionRepository发布建立而后再自己监听,主要是考虑分布式或者集群环境中SessionCreateEvent事件的处理。
2.Session删除事件的触发
Tips:
删除事件中使用到了Redis KeySpace Notification,建议先了解该技术。
当调用HttpSession的invalidate方法让Session失效时,即会调用RedisOperationSessionRepository的deleteById方法删除Session的过时键。
/** * Allows creating an HttpSession from a Session instance. * * @author Rob Winch * @since 1.0 */ private final class HttpSessionWrapper extends HttpSessionAdapter<S> { HttpSessionWrapper(S session, ServletContext servletContext) { super(session, servletContext); } @Override public void invalidate() { super.invalidate(); SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true; setCurrentSession(null); clearRequestedSessionCache(); // 调用删除方法 SessionRepositoryFilter.this.sessionRepository.deleteById(getId()); } }
上篇中介绍了包装Spring Session为HttpSession,这里再也不赘述。这里重点分析deleteById内容:
@Override public void deleteById(String sessionId) { // 若是session为空则返回 RedisSession session = getSession(sessionId, true); if (session == null) { return; } cleanupPrincipalIndex(session); this.expirationPolicy.onDelete(session); // 获取session的过时键 String expireKey = getExpiredKey(session.getId()); // 删除过时键,redis键空间产生del事件消息,被MessageListener即 // RedisOperationSessionRepository监听 this.sessionRedisOperations.delete(expireKey); session.setMaxInactiveInterval(Duration.ZERO); save(session); }
后续流程同SessionCreateEvent流程。
3.Session失效事件的触发
Session的过时事件流程比较特殊,由于Redis的键空间通知的特殊性,Redis键空间通知不能保证过时键的通知的及时性。
@Scheduled(cron = "0 * * * * *") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
定时任务每整分运行,执行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy实例,是RedisSession过时策略。
public void cleanExpiredSessions() { // 获取当前时间戳 long now = System.currentTimeMillis(); // 时间滚动至整分,去掉秒和毫秒部分 long prevMin = roundDownMinute(now); if (logger.isDebugEnabled()) { logger.debug("Cleaning up sessions expiring at " + new Date(prevMin)); } // 根据整分时间获取过时键集合,如:spring:session:expirations:1439245080000 String expirationKey = getExpirationKey(prevMin); // 获取全部的全部的过时session Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members(); // 删除过时Session键集合 this.redis.delete(expirationKey); // touch访问全部已通过期的session,触发Redis键空间通知消息 for (Object session : sessionsToExpire) { String sessionKey = getSessionKey((String) session); touch(sessionKey); } }
将时间戳滚动至整分
static long roundDownMinute(long timeInMs) { Calendar date = Calendar.getInstance(); date.setTimeInMillis(timeInMs); // 清理时间错的秒位和毫秒位 date.clear(Calendar.SECOND); date.clear(Calendar.MILLISECOND); return date.getTimeInMillis(); }
获取过时Session的集合
String getExpirationKey(long expires) { return this.redisSession.getExpirationsKey(expires); } // 如:spring:session:expirations:1439245080000 String getExpirationsKey(long expiration) { return this.keyPrefix + "expirations:" + expiration; }
调用Redis的Exists命令,访问过时Session键,触发Redis键空间消息
/** * By trying to access the session we only trigger a deletion if it the TTL is * expired. This is done to handle * https://github.com/spring-projects/spring-session/issues/93 * * @param key the key */ private void touch(String key) { this.redis.hasKey(key); }
至此Spring-Session的Session事件通知模块就已经很清晰: