作者:怀瑾握瑜

www.cnblogs.com/lxyit/p/9672097.html

上一篇文章中介绍了Spring-Session的核心原理,Filter,Session,Repository等等,传送门:玩转SpringSession,重要知识点全面剖析!

这篇继上一篇的原理逐渐深入Spring-Session中的事件机制原理的探索。

众所周知,Servlet规范中有对HttpSession的事件的处理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,可以查看

https://docs.oracle.com/javaee/7/api/javax/servlet/package-summary.html

在Spring-Session中也有相应的Session事件机制实现,包括Session创建/过期/删除事件。

本文主要从以下方面探索Spring-Session中事件机制

  • Session事件的抽象

  • 事件的触发机制

Note:
这里的事件触发机制只介绍基于RedissSession的实现。基于内存Map实现的MapSession不支持Session事件机制。其他的Session实现这里也不做关注。

一.Session事件的抽象

先来看下Session事件抽象UML类图,整体掌握事件之间的依赖关系。

Session Event最顶层是ApplicationEvent,即Spring上下文事件对象。由此可以看出Spring-Session的事件机制是基于Spring上下文事件实现。

抽象的AbstractSessionEvent事件对象提供了获取Session(这里的是指Spring Session的对象)和SessionId。

基于事件的类型,分类为:

  1. Session创建事件

  2. Session删除事件

  3. Session过期事件

Tips:
Session销毁事件只是删除和过期事件的统一,并无实际含义。

事件对象只是对事件本身的抽象,描述事件的属性,如:

  1. 获取事件产生的源:getSource获取事件产生源

  2. 获取相应事件特性:getSession/getSessoinId获取时间关联的Session

下面再深入探索以上的Session事件是如何触发,从事件源到事件监听器的链路分析事件流转过程。

二.事件的触发机制

阅读本节前,读者应该了解Redis的Pub/Sub和KeySpace Notification。

上节中也介绍Session Event事件基于Spring的ApplicationEvent实现。先简单认识spring上下文事件机制:

  • ApplicationEventPublisher实现用于发布Spring上下文事件ApplicationEvent

  • ApplicationListener实现用于监听Spring上下文事件ApplicationEvent

  • ApplicationEvent抽象上下文事件

那么在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() {@Overridepublic void publishEvent(ApplicationEvent event) {}@Overridepublic void publishEvent(Object event) {}
};

但是该ApplicationEventPublisher是空实现,实际实现是在应用启动时由Spring-Session自动配置。在spring-session-data-redis模块中RedisHttpSessionConfiguration中有关于创建RedisOperationSessionRepository Bean时将调用set方法将ApplicationEventPublisher配置。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfigurationimplements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,SchedulingConfigurer {private ApplicationEventPublisher applicationEventPublisher;@Beanpublic 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@Autowiredpublic 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";@Overridepublic 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应用。

搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典.pdf

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 implementsFindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,MessageListener {@Override@SuppressWarnings("unchecked")public void onMessage(Message message, byte[] pattern) {// 获取该消息发布的redis通道channelbyte[] messageChannel = message.getChannel();// 获取消息体内容byte[] messageBody = message.getBody();String channel = new String(messageChannel);// 如果是由Session创建通道发布的消息,则是Session创建事件if (channel.startsWith(getSessionCreatedChannelPrefix())) {// 从消息体中载入SessionMap<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的过期键。如果不是,可能是应用中其他的键的操作,所以直接returnString 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}",以下是根据规则解析sessionIdString sessionId = body.substring(beginIndex, endIndex);// 根据sessionId加载sessionRedisSession 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创建事件的触发

  1. 由RedisOperationSessionRepository向Redis指定通道**{sessionId}**发布一个message

  2. MessageListener的实现RedisOperationSessionRepository监听到Redis指定通道**{sessionId}**的消息

  3. 将其传播至ApplicationEventPublisher

  4. ApplicationEventPublisher发布SessionCreateEvent

  5. ApplicationListener监听SessionCreateEvent,执行相应逻辑

RedisOperationSessionRepository中保存一个Session时,判断Session是否新创建。
如果新创建,则向

@Override
public void save(RedisSession session) {session.saveDelta();// 判断是否为新创建的sessionif (session.isNew()) {// 获取redis指定的channel:${namespace}:event:created:${sessionId},// 如:session.example:event:created:82sdd-4123-o244-ps123String 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,建议先了解该技术。

  1. 由RedisOperationSessionRepository删除Redis键空间中的指定Session的过期键,Redis键空间会向 __keyevent@*:del 的channel发布删除事件消息

  2. MessageListener的实现RedisOperationSessionRepository监听到Redis指定通道 __keyevent@*:del 的消息

  3. 将其传播至ApplicationEventPublisher

  4. ApplicationEventPublisher发布SessionDeleteEvent

  5. ApplicationListener监听SessionDeleteEvent,执行相应逻辑

当调用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);}@Overridepublic 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流程。搜索Java知音公众号,回复“后端面试”,送你一份Java面试题宝典.pdf

3.Session失效事件的触发

Session的过期事件流程比较特殊,因为Redis的键空间通知的特殊性,Redis键空间通知不能保证过期键的通知的及时性。

  1. RedisOperationsSessionRepository中有个定时任务方法每整分运行访问整分Session过期键集合中的过期sessionId,如:spring:session:expirations:1439245080000。触发Redis键空间会向 __keyevent@*:expired 的channel发布过期事件消息

  2. MessageListener的实现RedisOperationSessionRepository监听到Redis指定通道 __keyevent@*:expired 的消息

  3. 将其传播至ApplicationEventPublisher

  4. ApplicationEventPublisher发布SessionDeleteEvent

  5. ApplicationListener监听SessionDeleteEvent,执行相应逻辑

@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:1439245080000String expirationKey = getExpirationKey(prevMin);// 获取所有的所有的过期sessionSet<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事件通知模块就已经很清晰:

  1. Redis键空间Session事件源:Session创建通道/Session删除通道/Session过期通道

  2. Spring-Session中的RedisOperationsSessionRepository消息监听器监听Redis的事件类型

  3. RedisOperationsSessionRepository负责将其传播至ApplicationEventPublisher

  4. ApplicationEventPublisher将其包装成ApplicationEvent类型的Session Event发布

  5. ApplicationListener监听Session Event,处理相应逻辑

琐碎时间想看一些技术文章,可以去公众号菜单栏翻一翻我分类好的内容,应该对部分童鞋有帮助。同时看的过程中发现问题欢迎留言指出,不胜感谢~。另外,有想多了解哪些方面内容的可以留言(什么时候,哪篇文章下留言都行),附菜单栏截图(PS:很多人不知道公众号菜单栏是什么)

END

我知道你 “在看”

玩转SpringSession,重要知识点全面剖析(续篇)相关推荐

  1. 玩转SpringSession,重要知识点全面剖析!

    作者:怀瑾握瑜 www.cnblogs.com/lxyit/p/9672097.html 前言 在开始spring-session揭秘之前,先做下热脑(活动活动脑子)运动.主要从以下三个方面进行热脑: ...

  2. Spring-Session 基础知识点 和 源码分析(下)

    一.Spring-Session的使用场景: 场景一 .相同域名下相同项目实现session共享. 集群部署之后我们需要使用spring-session. 原因: 我们tomcat 服务器集群部署(处 ...

  3. 玩转Android之Activity详细剖析

    本文主讲了什么是Activity,它的生命周期,不对的操作,调用了什么函数.以及不同的Activity之间的跳转.数据传递等. Activity 是用户接口程序,原则上它会提供给用户一个交互式的接口功 ...

  4. Java进阶3 - 易错知识点整理(待更新)

    Java进阶3 - 易错知识点整理(待更新) 该章节是Java进阶2- 易错知识点整理的续篇: 在前一章节中介绍了 ORM框架,中间件相关的面试题,而在该章节中主要记录关于项目部署中间件,监控与性能优 ...

  5. C#玩转指针(二):预处理器、using、partial关键字与region的妙用

    欲练神功,引刀自宫.为了避免内存管理的烦恼,Java咔嚓一下,把指针砍掉了.当年.Net也追随潮流,咔嚓了一下,化名小桂子,登堂入室进了皇宫.康熙往下面一抓:咦?还在?--原来是假太监韦小宝. 打开u ...

  6. 中原工学院计算机二级证书,中原工学院@计算机等级考试二级MS_Office基础知识(常考知识点记忆).doc...

    中原工学院@计算机等级考试二级MS_Office基础知识(常考知识点记忆)剖析 计算机的发展.类型及其应用领域.计算机(computer)是一种能自动.高速进行大量算术运算和逻辑运算的电子设备. 速度 ...

  7. 火星人培训python

    画具.乐器.台式电脑,当读小学初中时,父母总是会选择其中一两件,用来培养孩子的兴趣爱好. 近年来,随着人工智能的发展,机器人从少数人的专属玩物进入到了主流消费市场,教育机器人也逐渐成为了「家用教具老三 ...

  8. python趣味编程入门 迈克 桑德斯_Python趣味编程入门

    多年以前,编程可能还只是少数人掌握的一项技能.但是随着计算机的普及和人工智能的流行,编程已经成为一项男女老幼皆可学习的技术.Python是一种面向对象的解释型程序设计语言,也是2017年很受欢迎的人工 ...

  9. java自定义字段_自定义字段的设计与实现(Java实用版)

    前言 自定义字段又叫做"开放模型",用户可以根据自已的需求,添加需要的字段,实现个性化定制. 使用自定义字段的目的,使用自定义字段解决哪些问题 如现有一套CRM系统,客户模块中客户 ...

最新文章

  1. 西北工业大学附属中学2019届高考毕业生去向,其中北大清华88人
  2. activex for chrome 网银助手_这 10 款插件让你的 Chrome 更好用
  3. Docker 原理、学习教程
  4. 2019阿里巴巴技术面试题集锦(含答案)
  5. eclipse中run运行不了_使用Eclipse编写第一个Java程序HelloWorld
  6. Quartus II 9.0sp1之功能仿真
  7. 学习zookeeper基础知识
  8. python两组数的差异 pca_python – scikit KernelPCA不稳定的结果
  9. 微信小程序图片上传一直loading中,上传没反应
  10. wi ndows防火墙,网吧的防火墙怎么关?四种方法关闭WINDOWS防火墙
  11. 小觅深度相机标准版 ROS使用
  12. Android背景斜线
  13. 【CSAPP】二进制拆弹实验
  14. Java程序员转行都可以做什么呢?
  15. TesterHome android app 编写历程(五)
  16. js实现点击切换checkbox背景图片
  17. 基于OpenCV实现二维码发现与定位
  18. Web前端HTML+CSS全套(1~20)
  19. Celery定时任务
  20. 产品开发中项目与项目管理

热门文章

  1. 5G还没来,我的4G网速就变慢了!运营商到底有没有说实话?
  2. 告别3D Touch 2019款iPhone手机或将拿掉屏幕压感功能
  3. 网约代收垃圾App火了!别笑,垃圾分类下一个就到你了
  4. linphone相关(转)
  5. 用C实现任意一年的日历
  6. tokengetall php,token_get_all Split given source into PHP tokens php函数分享
  7. linux目录结构与功能_深入理解linux系统的目录结构(总结的非常详细)
  8. 我的YUV播放器MFC小笔记:设置picture控件背景为黑色、窗口缩放
  9. android loader使用教程,Android Loader 机制,让你的数据加载更加轻松
  10. hbase 命令_HBase原理与实践 | 生产环境上线前真的优化过吗?