• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    Tomcat 是如何管理Session的方法示例

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

    Session结构

    不多废话,直接上图

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

    为什么需要使用 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 , 总结其流程如下。

    (1000*60*counter)/(int)(now - oldest)
    其中

     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的元素的数量除以当前的时间与最早创建对象的时间的差,便可以得出其速率。(注意时间单位的转换)

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

    上一篇:windows2008 server子域加入父域时提示错误 域已经存在
    下一篇:CentOS7开启MySQL8主从备份、每日定时全量备份(推荐)
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

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