概述

学了ConcurrentHashMap却不知如何应用?用了Tomcat的Session却不知其是如何实现的,Session是怎么被创建和销毁的?往下看你就知道了。

Session结构

不多废话,直接上图

仔细观察上图,我们可以得出以下结论

HttpSession是JavaEE标准中操作Session的接口类,因此我们实际上操作的是StandardSessionFacade类

Session保存数据所使用的数据结构是ConcurrentHashMap, 如你在图上看到的我们往Session中保存了一个msg

为什么需要使用ConcurrentHashMap呢?原因是,在处理Http请求并不是只有一个线程会访问这个Session, 现代Web应用访问一次页面,通常需要同时执行多次请求, 而这些请求可能会在同一时刻内被Web容器中不同线程同时执行,因此如果采用HashMap的话,很容易引发线程安全的问题。 让我们先来看看HttpSession的包装类。

StandardSessionFacade

在此类中我们可以学习到外观模式(Facde)的实际应用。其定义如下所示。

 public class StandardSessionFacade implements HttpSession

那么此类是如何实现Session的功能呢?观察以下代码不难得出,此类并不是HttpSession的真正实现类,而是将真正的HttpSession实现类进行包装,只暴露HttpSession接口中的方法,也就是设计模式中的外观(Facde)模式。

private final HttpSession session;  public StandardSessionFacade(HttpSession session) { this.session = session;
}

那么我们为什么不直接使用HttpSession的实现类呢?
根据图1,我们可以知道HttpSession的真正实现类是StandardSession,假设在该类内定义了一些本应由Tomcat调用而非由程序调用的方法,那么由于Java的类型系统我们将可以直接操作该类,这将会带来一些不可预见的问题,如以下代码所示。

而如果我们将StandardSession再包装一层,上图代码执行的时候将会发生错误。如下图所示,将会抛出类型转换的异常,从而阻止此处非法的操作。

GetMapping("/s")public String sessionTest(HttpSession httpSession) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {StandardSessionFacade session = (StandardSessionFacade) httpSession;Class targetClass = Class.forName(session.getClass().getName());//修改可见性Field standardSessionField = targetClass.getDeclaredField("session");standardSessionField.setAccessible(true);//获取StandardSession standardSession = (StandardSession) standardSessionField.get(session);return standardSession.getManager().toString();}

该类的定义如下

 public class StandardSession implements HttpSession, Session, Serializable

通过其接口我们可以看出此类除了具有JavaEE标准中HttpSession要求实现的功能之外,还有序列化的功能。

在图1中我们已经知道StandardSession是用ConcurrentHashMap来保存的数据,因此接下来我们主要关注StandardSession的序列化以及反序列化的实现,以及监听器的功能。

序列化

还记得上一节我们通过反射机制获取到了StandardSession吗?利用以下代码我们可以直接观察到反序列化出来的StandardSession是咋样的。

 @GetMapping("/s")public void sessionTest(HttpSession httpSession, HttpServletResponse response) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {StandardSessionFacade session = (StandardSessionFacade) httpSession;Class targetClass = Class.forName(session.getClass().getName());//修改可见性Field standardSessionField = targetClass.getDeclaredField("session");standardSessionField.setAccessible(true);//获取StandardSession standardSession = (StandardSession) standardSessionField.get(session);//存点数据以便观察standardSession.setAttribute("msg","hello,world");standardSession.setAttribute("user","kesan");standardSession.setAttribute("password", "点赞");standardSession.setAttribute("tel", 10086L);//将序列化的结果直接写到Http的响应中ObjectOutputStream objectOutputStream = new ObjectOutputStream(response.getOutputStream());standardSession.writeObjectData(objectOutputStream);}

如果不出意外,访问此接口浏览器将会执行下载操作,最后得到一个文件

使用WinHex打开分析,如图所示为序列化之后得结果,主要是一大堆分隔符,以及类型信息和值,如图中红色方框标准的信息。

不建议大家去死磕序列化文件是如何组织数据的,因为意义不大

如果你真的有兴趣建议你阅读以下代码

 org.apache.catalina.session.StandardSession.doWriteObject

监听器

在JavaEE的标准中,我们可以通过配置HttpSessionAttributeListener来监听Session的变化,那么在StandardSession中是如何实现的呢,如果你了解观察者模式,那么想必你已经知道答案了。 以setAttribute为例,在调用此方法之后会立即在本线程调用监听器的方法进行处理,这意味着我们不应该在监听器中执行阻塞时间过长的操作。

 public void setAttribute(String name, Object value, boolean notify) {//省略无关代码//获取上文中配置的事件监听器Object listeners[] = context.getApplicationEventListeners();if (listeners == null) {return;}for (int i = 0; i < listeners.length; i++) {//只有HttpSessionAttributeListener才可以执行if (!(listeners[i] instanceof HttpSessionAttributeListener)) {continue;}HttpSessionAttributeListener listener = (HttpSessionAttributeListener) listeners[i];try {//在当前线程调用监听器的处理方法if (unbound != null) {if (unbound != value || manager.getNotifyAttributeListenerOnUnchangedValue()) {//如果是某个键的值被修改则调用监听器的attributeReplaced方法context.fireContainerEvent("beforeSessionAttributeReplaced", listener);if (event == null) {event = new HttpSessionBindingEvent(getSession(), name, unbound);}listener.attributeReplaced(event);context.fireContainerEvent("afterSessionAttributeReplaced", listener);}} else {//如果是新添加某个键则执行attributeAdded方法context.fireContainerEvent("beforeSessionAttributeAdded", listener);if (event == null) {event = new HttpSessionBindingEvent(getSession(), name, value);}listener.attributeAdded(event);context.fireContainerEvent("afterSessionAttributeAdded", listener);}} catch (Throwable t) {//异常处理}}}

Sesssion生命周期

如何保存Session

在了解完Session的结构之后,我们有必要明确StandardSession是在何时被创建的,以及需要注意的点。
首先我们来看看StandardSession的构造函数, 其代码如下所示。

public StandardSession(Manager manager) {//调用Object类的构造方法,默认已经调用了//此处再声明一次,不知其用意,或许之前此类有父类?super();this.manager = manager;//是否开启访问计数if (ACTIVITY_CHECK) {accessCount = new AtomicInteger();}}

在创建StandardSession的时候都必须传入Manager对象以便与此StandardSession关联,因此我们可以将目光转移到Manager,而Manager与其子类之间的关系如下图所示。

我们将目光转移到ManagerBase中可以发现以下代码。
protected Map<String, Session> sessions = new ConcurrentHashMap<>(); 复制代码
Session是Tomcat自定义的接口,StandardSession实现了HttpSession以及Session接口,此接口功能更加丰富,但并不向程序员提供。

查找此属性可以发现,与Session相关的操作都是通过操作sessions来实现的,因此我们可以明确保存Session的数据结构是ConcurrentHashMap。

如何创建Session

那么Session到底是如何创建的呢?我找到了以下方法ManagerBase.creaeSession, 总结其流程如下。
检查session数是否超过限制,如果有就抛出异常
创建StandardSession对象
设置session各种必须的属性(合法性, 最大超时时间, sessionId)
生成SessionId, Tomcat支持不同的SessionId算法,本人调试过程其所使用的SessionId生成算法是LazySessionIdGenerator(此算法与其他算法不同之处就在于并不会在一开始就加载随机数数组,而是在用到的时候才加载,此处的随机数组并不是普通的随机数组而是SecureRandom)
增加session的计数,由于Tomcat的策略是只计算100个session的创建速率,因此sessionCreationTiming是固定大小为100的链表(一开始为100个值为null的元素),因此在将新的数据添加到链表中时必须要将旧的数据移除链表以保证其固定的大小。session创建速率计算公式如下
(100060counter)/(int)(now - oldest)
其中
now为获取统计数据时的时间System.currentTimeMillis()
oldest为队列中最早创建session的时间
counter为队列中值不为null的元素的数量
由于计算的是每分钟的速率因此在此处必须将1000乘以60(一分钟内有60000毫秒)

 public Session createSession(String sessionId) {//检查Session是否超过限制,如果是则抛出异常if ((maxActiveSessions >= 0) &&(getActiveSessions() >= maxActiveSessions)) {rejectedSessions++;throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"),maxActiveSessions);}//该方法会创建StandardSession对象Session session = createEmptySession();//初始化Session中必要的属性session.setNew(true);//session是否可用session.setValid(true);//创建时间session.setCreationTime(System.currentTimeMillis());//设置session最大超时时间session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);String id = sessionId;if (id == null) {id = generateSessionId();}session.setId(id);sessionCounter++;//记录创建session的时间,用于统计数据session的创建速率//类似的还有ExpireRate即Session的过期速率//由于可能会有其他线程对sessionCreationTiming操作因此需要加锁SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);synchronized (sessionCreationTiming) {//sessionCreationTiming是LinkedList//因此poll会移除链表头的数据,也就是最旧的数据sessionCreationTiming.add(timing);sessionCreationTiming.poll();}return session;}

Session的销毁

要销毁Session,必然要将Session从ConcurrentHashMap中移除,顺藤摸瓜我们可以发现其移除session的代码如下所示。

 @Overridepublic void remove(Session session, boolean update) {//检查是否需要将统计过期的session的信息if (update) {long timeNow = System.currentTimeMillis();int timeAlive =(int) (timeNow - session.getCreationTimeInternal())/1000;updateSessionMaxAliveTime(timeAlive);expiredSessions.incrementAndGet();SessionTiming timing = new SessionTiming(timeNow, timeAlive);synchronized (sessionExpirationTiming) {sessionExpirationTiming.add(timing);sessionExpirationTiming.poll();}}//将session从Map中移除if (session.getIdInternal() != null) {sessions.remove(session.getIdInternal());}}

被销毁的时机

主动销毁

我们可以通过调用HttpSession.invalidate()方法来执行session销毁操作。此方法最终调用的是StandardSession.invalidate()方法,其代码如下,可以看出使session销毁的关键方法是StandardSession.expire()

 public void invalidate() {if (!isValidInternal())throw new IllegalStateException(sm.getString("standardSession.invalidate.ise"));// Cause this session to expireexpire();}

超时销毁

除了主动销毁之外,我们可以为session设置一个过期时间,当时间到达之后session会被后台线程主动销毁。我们可以为session设置一个比较短的过期时间,然后通过JConsole来追踪其调用栈,其是哪个对象哪个线程执行了销毁操作。
如下图所示,我们为session设置了一个30秒的超时时间。

然后我们在ManagerBase.remove方法上打上断点,等待30秒之后,如下图所示

Tomcat会开启一个后台线程,来定期执行子组件的backgroundProcess方法(前提是子组件被Tomcat管理且实现了Manager接口)

 @Overridepublic void backgroundProcess() {count = (count + 1) % processExpiresFrequency;if (count == 0)processExpires();}public void processExpires() {long timeNow = System.currentTimeMillis();Session sessions[] = findSessions();int expireHere = 0 ;if(log.isDebugEnabled())log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);//从JConsole的图中可以看出isValid可能导致expire方法被调用for (int i = 0; i < sessions.length; i++) {if (sessions[i]!=null && !sessions[i].isValid()) {expireHere++;}}long timeEnd = System.currentTimeMillis();if(log.isDebugEnabled())log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);processingTime += ( timeEnd - timeNow );}

我们可以来看看接口中Manager.backgroundProcess中注释,简略翻译一下就是backgroundProcess会被容器定期的执行,可以用来执行session清理任务等。

 /*** This method will be invoked by the context/container on a periodic* basis and allows the manager to implement* a method that executes periodic tasks, such as expiring sessions etc.*/public void backgroundProcess();

总结

Session的数据结构如下图所示,简单来说就是用ConcurrentHashMap来保存Session,而Session则用ConcurrentHashMap来保存键值对,其结构如下图所示。

//bad
httpSession.setAttribute("user","kesan");
httpSession.setAttribute("nickname","点赞");
httpSession.setAttribute("sex","男");
//good
User kesan =  userDao.getUser()
httpSession.setAttribute("user", kesan);

如果你为Session配置了监听器,那么对Session执行任何变更都将直接在当前线程执行监听器的方法,因此最好不要在监听器中执行可能会发生阻塞的方法。

Tomcat会开启一个后台线程来定期执行ManagerBase.backgroundProcess方法用来检测过期的Session并将其销毁。

作者:柯三
原文链接:https://juejin.im/post/5dbfe35cf265da4d4a3067ae

Tomcat 是如何管理Session的?相关推荐

  1. tomcat 使用 memcached管理session ,并且实现统一登录

    2019独角兽企业重金招聘Python工程师标准>>> 把tomcat的session存放在memcached 的集中式缓存中,所有的tomcat共享memcached中的https ...

  2. Rainbond最佳实践:Tomcat配置Redis实现Session共享

    Rainbond:生产级无服务器PaaS Rainbond是国内首个开源的生产级无服务器PaaS,深度整合基于Kubernetes的容器管理.多类型CI/CD应用构建与交付.多数据中心的资源管理等技术 ...

  3. Spring-Session基于Redis管理Session

    2019独角兽企业重金招聘Python工程师标准>>> 系列文章 Nginx+Tomcat关于Session的管理 Tomcat Session管理分析 Spring-Session ...

  4. java毕业设计——基于java+Jsp+Tomcat的牙科诊所管理系统设计与实现(毕业论文+程序源码)——牙科诊所管理系统

    基于java+Jsp+Tomcat的牙科诊所管理系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于java+Jsp+Tomcat的牙科诊所管理系统设计与实现,文章末尾附有本毕业设计的论文 ...

  5. MSM实现tomcat集群的session共享

    本文首发于我的个人网站: https://hewanyue.com/ 本文作者: Hechao 本文链接: https://hewanyue.com/blog/6254cc16.html 会话保持起源 ...

  6. Tomcat关闭后,重新启动,session中保存的对象为什么还存在解决方法

    Tomcat关闭后,重新启动,session中保存的对象为什么还存在 各们朋友大家好: 当我关闭Tomcat,重新启动后,session中保存的对象还依然存在,仍然可以使用,不知这是什么原因? 谢谢! ...

  7. Tomcat通过Redis实现session共享的完整部署记录

    对于生产环境有了一定规模的tomcat集群业务来说,要实现session会话共享,比较稳妥的方式就是使用数据库持久化session.为什么要持久化session(共享session)呢?因为在客户端每 ...

  8. apache+tomcat​现在我们实现session共享

    apache+tomcat现在我们实现session共享 一.tomcat集群配置,session 同步配置: tomcat1配置   A.修改Engine节点信息: <Engine name= ...

  9. hibernate 管理 Session(单独使用session,非spring)

    hibernate 管理 Session(单独使用session,非spring) Hibernate 自身提供了三种管理 Session 对象的方法 Session 对象的生命周期与本地线程绑定 S ...

最新文章

  1. 虚拟机实现二层交换机_局域网SDN技术硬核内幕 5 虚拟化网络的实现
  2. CalledFromWrongThreadException
  3. 记-php连接mssql遇上的问题
  4. php判断目录是否有写的权限,php中判断文件空目录是否有读写权限的函数代码_php技巧...
  5. Raft协议安全性保证
  6. class12_pack_grid_place 放置位置
  7. 下班理财超过上班赚钱
  8. vue vuex 挂载_GitHub - BingoVue/vuex: 用Vue实现简版vuex
  9. SDUT OJ 顺序表应用3:元素位置互换之移位算法
  10. 干货来袭丨资产可用性真的是终极目标吗?
  11. lzg_ad: WES7 技术概览
  12. 计算机键盘的认识,认识键盘和使用键盘的方法
  13. Kettle数据库连接中的集群与分片
  14. 【C++】严重性代码说明项目文件行错误
  15. get_chunk用法
  16. 三分钟深入TT猫之故障转移
  17. mybatis多数据源配置
  18. tyler cowen_Codenvy背后的技术。 首席执行官Tyler Jewell访谈
  19. PQA2000 地震应急救生器
  20. Mondrian + JPivot环境配置和演示

热门文章

  1. 走走北京中轴(七):午门至神武门的功课
  2. CentOS 7.7 救援模式修改Fstab
  3. QCon北京2013大会图灵全程为您准备好图书
  4. Android关闭软键盘的方法
  5. 四个提高用户活跃度的经验分享
  6. 查看公司和公司产品数的sql语句
  7. 【ROS】—— 机器人系统仿真 —Rviz中控制机器人模型运动与URDF集成Gazebo(十五)
  8. 3D竖版射击游戏Icarus-X
  9. 使用Java语言打印一个心形图案
  10. ChainNet: Learning on Blockchain Graphs with Topological Features