Tomcat 9 源码分析(5)— Session管理

Session管理

Session可以用来管理用户的会话信息,最常见的就是拿Session来存放用户登录、身份、权限及状态等信息。对于使用Tomcat作为Web容器的大部分开发人员而言,本文介绍分析Tomcat是如何实现Session标记用户和管理Session信息

Session接口

Tomcat内部定义了SessionHttpSession这两个会话相关的接口,其类继承体系如下

avator

Session:Tomcat中有关会话的基本接口规范,下面介绍它定义的主要方法

方法 描述
getCreationTime()/setCreationTime() 获取与设置Session的创建时间
getId()/setId() 获取与设置Session的ID
getThisAccessedTime() 获取最近一次请求的开始时间
getLastAccessedTime() 获取最近一次请求的完成时间
getManager()/setManager() 获取与设置Session管理器
getMaxInactiveInterval() 获取Session的最大访问间隔
setMaxInactiveInterval() 设置Session的最大访问间隔
getSession() 获取HttpSession
setValid()/isValid() 获取与设置Session的有效状态
access()/endAccess() 开始与结束Session的访问
expire() 设置Session过期

HttpSession:在HTTP客户端与HTTP服务端提供的一种会话的接口规范

方法 描述
getCreationTime() 获取Session的创建时间
getId() 获取Session的ID
getLastAccessedTime() 获取最近一次请求的完成时间
getServletContext() 获取当前Session所属的ServletContext
getMaxInactiveInterval() 获取Session的最大访问间隔
setMaxInactiveInterval() 设置Session的最大访问间隔
getAttribute()/setAttribute() 获取与设置Session作用域的属性
removeAttribute() 清除Session作用域的属性
invalidate() 使Session失效并解除任何与此Session绑定的 对象

ClusterSession:集群部署下的会话接口规范

方法 描述
isPrimarySession() 判断是否为集群的主Session
setPrimarySession() 设置集群主Session

StandardSession:标准的HTTP Session实现,本文将以此实现为例展开

DeltaSession:Tomcat集群会话同步的策略,对会话中增量修改的属性进行同步。这种方式由于是增量的,所以会大大降低网络I/O的开销,但是是线上会比较复杂因为涉及到对会话属性操作过程的管理

ReplicationSessionListener:Tomcat集群会话同步的策略,每次都会把整个会话对象同步给集群中的其他节点,其他节点然后更新整个会话对象。这种实现比较简单但会造成大量无效信息的传输

Session管理器

Tomcat内部定义了Manager接口用于制定Session管理器的接口规范,目前已经有很多Session管理器的实现

avator

Manager:Tomcat对于Session管理器定义的接口规范

方法 描述
getContext()/setContext() 获取与设置上下文
getSessionIdGenerator() 获取会话id生成器
setSessionIdGenerator() 设置会话id生成器
getSessionCounter() 获取Session计数器
setSessionCounter() 设置Session计数器
getMaxActive()/setMaxActive() 获取与设置处于活动状态的最大会话数
getActiveSessions() 获取处于活跃状态的会话数
getExpiredSessions() 获取过期的会话数
setExpiredSessions() 设置过期的会话数
getRejectedSessions() 获取未创建的会话数
getSessionMaxAliveTime() 获取会话存活的最长时间(单位为秒)
setSessionMaxAliveTime() 设置会话存活的最长时间(单位为秒)
getSessionAverageAliveTime() 获取会话平均存活时间
getSessionCreateRate() 获取当前会话创建速率
getSessionExpireRate() 获取当前会话过期速率
add() 将此会话添加到处于活动状态的会话集合
addPropertyChangeListener() 将属性更改监听器到此组件
changeSessionId() 将当前会话的ID更改为新的随机生成的会话ID
rotateSessionId() 将当前会话的ID更改为新的随机生成的会话ID
changeSessionId() 将当前会话的ID更改为指定的会话ID
createEmptySession() 从回收的会话中获取会话或创建一个新的会话
createSession() 根据默认值构造并返回一个新的会话对象
findSession() 返回与此管理器关联的会话
findSessions() 返回与此管理器关联的会话集合
load()/unload() 从持久化机制中加载Session或向持久化机制写入Session
remove() 从此管理器的活动会话中删除此会话
removePropertyChangeListener() 从此组件中删除属性更改监听器
backgroundProcess() 容器接口中定义为具体容器在后台处理相关工作的实现,Session管理器基于此机制实现了过期Session的
willAttributeDistribute() 管理器写入指定的会话属性

ManagerBase:封装了Manager接口通用实现的抽象类,未提供对load()/unload()等方法的实现,需要具体子类去实现。所有的Session管理器都集成自ManagerBase

ClusterManager:在Manager接口的基础上增加了集群部署下的一些接口,所有实现集群下Session管理器都要实现此接口

PersistentManagerBase:提供了对于Session持久化的基本实现

PersistentManager:继承自PersistentManagerBase,可以在Server.xml的元素下通过配置元素来使用。PersistentManager可以将内存中的Session信息备份到文件或数据库中。当备份一个Session对象时,该Session对象会被复制到存储器(文件或者数据库)中,而原对象仍然留在内存中。因此即便服务器宕机,仍然可以从存储器中获取活动的Session对象。如果活动的Session对象超过了上限值或者Session对象闲置了的时间过长,那么Session会被换出到存储器中以节省内存空间

StandardManager:不用配置元素,当Tomcat正常关闭,重启或Web应用重新加载时,它会将内存中的Session序列化到Tomcat目录的/work/Catalina/host_name/webapp_name/SESSIONS.ser文件中。当Tomcat重启或者应用加载完成后,Tomcat会将文件中的Session重新还原到内存中。如果突然中止该服务器,则所有Session豆浆丢失,因为StandardManager没有机会实现存盘处理

ClusterManagerBase:提供了对于Session的集群管理实现

DeltaManager:继承自ClusterManagerBase。此Session管理器是Tomcat集群部署下的默认管理器,当集群中的某一节点生成或修改Session后,DeltaManager将会把这些修改增量复制到其他节点

BackupManager:没有继承ClusterManagerBase,而是直接实现了ClusterManager接口。是Tomcat在集群部署下的可选的Session管理器,集群中的所有Session都被全量复制到一个备份节点。集群中的所有节点都可以访问此备份节点,达到Session在集群下的备份效果

本文以StandardManager为例讲解Session的管理。StandardManager是StandardContext的子组件,用来管理当前Context的所有Session的创建和维护。由Tomcat生命周期管理可知,当StandardContext正式启动,也就是StandardContext的startInternal方法被调用时,StandardContext还会启动StandardManager

org.apache.catalina.core.StandardContext.startInternal()

@Override
protected synchronized void startInternal() throws LifecycleException {

    // 省略与Session管理无关的代码

            // Acquire clustered manager
            Manager contextManager = null;
            Manager manager = getManager();
            if (manager == null) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("standardContext.cluster.noManager",
                            Boolean.valueOf((getCluster() != null)),
                            Boolean.valueOf(distributable)));
                }
                if ((getCluster() != null) && distributable) {
                    try {
                        contextManager = getCluster().createManager(getName());
                    } catch (Exception ex) {
                        log.error(sm.getString("standardContext.cluster.managerError"), ex);
                        ok = false;
                    }
                } else {
                    contextManager = new StandardManager();
                }
            }

            // Configure default manager if none was specified
            if (contextManager != null) {
                if (log.isDebugEnabled()) {
                    log.debug(sm.getString("standardContext.manager",
                            contextManager.getClass().getName()));
                }
                setManager(contextManager);
            }

            if (manager!=null && (getCluster() != null) && distributable) {
                //let the cluster know that there is a context that is distributable
                //and that it has its own manager
                getCluster().registerManager(manager);
            }
        }

       // 省略与Session管理无关的代码

        try {
            // Start manager
            Manager manager = getManager();
            if (manager instanceof Lifecycle) {
                ((Lifecycle) manager).start();
            }
        } catch(Exception e) {
            log.error(sm.getString("standardContext.managerFail"), e);
            ok = false;
        }

        // 省略与Session管理无关的代码
}

从中可知StandardContext.startInternal()中涉及Session管理的执行步骤如下:

  1. 创建StandardManager
  2. 如果Tomcat结合Apache做了分布式部署,会将当前StandardManager注册到集群中
  3. 启动StandardManager

StandardManger.start()用于启动StandardManager

org.apache.catalina.util.LifecycleBase.start()

@Override
public final synchronized void start() throws LifecycleException {

    // 省略状态校验的代码

    if (state.equals(LifecycleState.NEW)) {
        init();
    } else if (state.equals(LifecycleState.FAILED)) {
        stop();
    } else if (!state.equals(LifecycleState.INITIALIZED) &&
            !state.equals(LifecycleState.STOPPED)) {
        invalidTransition(Lifecycle.BEFORE_START_EVENT);
    }

    try {
        setStateInternal(LifecycleState.STARTING_PREP, null, false);
        startInternal();
        if (state.equals(LifecycleState.FAILED)) {
            // This is a 'controlled' failure. The component put itself into the
            // FAILED state so call stop() to complete the clean-up.
            stop();
        } else if (!state.equals(LifecycleState.STARTING)) {
            // Shouldn't be necessary but acts as a check that sub-classes are
            // doing what they are supposed to.
            invalidTransition(Lifecycle.AFTER_START_EVENT);
        } else {
            setStateInternal(LifecycleState.STARTED, null, false);
        }
    } catch (Throwable t) {
        // This is an 'uncontrolled' failure so put the component into the
        // FAILED state and throw an exception.
        handleSubClassException(t, "lifecycleBase.startFail", toString());
    }
}

从中可知启动StandardManager的步骤如下:

  1. 调用init方法初始化StandardManager
  2. 调用startInternal方法启动StandardManager

StandardManager的初始化

经上分析可知,启动StandardManager的第一部就是调用父类LifecycleBase的init方法,init方法在Tomcat生命周期管理中已介绍,现在只需要关心StandardManager的initInternal。StandardManager本身并没有实现initInternal方法,但是StandardManager的父类ManagerBase实现了此方法,将StandardManager注册为到 JMX

org.apache.catalina.session.ManagerBase.initInternal()

@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();

    if (context == null) {
        throw new LifecycleException(sm.getString("managerBase.contextNull"));
    }
}

StandardManager的启动

调用StandardManager的startInternal方法用于启动StandardManager

org.apache.catalina.session.StandardManager.startInternal()

@Override
protected synchronized void startInternal() throws LifecycleException {

    super.startInternal();

    // Load unloaded sessions, if any
    try {
        load();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("standardManager.managerLoad"), t);
    }

    setState(LifecycleState.STARTING);
}

org.apache.catalina.session.ManagerBase.startInternal()

@Override
protected void startInternal() throws LifecycleException {

    // Ensure caches for timing stats are the right size by filling with
    // nulls.
    while (sessionCreationTiming.size() < TIMING_STATS_CACHE_SIZE) {
        sessionCreationTiming.add(null);
    }
    while (sessionExpirationTiming.size() < TIMING_STATS_CACHE_SIZE) {
        sessionExpirationTiming.add(null);
    }

    /* Create sessionIdGenerator if not explicitly configured */
    SessionIdGenerator sessionIdGenerator = getSessionIdGenerator();
    if (sessionIdGenerator == null) {
        sessionIdGenerator = new StandardSessionIdGenerator();
        setSessionIdGenerator(sessionIdGenerator);
    }

    sessionIdGenerator.setJvmRoute(getJvmRoute());
    if (sessionIdGenerator instanceof SessionIdGeneratorBase) {
        SessionIdGeneratorBase sig = (SessionIdGeneratorBase)sessionIdGenerator;
        sig.setSecureRandomAlgorithm(getSecureRandomAlgorithm());
        sig.setSecureRandomClass(getSecureRandomClass());
        sig.setSecureRandomProvider(getSecureRandomProvider());
    }

    if (sessionIdGenerator instanceof Lifecycle) {
        ((Lifecycle) sessionIdGenerator).start();
    } else {
        // Force initialization of the random number generator
        if (log.isDebugEnabled())
            log.debug("Force random number initialization starting");
        sessionIdGenerator.generateSessionId();
        if (log.isDebugEnabled())
            log.debug("Force random number initialization completed");
    }
}

StandardManager的startInternal首先调用了父类的startInternal方法,其内调generateSessionId方法初始化随机数生成器。然后加载持久化的Session信息,由于StandardManager中,所有的Session都维护在一个ConcurrentHashMap中,因此服务器重启或者宕机会造成这些Session信息丢失或失效,为了解决这个问题,Tomcat将这些Session通过持久化的方式来保证不会丢失。下面分析StandardManager的load方法实现

org.apache.catalina.session.StandardManager.load()

@Override
public void load() throws ClassNotFoundException, IOException {
    if (SecurityUtil.isPackageProtectionEnabled()){
        try{
            AccessController.doPrivileged( new PrivilegedDoLoad() );
        } catch (PrivilegedActionException ex){
            Exception exception = ex.getException();
            if (exception instanceof ClassNotFoundException) {
                throw (ClassNotFoundException)exception;
            } else if (exception instanceof IOException) {
                throw (IOException)exception;
            }
            if (log.isDebugEnabled()) {
                log.debug("Unreported exception in load() ", exception);
            }
        }
    } else {
        doLoad();
    }
}

org.apache.catalina.session.StandardManager.PrivilegedDoLoad

private class PrivilegedDoLoad
    implements PrivilegedExceptionAction<Void> {

    PrivilegedDoLoad() {
        // NOOP
    }

    @Override
    public Void run() throws Exception{
       doLoad();
       return null;
    }
}

否则调用实际负责加载的方法doLoad,而加载Session信息的方法也是doLoad

org.apache.catalina.session.StandardManager.doLoad()

protected void doLoad() throws ClassNotFoundException, IOException {
    if (log.isDebugEnabled()) {
        log.debug("Start: Loading persisted sessions");
    }

    // Initialize our internal data structures
    sessions.clear();

    // Open an input stream to the specified pathname, if any
    File file = file();
    if (file == null) {
        return;
    }
    if (log.isDebugEnabled()) {
        log.debug(sm.getString("standardManager.loading", pathname));
    }
    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();
        if (loader != null) {
            classLoader = loader.getClassLoader();
        }
        if (classLoader == null) {
            classLoader = getClass().getClassLoader();
        }

        // Load the previously unloaded active sessions
        synchronized (sessions) {
            try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger,
                    getSessionAttributeValueClassNamePattern(),
                    getWarnOnSessionAttributeFilterFailure())) {
                Integer count = (Integer) ois.readObject();
                int n = count.intValue();
                if (log.isDebugEnabled())
                    log.debug("Loading " + n + " persisted sessions");
                for (int i = 0; i < n; i++) {
                    StandardSession session = getNewSession();
                    session.readObjectData(ois);
                    session.setManager(this);
                    sessions.put(session.getIdInternal(), session);
                    session.activate();
                    if (!session.isValidInternal()) {
                        // If session is already invalid,
                        // expire session to prevent memory leak.
                        session.setValid(true);
                        session.expire();
                    }
                    sessionCounter++;
                }
            } finally {
                // Delete the persistent storage file
                if (file.exists()) {
                    if (!file.delete()) {
                        log.warn(sm.getString("standardManager.deletePersistedFileFail", file));
                    }
                }
            }
        }
    } catch (FileNotFoundException e) {
        if (log.isDebugEnabled()) {
            log.debug("No persisted data file found");
        }
        return;
    }

    if (log.isDebugEnabled()) {
        log.debug("Finish: Loading persisted sessions");
    }
}

分析可知,StandarManager的doLoad方法执行步骤如下:

  1. 清空sessions缓存维护的Session信息
  2. 调用file方法返回当前Context下的Session持久化文件
  3. 打开Session持久化文件的输入流,并封装为CustomObjectInputStream
  4. 从Session持久化文件读入持久化的Session的数量,然后逐个读取Session信息并放入到sessions缓存中

至此StandardManager的启动到此完成

Session分配

在Tomcat请求原理分析中已经介绍了Filter的职责链,Tomcat接收到的请求会经过Filter职责链,最后交给具体的Servlet处理。以访问http://localhost:8080/host-manager这个路径为例,可以清楚的看到整个调用栈中Filter的职责链以及之后的JspServlet,最后到达org.apache.catalina.connector.Request.getSession()

Request的getSession方法用于获取当前请求对应的会话信息,如果没有则创建一个新的Session

org.apache.catalina.connector.Request.getSession()

@Override
public HttpSession getSession(boolean create) {
    Session session = doGetSession(create);
    if (session == null) {
        return null;
    }

    return session.getSession();
}

doGetSession方法的实现如下

org.apache.catalina.connector.Request.doGetSession()

protected Session doGetSession(boolean create) {

    // There cannot be a session if no context has been assigned yet
    Context context = getContext();
    if (context == null) {
        return null;
    }

    // Return the current session if it exists and is valid
    if ((session != null) && !session.isValid()) {
        session = null;
    }
    if (session != null) {
        return session;
    }

    // Return the requested session if it exists and is valid
    Manager manager = context.getManager();
    if (manager == null) {
        return null;      // Sessions are not supported
    }
    if (requestedSessionId != null) {
        try {
            session = manager.findSession(requestedSessionId);
        } catch (IOException e) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("request.session.failed", requestedSessionId, e.getMessage()), e);
            } else {
                log.info(sm.getString("request.session.failed", requestedSessionId, e.getMessage()));
            }
            session = null;
        }
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            session.access();
            return session;
        }
    }

    // Create a new session if requested and the response is not committed
    if (!create) {
        return null;
    }
    boolean trackModesIncludesCookie =
            context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE);
    if (trackModesIncludesCookie && response.getResponse().isCommitted()) {
        throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));
    }

    // Re-use session IDs provided by the client in very limited
    // circumstances.
    String sessionId = getRequestedSessionId();
    if (requestedSessionSSL) {
        // If the session ID has been obtained from the SSL handshake then
        // use it.
    } else if (("/".equals(context.getSessionCookiePath())
            && isRequestedSessionIdFromCookie())) {
        /* This is the common(ish) use case: using the same session ID with
         * multiple web applications on the same host. Typically this is
         * used by Portlet implementations. It only works if sessions are
         * tracked via cookies. The cookie must have a path of "/" else it
         * won't be provided for requests to all web applications.
         *
         * Any session ID provided by the client should be for a session
         * that already exists somewhere on the host. Check if the context
         * is configured for this to be confirmed.
         */
        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) {
                        // Ignore. Problems with this manager will be
                        // handled elsewhere.
                    }
                }
            }
            if (!found) {
                sessionId = null;
            }
        }
    } else {
        sessionId = null;
    }
    session = manager.createSession(sessionId);

    // Creating a new session cookie based on that session
    if (session != null && trackModesIncludesCookie) {
        Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie(
                context, session.getIdInternal(), isSecure());

        response.addSessionCookieInternal(cookie);
    }

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

    session.access();
    return session;
}

分析其内代码可知,整个获取Session的步骤如下:

  1. 判断当前Request对象是否已经存在有效的Session信息,如果存在则返回此Session,否则进入下一步
  2. 获取Session管理器,比如StandardManager
  3. 从StandardManager的Session缓存中获取Session,如果有则返回此Session否则进入下一步
  4. 创建Session
  5. 创建保存Session ID的Cookie
  6. 通过Session的access方法更新Session的访问时间以及访问次数

接下来浏览ManagerBase实现的createSession方法

org.apache.catalina.session.ManagerBase.createSession()

@Override
public Session createSession(String sessionId) {

    if ((maxActiveSessions >= 0) &&
            (getActiveSessions() >= maxActiveSessions)) {
        rejectedSessions++;
        throw new TooManyActiveSessionsException(
                sm.getString("managerBase.createSession.ise"),
                maxActiveSessions);
    }

    // Recycle or create a Session instance
    Session session = createEmptySession();

    // Initialize the properties of the new session and return it
    session.setNew(true);
    session.setValid(true);
    session.setCreationTime(System.currentTimeMillis());
    session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
    String id = sessionId;
    if (id == null) {
        id = generateSessionId();
    }
    session.setId(id);
    sessionCounter++;

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

至此,Session的创建与分配就介绍到这

Session追踪

HTTP是一种无状态的协议,如果一个客户端只是单纯的一个请求,服务端并不需要知道一连串的请求是否来自于相同的客户端,而且也不需要担心客户端是否处于连接状态。但是这样的通信协议使得服务器端难以判断所连接的客户端是否是同一个人。当进行Web程序开发时,必须想办法将相关的请求结合一起,并努力维持用户的状态在服务器上,这就引出了会话最终(session tracking)

Tomcat追踪Session主要借助其ID,因此在接收到请求后应该需要拿到此请求对应的会话ID,这样才能够和StandardManager的缓存中维护的Session相匹配,达到Session追踪的效果。在Tomcat请求原理中介绍了CoyoteAdapter.service()调用的postParseRequest(),其内有这么一段代码

org.apache.catalina.connector.CoyoteAdapter.postParseRequest()节选

String sessionID;
if (request.getServletContext().getEffectiveSessionTrackingModes()
        .contains(SessionTrackingMode.URL)) {

    // Get the session ID if there was one
    sessionID = request.getPathParameter(
            SessionConfig.getSessionUriParamName(
                    request.getContext()));
    if (sessionID != null) {
        request.setRequestedSessionId(sessionID);
        request.setRequestedSessionURL(true);
    }
}

// Look for session ID in cookies and SSL session
try {
    parseSessionCookiesId(request);
} catch (IllegalArgumentException e) {
    // Too many cookies
    if (!response.isError()) {
        response.setError();
        response.sendError(400);
    }
    return true;
}
parseSessionSslId(request);

其内执行的步骤如下:

  1. 如果开启了会话跟踪(session tracking),则需要从缓存中获取维护的Session ID
  2. 从请求所带的Cookie中获取Session ID
  3. 如果Cookie没有携带Session ID,但是开启了会话跟踪(session tracking),则可以从SSL中获取SessionID

从缓存中获取维护的Session ID

CoyoteAdapter.service()调用的postParseRequest()中使用了getSessionUriParamName()获取Session的参数名称

org.apache.catalina.util.SessionConfig.getSessionUriParamName()

public static String getSessionUriParamName(Context context) {

    String result = getConfiguredSessionCookieName(context);

    if (result == null) {
        result = DEFAULT_SESSION_PARAMETER_NAME;
    }

    return result;
}

从中可以看出,getSessionUriParamName()首先调用getConfiguredSessionCookieName()获取Session的Cookie名称,如果没有则默认为jsessionid(常量DEFAULT_SESSION_PARAMETER)NAME的值),而getSessionUriParamName方法的返回值会作为CoyoteAdapter.postParseRequest()中调用的getPathParameter方法的参数查询Session ID

org.apache.coyote.Request.getPathParameter()

public String getPathParameter(String name) {
    return pathParameters.get(name);
}

从请求所带的Cookie中获取Session ID

CoyoteAdapter.postParseRequest()中调用了parseSessionCookieId()从Cookie中获取Session ID

org.apache.catalina.connector.CoyoteAdapter.parseSessionCookiesId()

protected void parseSessionCookiesId(Request request) {

    // If session tracking via cookies has been disabled for the current
    // context, don't go looking for a session ID in a cookie as a cookie
    // from a parent context with a session ID may be present which would
    // overwrite the valid session ID encoded in the URL
    Context context = request.getMappingData().context;
    if (context != null && !context.getServletContext()
            .getEffectiveSessionTrackingModes().contains(
                    SessionTrackingMode.COOKIE)) {
        return;
    }

    // Parse session id from cookies
    ServerCookies serverCookies = request.getServerCookies();
    int count = serverCookies.getCookieCount();
    if (count <= 0) {
        return;
    }

    String sessionCookieName = SessionConfig.getSessionCookieName(context);

    for (int i = 0; i < count; i++) {
        ServerCookie scookie = serverCookies.getCookie(i);
        if (scookie.getName().equals(sessionCookieName)) {
            // Override anything requested in the URL
            if (!request.isRequestedSessionIdFromCookie()) {
                // Accept only the first session id cookie
                convertMB(scookie.getValue());
                request.setRequestedSessionId
                    (scookie.getValue().toString());
                request.setRequestedSessionCookie(true);
                request.setRequestedSessionURL(false);
                if (log.isDebugEnabled()) {
                    log.debug(" Requested cookie session id is " +
                        request.getRequestedSessionId());
                }
            } else {
                if (!request.isRequestedSessionIdValid()) {
                    // Replace the session id until one is valid
                    convertMB(scookie.getValue());
                    request.setRequestedSessionId
                        (scookie.getValue().toString());
                }
            }
        }
    }

}

从SSL中获取Session ID

CoyoteAdapter.postParseRequest()中调用了parseSessionSslId()从SSL中获取Session ID

org.apache.catalina.connector.CoyoteAdapter.parseSessionSslId()

protected void parseSessionSslId(Request request) {
    if (request.getRequestedSessionId() == null &&
            SSL_ONLY.equals(request.getServletContext()
                    .getEffectiveSessionTrackingModes()) &&
                    request.connector.secure) {
        String sessionId = (String) request.getAttribute(SSLSupport.SESSION_ID_KEY);
        if (sessionId != null) {
            request.setRequestedSessionId(sessionId);
            request.setRequestedSessionSSL(true);
        }
    }
}

Session销毁

在Tomcat生命周期中介绍了容器的生命周期管理相关的内容,StandardEngine作为容器,其启动过程中也会调用startInternal方法

org.apache.catalina.core.StandardEngine.startInternal()

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Log our server identification information
    if (log.isInfoEnabled()) {
        log.info(sm.getString("standardEngine.start", ServerInfo.getServerInfo()));
    }

    // Standard container startup
    super.startInternal();
}

StandardEngine.startInternal()实际代理了父类ContainerBase.startInternal()

org.apache.catalina.core.ContainerBase.startInternal()

@Override
protected synchronized void startInternal() throws LifecycleException {

    // Start our subordinate components, if any
    logger = null;
    getLogger();
    Cluster cluster = getClusterInternal();
    if (cluster instanceof Lifecycle) {
        ((Lifecycle) cluster).start();
    }
    Realm realm = getRealmInternal();
    if (realm instanceof Lifecycle) {
        ((Lifecycle) realm).start();
    }

    // Start our child containers, if any
    Container children[] = findChildren();
    List<Future<Void>> results = new ArrayList<>();
    for (Container child : children) {
        results.add(startStopExecutor.submit(new StartChild(child)));
    }

    MultiThrowable multiThrowable = null;

    for (Future<Void> result : results) {
        try {
            result.get();
        } catch (Throwable e) {
            log.error(sm.getString("containerBase.threadedStartFailed"), e);
            if (multiThrowable == null) {
                multiThrowable = new MultiThrowable();
            }
            multiThrowable.add(e);
        }

    }
    if (multiThrowable != null) {
        throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
                multiThrowable.getThrowable());
    }

    // Start the Valves in our pipeline (including the basic), if any
    if (pipeline instanceof Lifecycle) {
        ((Lifecycle) pipeline).start();
    }

    setState(LifecycleState.STARTING);

    // Start our thread
    if (backgroundProcessorDelay > 0) {
        monitorFuture = Container.getService(ContainerBase.this).getServer()
                .getUtilityExecutor().scheduleWithFixedDelay(
                        new ContainerBackgroundProcessorMonitor(), 0, 60, TimeUnit.SECONDS);
    }
}

其内一开始对各种子容器进行了启动,最后启动线程加载ContainerBackgroundProcessorMonitor类,而该类的run方法会调用threadStart方法

org.apache.catalina.core.ContainerBase.ContainerBackgroundProcessorMonitor ()

protected class ContainerBackgroundProcessorMonitor implements Runnable {
    @Override
    public void run() {
        if (getState().isAvailable()) {
            threadStart();
        }
    }
}

org.apache.catalina.core.ContainerBase.threadStart()

protected void threadStart() {
    if (backgroundProcessorDelay > 0
            && (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
            && (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
        if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
            // There was an error executing the scheduled task, get it and log it
            try {
                backgroundProcessorFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                log.error(sm.getString("containerBase.backgroundProcess.error"), e);
            }
        }
        backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
                .scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
                        backgroundProcessorDelay, backgroundProcessorDelay,
                        TimeUnit.SECONDS);
    }

threadStart方法启动了一个后台线程,任务为加载ContainerBackgroundProcessor类,该类的run方法中调用了processChildren()

org.apache.catalina.core.ContainerBase.ContainerBackgroundProcessor ()

protected class ContainerBackgroundProcessor implements Runnable {

    @Override
    public void run() {
        processChildren(ContainerBase.this);
    }

    protected void processChildren(Container container) {
        ClassLoader originalClassLoader = null;

        try {
            if (container instanceof Context) {
                Loader loader = ((Context) container).getLoader();
                // Loader will be null for FailedContext instances
                if (loader == null) {
                    return;
                }

                // Ensure background processing for Contexts and Wrappers
                // is performed under the web app's class loader
                originalClassLoader = ((Context) container).bind(false, null);
            }
            container.backgroundProcess();
            Container[] children = container.findChildren();
            for (Container child : children) {
                if (child.getBackgroundProcessorDelay() <= 0) {
                    processChildren(child);
                }
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("containerBase.backgroundProcess.error"), t);
        } finally {
            if (container instanceof Context) {
                ((Context) container).unbind(false, originalClassLoader);
            }
        }
    }
}

processChildren()会不断迭代StandardEngine的子容器并调用这些子容器的backgroundProcess方法,这里直接浏览StandardEngine的孙子容器StandardManager的backgroundProcess实现,即MangerBase的backgroundProcess方法

org.apache.catalina.session.ManagerBase.backgroundProcess()

@Override
public void backgroundProcess() {
    count = (count + 1) % processExpiresFrequency;
    if (count == 0)
        processExpires();
}

backgroundProcess里实现了一个简单的算法:

count:计数器,起始为0

processExpiresFrequency:执行processExpires方法的频率,默认为6

每执行一次backgroundProcess方法,count会增加1,每当count+1与processExpiresFrequency求模等于0,则调用processExpires。简而言之,每执行processExpiresFrequency指定次数的backgroundProcess方法,执行一次processExpires方法

org.apache.catalina.session.ManagerBase.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);
    for (Session session : sessions) {
        if (session != null && !session.isValid()) {
            expireHere++;
        }
    }
    long timeEnd = System.currentTimeMillis();
    if(log.isDebugEnabled())
         log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
    processingTime += ( timeEnd - timeNow );

}

processExpires方法的执行步骤如下:

  1. 从缓存中取出所有的Session
  2. 逐个校验每个Session是否过期,处理已经过期的Session

Session的标准实现是StandardSession,其isValid方法的主要功能是判断Session是否过期,对于过期的Session,将其expiring状态改为ture。

判断过期的公式为:((当前时间 - Session的最后访问时间)/ 1000) >= 最大访问间隔

org.apache.catalina.session.StandardSession.isValid()

@Override
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;
}

总结

Tomcat对于Session的管理过程包括创建、分配、维护、跟踪和销毁

posted @ 2021-04-08 19:44  Kaiyko  阅读(515)  评论(0)    收藏  举报