Tomcat 是如何管理Session的方法示例

学了 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 再包装一层,上图代码执行的时候将会发生错误。如下图所示,将会抛出类型转换的异常,从而阻止此处非法的操作。

再进一步,我们由办法绕外观类直接访问 StandardSession 吗?

事实上是可以的,我们可以通过反射机制来获取 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();
 }

StandardSession

该类的定义如下

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创建速率计算公式如下

(1000*60*counter)/(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的代码如下所示。

 @Override
 public 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 expire
 expire();
 }

expire 方法的代码如下

 @Override
 public void expire() {

 expire(true);

 }
 public void expire(boolean notify) {
  //省略代码
  //将session从ConcurrentHashMap中移除
  manager.remove(this, true);
  //被省略的代码主要是将session被销毁的消息通知
  //到各个监听器上
 }

超时销毁

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

如下图所示,我们为session设置了一个30秒的超时时间。

然后我们在 ManagerBase.remove

方法上打上断点,等待30秒之后,如下图所示

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

 @Override
 public 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 来保存键值对,其结构如下图所示。 .jpg

这意味着,不要拼命的往Session里面添加离散的数据, 把离散的数据封装成一个对象性能会更加好 如下所示

//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并将其销毁。

思想迁移

对象生成速率算法此算法设计比较有趣,并且也可以应用到其他项目中,因此做如下总结。

首先生成一个固定大小的链表(比如说100),然后以null元素填充。 当创建新的对象时,将创建时间加入链表末尾中(当然是封装后的对象),然后将链表头节点移除,此时被移除的对象要么是null节点要么是最早加入链表的节点 当要计算对象生成速率时,统计链表中不为null的元素的数量除以当前的时间与最早创建对象的时间的差,便可以得出其速率。(注意时间单位的转换)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈Tomcat Session管理分析

    前言 在上文Nginx+Tomcat关于Session的管理中简单介绍了如何使用redis来集中管理session,本文首先将介绍默认的管理器是如何管理Session的生命周期的,然后在此基础上对Redis集中式管理Session进行分析. Tomcat Manager介绍 上文中在Tomcat的context.xml中配置了Session管理器RedisSessionManager,实现了通过redis来存储session的功能:Tomcat本身提供了多种Session管理器,如下类图: 1.

  • 深入浅析TomCat Session管理分析

    前言 对于广大java开发者而已,对于J2EE规范中的Session应该并不陌生,我们可以使用Session管理用户的会话信息,最常见的就是拿Session用来存放用户登录.身份.权限及状态等信息.对于使用Tomcat作为Web容器的大部分开发人员而言,Tomcat是如何实现Session标记用户和管理Session信息的呢? 概要 SESSION Tomcat内部定义了Session和HttpSession这两个会话相关的接口,其类继承体系如图1所示. 图1 Session类继承体系 图1中额

  • Nginx+Tomcat关于Session的管理的实现

    前言 Nginx+Tomcat对Session的管理一直有了解,但是一直没有实际操作一遍,本文从最简单的安装启动开始,通过实例的方式循序渐进的介绍了几种管理session的方式. nginx安装配置 1.安装nginx [root@localhost ~]# yum install nginx 提示报如下错误: No package nginx available. 解决办法安装epel:EPEL是企业版 Linux 附加软件包的简称,EPEL是一个由Fedora特别兴趣小组创建.维护并管理的,

  • Tomcat中session的管理机制

    详细描述Tomcat中session的管理机制: 1. 请求过程中的session操作: 简述:在请求过程中首先要解析请求中的sessionId信息,然后将sessionId存储到request的参数列表中.然后再从 request获取session的时候,如果存在sessionId那么就根据Id从session池中获取session,如果sessionId不 存在或者session失效,那么则新建session并且将session信息放入session池,供下次使用. (1)SessionId

  • Tomcat 是如何管理Session的方法示例

    学了 ConcurrentHashMap 却不知如何应用?用了Tomcat的Session却不知其是如何实现的,Session是怎么被创建和销毁的?往下看你就知道了. Session结构 不多废话,直接上图 仔细观察上图,我们可以得出以下结论 HttpSession 是JavaEE标准中操作Session的接口类,因此我们实际上操作的是 StandardSessionFacade 类 Session 保存数据所使用的数据结构是 ConcurrentHashMap , 如你在图上看到的我们往 Se

  • webapi跨域使用session的方法示例

    在之前的项目中,我们设置跨域都是直接在web.config中设置的. 这样是可以实现跨域访问的.因为我们这边一般情况下一个webapi会有多个网站.小程序.微信公众号等访问,所以这样设置是没有问题的.但是--如果其中一个网站需要用到cookie或者session的时候, Access-Control-Allow-Origin如果还是设置成"*"就会报错,当然是前端报错...数据返回还有cookie/session都还是能存,但是报错就不爽了啊. 于是,想着整改一下. 先上前端代码.来个

  • 在Docker中利用Tomcat快速部署web应用的方法示例

    在学习了docker的基本操作之后,我们就可以尝试在我们的container中部署一些基本的应用了. 这篇文章我们就来说一下怎么在docker中快速部署一个web应用. 首先肯定是要机器中安装了docker,如果没安装就是用yum install -y docker 命令安装一下 yum install -y docker 既然是部署web应用,那么当然少不了Tomcat了,所以我们应该先拉取Tomcat镜像.命令如下 docker pull tomcat 这个镜像有点大,所以可以事先拉取好,节

  • 使用 Go 管理版本的方法示例

    简介 如果你曾经运行过 docker version, 就会发现它提供了很多信息: PS C:\Users\tzh> docker version Client: Docker Engine - Community Version: 19.03.4 API version: 1.40 Go version: go1.12.10 Git commit: 9013bf5 Built: Thu Oct 17 23:44:48 2019 OS/Arch: windows/amd64 Experiment

  • 在react中使用vue的状态管理的方法示例

    我是要介绍一个新的 react 全局共享状态管理器,它和 vue 组件的状态管理一起同工之妙. 马上体验 在 react 状态管理领域,react-redux 可谓是只手遮天了,基于 flux 思想实现,小巧,immutable 的思想让数据变化可控.但 immutable 所带来的编程代价太大了,如果你要更新一个深层结构的对象的某个节点,写作将会是极其麻烦的一件事,而且还保不准会出错.为了保证 immutable,redux 的 reducer 机制让开发者掉光了头发.于是有了类似 dva.r

  • tomcat默认最大连接数与调整的方法示例

    一般来说我们都是用tomcat默认的配置做基础的本地开发,测试及生产肯定不用tomcat啦,正式一点的企业肯定换大型容器了,当然不排除还是用tomcat或者它的集群的,言归正传,tomcat的连接数相关配置及修改干货如下: 在tomcat配置文件server.xml中的<Connector  />标签配置中,和连接数相关的参数有下面几个(如果你现在看肯定都没有的): minProcessors------------------最小空闲连接线程数,用于提高系统处理性能,默认值为10 maxPr

  • Hibernate用ThreadLocal模式(线程局部变量模式)管理Session

    Hibernate ThreadLocal 它会为每个线程维护一个私有的变量空间.实际上, 其实现原理是在JVM 中维护一个Map,这个Map的key 就是当前的线程对象,而value则是 线程通过Hibernate ThreadLocal.set方法保存的对象实例.当线程调用Hibernate ThreadLocal.get方法时, Hibernate ThreadLocal会根据当前线程对象的引用,取出Map中对应的对象返回. 这样,Hibernate ThreadLocal通过以各个线程对

  • Docker搭建Harbor公开仓库的方法示例

    上一篇博文讲到了Registry私有仓库,今天配置一下Harbor仓库,Harbor呢可以作为公开仓库,也可以作为私有仓库,今天就来配置一下Harbor如何实现公开仓库和私有仓库. 关于Registry公开仓库请访问博文:部署Docker私有仓库Registry Registry和Harbor的区别 Registry:是一个私有镜像仓库,图形化支持较差,小型企业使用: Harbor:支持可视化管理,支持私有仓库和公有仓库,支持镜像的管理控制: Docker Harbor的优点 VMWare公司的

  • Nginx+Tomcat负载均衡集群的实现示例

    目录 引言 一.案例概述 二.环境部署 三.Nginx 主机安装 四.Tomcat 安装及配置 1. 安装 Tomcat 2. Tomcat 服务器1配置 3. Tomcat 服务器2配置 五.Nginx server 配置 六.验证结果 总结 引言 通常情况下,一个 Tomcat 站点由于可能出现单点故障以及无法应付过多客户复杂多样的请求等问题,不能单独应用于生产环境中,所以需要一套更可靠的解决方案来完善 Web 站点架构. 一.案例概述 Nginx 是一款非常优秀的 http 服务器软件,它

  • PHP使用Redis替代文件存储Session的方法

    本文实例讲述了PHP使用Redis替代文件存储Session的方法.分享给大家供大家参考,具体如下: PHP默认使用文件存储session,如果并发量大,效率非常低.而Redis对高并发的支持非常好,所以,可以使用redis替代文件存储session. 这里,介绍下php的session_set_save_handler 函数的作用和使用方法.该函数定义用户级session保存函数(如打开.关闭.写入等). 原型如下: bool session_set_save_hanler(callback

随机推荐