Tomcat 是如何管理Session的

概述

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

Session结构

很少废话,直接上图 程序员

仔细观察上图,咱们能够得出如下结论

  • HttpSession是JavaEE标准中操做Session的接口类,所以咱们实际上操做的是StandardSessionFacade算法

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

为何须要使用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的实现类呢?session

根据图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来保存键值对,其结构以下图所示。

这意味着,不要拼命的往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的元素的数量除以当前的时间与最先建立对象的时间的差,即可以得出其速率。(注意时间单位的转换)
相关文章
相关标签/搜索