Tomcat-Session管理


​ 本文从源码出发,剖析 Tomcat 中 Session 的懒加载、保活与生命周期控制机制,并解析了 StandardManager 如何借助序列化完成热部署场景下的 Session 持久化与自动恢复

​ 在 StandardContext#start() 阶段,Tomcat 会默认创建一个 StandardManager 实例作为 Session 管理器。它是一个基于本地 JVM 内存的实现,负责:

  • 实例化和缓存Session
  • 定期检查并清理过期的 Session
  • 在 Context 热加载触发时,将 Session 序列化持久化到磁盘。并在启动后,尝试从磁盘恢复上一次的 Session 数据

那么这些功能具体是怎么实现的呢?我们接下来结合源码逐一分析,也顺便看看有没有什么值得优化的地方。

Session创建、缓存和刷新

获取和创建

Tomcat 中的 Session 采用 懒加载策略,即只有在显式调用 HttpServletRequest#getSession()时,才会触发 Session 的获取或创建、绑定到当前Request等操作。该逻辑核心实现位于 org.apache.catalina.connector.Request#doGetSession 方法,其处理流程如下:

  1. 已绑定有效 Session:若当前 Request 已绑定有效 Session,直接返回;
  2. 尝试查找已有 Session:从全局缓存中查找session,如果存在则刷新访问时间,在绑定到当前Request并返回
  3. 按需创建新 Session:create=true才创建session。先进行sessionId的校验(避免伪造攻击),再创建默认的StandardSession并全局缓存,记录访问时间后返回
protected Session doGetSession(boolean create) {

    Context context = getContext();
    if (context == null) {
        return null;
    }
    // 当前request中已获取过session切session有效,则直接返回
    if ((session != null) && !session.isValid()) {
        session = null;
    }
    if (session != null) {
        return session;
    }

    Manager manager = context.getManager();
    if (manager == null) {
        return null; 
    }
    if (requestedSessionId != null) {
        try {
            // 从缓存中找session
            session = manager.findSession(requestedSessionId);
        } catch (IOException e) {
            session = null;
        }
        if ((session != null) && !session.isValid()) {
            session = null;
        }
         // session找到了且有效,刷新访问时间后返回
        if (session != null) {
            session.access();
            return session;
        }
    }

    // 不许要创建则直接返回
    if (!create) {
        return null;
    }
    boolean trackModesIncludesCookie = context.getServletContext().getEffectiveSessionTrackingModes()
            .contains(SessionTrackingMode.COOKIE);
    if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
        throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
    }
    // ====================== 到这里,表示需要创建session ================

    // 对客户端提供的sessionId进行校验,一般来说是不允许使用客户端提供的sessionId创建session的
    String sessionId = getRequestedSessionId();
    if (requestedSessionSSL) { // 一般不会走这

    } else if (("/".equals(context.getSessionCookiePath())
            && isRequestedSessionIdFromCookie())) {
        // 支持跨 Context 共享 sessionId(必须配置 cookiePath="/" 且启用校验,默认已启用)
        if (context.getValidateClientProvidedNewSessionId()) {
            boolean found = false;
            for (Container container : getHost().findChildren()) {
                Manager m = ((Context) container).getManager();
                if (m != null) {
                    try {
                        if (m.findSession(sessionId) != null) {
                            found = true;
                            break;
                        }
                    } catch (IOException e) {
                    }
                }
            }
            if (!found) {
                // 未找到则清空,避免伪造攻击
                sessionId = null;
            }
        }
    } else {
        sessionId = null;
    }

    // 新建session(默认StandardSession)
    session = manager.createSession(sessionId);
    // 设置cookie到response中
    if (session != null && trackModesIncludesCookie) {
        Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
                context, session.getIdInternal(), isSecure());

        response.addSessionCookieInternal(cookie);
    }

    if (session == null) {
        return null;
    }
    session.access();
    return session;
}

/**
 * org.apache.catalina.session.ManagerBase#createSession中真正创建session
 */
public Session createSession(String sessionId) {

    // session数量较校验(默认-1无上限)
    if ((maxActiveSessions >= 0) &&
            (getActiveSessions() >= maxActiveSessions)) {
        rejectedSessions++;
        throw new TooManyActiveSessionsException(
                sm.getString("managerBase.createSession.ise"),
                maxActiveSessions);
    }

    Session session = createEmptySession();
    session.setNew(true);
    session.setValid(true);
    session.setCreationTime(System.currentTimeMillis());
    // session最大不活跃的时间(默认30分钟)
    session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
    String id = sessionId;
    if (id == null) {
        id = generateSessionId();
    }
    // 设置id并将这个session缓存到ManagerBase#sessions里
    session.setId(id);
    sessionCounter++;

    SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
    synchronized (sessionCreationTiming) {
        sessionCreationTiming.add(timing);
        sessionCreationTiming.poll();
    }
    return session;
}

刷新

只有在一次 HTTP 请求中真正访问了 Session,Tomcat 才会刷新它的访问时间,具体如下:

  1. 当请求中第一次调用 request.getSession() 或相关方法时,会触发 org.apache.catalina.Session#access(),记录这次访问的时间
  2. 而在请求结束的时候,会调用 Session#endAccess() 来记录会话访问的结束时间。调用链为:
    CoyoteAdapter#service(方法最后部分) → Request#recycle → Request#recycleSessionInfo → Session#endAccess()

通过这套机制,Tomcat 仅在真正使用了Session的请求中才刷新它的访问时间,从而实现只在用到时才保活

Session过期清理

入口

被StandardEngine的backgroundThread定期触发,调用路径为:ContainerBackgroundProcessor#run -> ContainerBackgroundProcessor#processChildren -> StandardContext.backgroundProcess -> Manager#backgroundProcess -> ManagerBase#processExpires

public void processExpires() {

    long timeNow = System.currentTimeMillis();
    // 所有session
    Session sessions[] = findSessions();
    int expireHere = 0;
    for (Session session : sessions) {
        // 对session校验是否式或并清理
        if (session != null && !session.isValid()) {
            expireHere++;
        }
    }
    long timeEnd = System.currentTimeMillis();
    processingTime += (timeEnd - timeNow);

}

校验清理

StandardSession#isValid为校验session是否有效,如果无效则进行清理。整体逻辑比较简单,稍微关注下会被触发的如下三个EventListener,如果有session和其attribute的监听需求就可以用到:

  1. HttpSessionListener#sessionDestroyed:Session 被销毁时触发。适合用于监听整体 Session 生命周期的结束
  2. HttpSessionBindingListener#valueUnbound:当某个属性从 Session 中移除,且该属性实现了该接口时触发。偏个体
  3. HttpSessionAttributeListener#attributeRemoved:Session 中的属性被移除时触发,属性可以不实现这个Listener。偏集中
public boolean isValid() {

    if (!this.isValid) {
        return false;
    }

    if (this.expiring) {
        return true;
    }

    if (ACTIVITY_CHECK && accessCount.get() > 0) {
        return true;
    }

    if (maxInactiveInterval > 0) {
        // 判断空闲时间是否超过最大不活跃时间,并进行清理
        int timeIdle = (int) (getIdleTimeInternal() / 1000L);
        if (timeIdle >= maxInactiveInterval) {
            expire(true);
        }
    }

    return this.isValid;
}

/*
 *  使session过期(从缓存中移除),并根据是否notify决定是否通知相关EventListener
 */
public void expire(boolean notify) {
    if (!isValid) {
        return;
    }

    synchronized (this) {

        if (expiring || !isValid) {
            return;
        }

        if (manager == null) {
            return;
        }

        expiring = true;

        Context context = manager.getContext();
        // 是否通知相关Listener
        if (notify) {
            ClassLoader oldContextClassLoader = null;
            try {
                oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
                Object listeners[] = context.getApplicationLifecycleListeners();
                if (listeners != null && listeners.length > 0) {
                    HttpSessionEvent event = new HttpSessionEvent(getSession());
                    for (int i = 0; i < listeners.length; i++) {
                        // 触发HttpSessionListener的sessionDestroyed事件
                        int j = (listeners.length - 1) - i;
                        if (!(listeners[j] instanceof HttpSessionListener)) {
                            continue;
                        }
                        HttpSessionListener listener = (HttpSessionListener) listeners[j];
                        try {
                            context.fireContainerEvent("beforeSessionDestroyed",
                                    listener);
                            listener.sessionDestroyed(event);
                            context.fireContainerEvent("afterSessionDestroyed",
                                    listener);
                        } catch (Throwable t) {
                            manager.getContext().getLogger().error(sm.getString("standardSession.sessionEvent"), t);
                        }
                    }
                }
            } finally {
                context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
            }
        }

        if (ACTIVITY_CHECK) {
            accessCount.set(0);
        }

        // 从缓存中移除
        manager.remove(this, true);

        if (notify) {
            fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);
        }

        setValid(false);
        expiring = false;

        String keys[] = keys();
        ClassLoader oldContextClassLoader = null;
        try {
            oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);
            // 触发session key相关Listener
            for (String key : keys) {
                removeAttributeInternal(key, notify);
            }
        } finally {
            context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);
        }
    }

}

Session持久化

众所周知,Tomcat 中Web 应用支持热部署,在热加载过程中,原来的Context会被卸载,它所持有的所有对象(包括HttpSession)都会被销毁。

但从业务逻辑角度来看,session 的生命周期不应该被应用自身的重部署打断。客户端用户都没登出,如果仅因为服务端代码热更新就丢失session数据,用户会难以接受

所以为了解决这个问题,Tomcat 的StandardManager实现了 session 的持久化机制。核心思想是:

  • Context 停止前(如热部署时),将内存中的所有有效 session 序列化写入磁盘
  • Context 启动时,尝试从磁盘加载这些 session,并恢复到内存中

两个核心方法如下:

  1. StandardManager#doUnload:context#stop时触发,将session序列化到磁盘
  2. StandardManager#doLoad:context#start时触发,将磁盘的session文件反序列化到内存
protected void doUnload() throws IOException {


    if (sessions.isEmpty()) {
        return; // nothing to do
    }

    // 默认为当前context的 ${javax.servlet.context.tempdir}/SESSIONS.ser 文件
    File file = file();
    if (file == null) {
        return;
    }

    // 即将被卸载的 session 实例
    List<StandardSession> list = new ArrayList<>();

    try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            ObjectOutputStream oos = new ObjectOutputStream(bos)) {

        synchronized (sessions) {
            oos.writeObject(Integer.valueOf(sessions.size()));
            // 将session序列化到磁盘
            for (Session s : sessions.values()) {
                StandardSession session = (StandardSession) s;
                list.add(session);
                session.passivate();
                session.writeObjectData(oos);
            }
        }
    }

    // 清楚session,但不触发通知(毕竟未真正失活)
    for (StandardSession session : list) {
        try {
            session.expire(false);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
        } finally {
            session.recycle();
        }
    }

}

protected void doLoad() throws ClassNotFoundException, IOException {

    sessions.clear();

    File file = file();
    // 没有就返回
    if (file == null) {
        return;
    }
    Loader loader = null;
    ClassLoader classLoader = null;
    Log logger = null;
    try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
            BufferedInputStream bis = new BufferedInputStream(fis)) {
        Context c = getContext();
        loader = c.getLoader();
        logger = c.getLogger();
        // 使用当前contxet的WebappClassLoader,加载session
        if (loader != null) {
            classLoader = loader.getClassLoader();
        }
        if (classLoader == null) {
            classLoader = getClass().getClassLoader();
        }

        synchronized (sessions) {
            try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger,
                    getSessionAttributeValueClassNamePattern(),
                    getWarnOnSessionAttributeFilterFailure())) {
                Integer count = (Integer) ois.readObject();
                int n = count.intValue();
                for (int i = 0; i < n; i++) {
                    // 反序列化到内存
                    StandardSession session = getNewSession();
                    session.readObjectData(ois);
                    session.setManager(this);
                    sessions.put(session.getIdInternal(), session);
                    // 通知 session 已激活
                    session.activate();
                    if (!session.isValidInternal()) {
                        session.setValid(true);
                        session.expire();
                    }
                    sessionCounter++;
                }
            } finally {
                if (file.exists()) {
                    // 删除持久化文件,避免重复加载
                    if (!file.delete()) {
                        log.warn(sm.getString("standardManager.deletePersistedFileFail", file));
                    }
                }
            }
        }
    } catch (FileNotFoundException e) {
        return;
    }

}