• TOMCAT8源码分析——SESSION管理分析(上)


    前言

      对于广大java开发者而已,对于J2EE规范中的Session应该并不陌生,我们可以使用Session管理用户的会话信息,最常见的就是拿Session用来存放用户登录、身份、权限及状态等信息。对于使用Tomcat作为Web容器的大部分开发人员而言,Tomcat是如何实现Session标记用户和管理Session信息的呢?

    概述

    SESSION

      Tomcat内部定义了Session和HttpSession这两个会话相关的接口,其类继承体系如图1所示。

    图1  Session类继承体系

    图1中额外列出了Session的类继承体系,这里对他们逐个进行介绍。

    Session:Tomcat中有关会话的基本接口规范,图1列出了它定义的主要方法,表1对这些方法进行介绍。

    表1  Session接口说明

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

    HttpSession:在HTTP客户端与HTTP服务端提供的一种会话的接口规范,图1列出了它定义的主要方法,表2对这些方法进行介绍。

    表2  HttpSession接口说明

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

    ClusterSession:集群部署下的会话接口规范,图1列出了它的主要方法,表3对这些方法进行介绍。

    表3  ClusterSession接口说明

    方法 描述
    isPrimarySession() 是否是集群的主Session
    setPrimarySession(boolean primarySession) 设置集群主Session

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

    在部署Tomcat集群时,需要使集群中各个节点的会话状态保持同步,目前Tomcat提供了两种同步策略:

    • ReplicatedSession:每次都把整个会话对象同步给集群中的其他节点,其他节点然后更新整个会话对象。这种实现比较简单方便,但会造成大量无效信息的传输。
    • DeltaSession:对会话中增量修改的属性进行同步。这种方式由于是增量的,所以会大大降低网络I/O的开销,但是实现上会比较复杂因为涉及到对会话属性操作过程的管理。

    SESSION管理器

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

    图2  Session管理器的类继承体系

    对应图2中的内容我们下面逐个描述:

    Manager:Tomcat对于Session管理器定义的接口规范,图2已经列出了Manager接口中定义的主要方法,表4详细描述了这些方法的作用。

    表4  Manager接口说明

    方法 描述
    getContainer()/setContainer(container : Container)  获取或设置Session管理器关联的容器,一般为Context容器
    getDistributable()/setDistributable(distributable : boolean)   获取或设置Session管理器是否支持分布式
    getMaxInactiveInterval()/setMaxInactiveInterval(interval : int)   获取或设置Session管理器创建的Session的最大非活动时间间隔
    getSessionIdLength()/setSessionIdLength(idLength : int)  获取或设置Session管理器创建的Session ID的长度
    getSessionCounter()/setSessionCounter(sessionCounter : long)   获取或设置Session管理器创建的Session总数
    getMaxActive()/setMaxActive(maxActive : int)  获取或设置当前已激活Session的最大数量
    getActiveSessions()   获取当前激活的所有Session
    getExpiredSessions()/setExpiredSessions(expiredSessions : long)  获取或设置当前已过期Session的数量
    getRejectedSessions()/setRejectedSessions(rejectedSessions : int)  获取或设置已拒绝创建Session的数量
    getSessionMaxAliveTime()/setSessionMaxAliveTime(sessionMaxAliveTime : int)   获取或设置已过期Session中的最大活动时长
    getSessionAverageAliveTime()/setSessionAverageAliveTime(sessionAverageAliveTime : int)  获取或设置已过期Session的平均活动时长
    add(session : Session)/remove(session : Session)  给Session管理器增加或删除活动Session
    changeSessionId(session : Session)  给Session设置新生成的随机Session ID
    createSession(sessionId : String)  基于Session管理器的默认属性配置创建新的Session
    findSession(id : String)  返回sessionId参数唯一标记的Session
    findSessions()  返回Session管理器管理的所有活动Session
    load()/unload()  从持久化机制中加载Session或向持久化机制写入Session
    backgroundProcess()  容器接口中定义的为具体容器在后台处理相关工作的实现,Session管理器基于此机制实现了过期Session的销毁

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

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

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

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

    StandardManager:不用配置<Store>元素,当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方法(见代码清单1)被调用时,StandardContext还会启动StandardManager。

    代码清单1

        @Override
        protected synchronized void startInternal() throws LifecycleException {
                   
            // 省略与Session管理无关的代码
                           
                    // Acquire clustered manager
                    Manager contextManager = null;
                    if (manager == null) {
                        if ( (getCluster() != null) && distributable) {
                            try {
                                contextManager = getCluster().createManager(getName());
                            } catch (Exception ex) {
                                log.error("standardContext.clusterFail", ex);
                                ok = false;
                            }
                        } else {
                            contextManager = new StandardManager();
                        }
                    } 
                    
                    // Configure default manager if none was specified
                    if (contextManager != null) {
                        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
                    if ((manager != null) && (manager instanceof Lifecycle)) {
                        ((Lifecycle) getManager()).start();
                    }
        
                    // Start ContainerBackgroundProcessor thread
                    super.threadStart();
                } catch(Exception e) {
                    log.error("Error manager.start()", e);
                    ok = false;
                }
                
         // 省略与Session管理无关的代码
        }

    从代码清单1可以看到StandardContext的startInternal方法中涉及Session管理的执行步骤如下:

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

    StandardManager的start方法实际上是调用LifecycleBase的start方法,实现见代码清单2。

    代码清单2

    /**
         * {@inheritDoc}
         */
        @Override
        public final synchronized void start() throws LifecycleException {
    
            if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
                    LifecycleState.STARTED.equals(state)) {
    
                if (log.isDebugEnabled()) {
                    Exception e = new LifecycleException();
                    log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
                } else if (log.isInfoEnabled()) {
                    log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
                }
    
                return;
            }
    
            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.
                ExceptionUtils.handleThrowable(t);
                setStateInternal(LifecycleState.FAILED, null, false);
                throw new LifecycleException(sm.getString("lifecycleBase.startFail", toString()), t);
            }
        }

    从代码清单2可以看出启动StandardManager的步骤如下:

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

    STANDARDMANAGER的初始化

       经过上面的分析,我们知道启动StandardManager的第一步就是调用父类LifecycleBase的init方法,关于此方法已在《Tomcat源码分析——生命周期管理》一文详细介绍,所以我们只需要关心StandardManager的initInternal。StandardManager本身并没有实现initInternal方法,但是StandardManager的父类ManagerBase实现了此方法,其实现见代码清单3。

    代码清单3

    @Override
        protected void initInternal() throws LifecycleException {
            super.initInternal();
    
            if (context == null) {
                throw new LifecycleException(sm.getString("managerBase.contextNull"));
            }
        }

    根据以上的分析,StandardManager的初始化主要就是执行了ManagerBase的initInternal方法。

    STANDARDMANAGER的启动

      调用StandardManager的startInternal方法用于启动StandardManager,见代码清单7。

     代码清单7

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

     从代码清单7可以看出启动StandardManager的步骤如下:

    步骤一 调用父类ManagerBase的startInternal方法(见代码清单8)初始化随机数生成器;

    注意:此处调用startInternal方法的目的不是为了生成Session ID,而是为了初始化随机数生成器。

    代码清单8

    @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");
            }
        }

    从清单8中的start方法了解到,实际上是调用SessionIdGeneratorBase的startInternal方法,代码如下

    @Override
        protected void startInternal() throws LifecycleException {
            // Ensure SecureRandom has been initialised
            generateSessionId();
    
            setState(LifecycleState.STARTING);
        }
    generateSessionId方法代码如下:
    /**
         * Generate and return a new session identifier.
         */
        @Override
        public String generateSessionId() {
            return generateSessionId(jvmRoute);
        }
    此方法实际上调用了子类StandardSessionIdGenerator类的generateSessionId方法,代码如下:
    @Override
        public String generateSessionId(String route) {
    
            byte random[] = new byte[16];
            int sessionIdLength = getSessionIdLength();
    
            // Render the result as a String of hexadecimal digits
            // Start with enough space for sessionIdLength and medium route size
            StringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20);
    
            int resultLenBytes = 0;
    
            while (resultLenBytes < sessionIdLength) {
                getRandomBytes(random);
                for (int j = 0;
                j < random.length && resultLenBytes < sessionIdLength;
                j++) {
                    byte b1 = (byte) ((random[j] & 0xf0) >> 4);
                    byte b2 = (byte) (random[j] & 0x0f);
                    if (b1 < 10)
                        buffer.append((char) ('0' + b1));
                    else
                        buffer.append((char) ('A' + (b1 - 10)));
                    if (b2 < 10)
                        buffer.append((char) ('0' + b2));
                    else
                        buffer.append((char) ('A' + (b2 - 10)));
                    resultLenBytes++;
                }
            }
    
            if (route != null && route.length() > 0) {
                buffer.append('.').append(route);
            } else {
                String jvmRoute = getJvmRoute();
                if (jvmRoute != null && jvmRoute.length() > 0) {
                    buffer.append('.').append(jvmRoute);
                }
            }
    
            return buffer.toString();
        }

    步骤二  加载持久化的Session信息。为什么Session需要持久化?由于在StandardManager中,所有的Session都维护在一个ConcurrentHashMap中,因此服务器重启或者宕机会造成这些Session信息丢失或失效,为了解决这个问题,Tomcat将这些Session通过持久化的方式来保证不会丢失。下面我们来看看StandardManager的load方法的实现,见代码清单9所示。

    代码清单9

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

    如果需要安全机制是打开的并且包保护模式打开,会通过创建PrivilegedDoLoad来加载持久化的Session,其实现如代码清单10所示。

    代码清单10

    /**
         * Performs the specified {@code PrivilegedExceptionAction} with
         * privileges enabled.  The action is performed with <i>all</i> of the
         * permissions possessed by the caller's protection domain.
         *
         * <p> If the action's {@code run} method throws an <i>unchecked</i>
         * exception, it will propagate through this method.
         *
         * <p> Note that any DomainCombiner associated with the current
         * AccessControlContext will be ignored while the action is performed.
         *
         * @param <T> the type of the value returned by the
         *                  PrivilegedExceptionAction's {@code run} method.
         *
         * @param action the action to be performed
         *
         * @return the value returned by the action's {@code run} method
         *
         * @exception PrivilegedActionException if the specified action's
         *         {@code run} method threw a <i>checked</i> exception
         * @exception NullPointerException if the action is {@code null}
         *
         * @see #doPrivileged(PrivilegedAction)
         * @see #doPrivileged(PrivilegedExceptionAction,AccessControlContext)
         * @see #doPrivilegedWithCombiner(PrivilegedExceptionAction)
         * @see java.security.DomainCombiner
         */
        @CallerSensitive
        public static native <T> T
            doPrivileged(PrivilegedExceptionAction<T> action)
            throws PrivilegedActionException;

    根据代码清单9知道默认情况下,加载Session信息的方法也是doLoad。所以我们只需要看看doLoad的实现了,见代码清单11。

    代码清单11

    /**
         * Load any currently active sessions that were previously unloaded
         * to the appropriate persistence mechanism, if any.  If persistence is not
         * supported, this method returns without doing anything.
         *
         * @exception ClassNotFoundException if a serialized class cannot be
         *  found during the reload
         * @exception IOException if an input/output error occurs
         */
        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()) {
                            file.delete();
                        }
                    }
                }
            } catch (FileNotFoundException e) {
                if (log.isDebugEnabled()) {
                    log.debug("No persisted data file found");
                }
                return;
            }
    
            if (log.isDebugEnabled()) {
                log.debug("Finish: Loading persisted sessions");
            }
        }

     从代码清单11看到StandardManager的doLoad方法的执行步骤如下:

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

    至此,有关StandardManager的启动就介绍到这里,我将会在《TOMCAT源码分析——SESSION管理分析(下)》一文讲解Session的分配、追踪、销毁等内容。

    如需转载,请标明本文作者及出处——作者:jiaan.gja,本文原创首发:博客园,原文链接:http://www.cnblogs.com/jiaan-geng/p/4913616.html
  • 相关阅读:
    Coding 初级教程(一)——用GitHub的GUI客户端对Coding的项目进行管理
    自己以前写的日记,现在公开(别的文章需要用)1
    7-网页,网站,微信公众号基础入门(微信配网_申请微信公众号)
    关于Keil4 转到 Keil5以后的一些错误解决
    6-网页,网站,微信公众号基础入门(PHP学习_1)
    5-网页,网站,微信公众号基础入门(配置网站--PHP配置上数据库)
    4-网页,网站,微信公众号基础入门(配置网站--下载安装PHP)
    3-网页,网站,微信公众号基础入门(学习网页_2)
    7-STM32物联网开发WIFI(ESP8266)+GPRS(Air202)系统方案安全篇(GPRS模块SSL连接MQTT)
    6-STM32物联网开发WIFI(ESP8266)+GPRS(Air202)系统方案安全篇(Wi-Fi模块SSL连接MQTT)
  • 原文地址:https://www.cnblogs.com/nizuimeiabc1/p/8691264.html
Copyright © 2020-2023  润新知