zoukankan      html  css  js  c++  java
  • Tomcat的Session管理(一)

    摘要:本文目的在介绍tomcat中session相关的架构以及session的查询。

    在Servlet开发中,Session代表用户会话,开发人员经常使用Session来临时存储一些信息,那么Session到底是什么,Tomcat中是如何对Session进行管理的,我们今天到源码中查看下。

    查看相关资料,我们先看下Session相关的类图

    从图上可以看到Session对应的接口有两个SessionHttpSessionStandardSession实现了这2个接口,StandardSessionFacade实现了HttpSessionStandardSessionFacede包含了StandardSession。看起来有点饶,我们详细讲解下每个类的含义。

    • HttpSession

    我们都知道Tomcat是支持Servlet规范的web容器,所以内部会包含一些Servlet的内容。HttpSession就是在Servlet规范中标准的Session定义。注意HttpSession的全路径是javax.servlet.http.HttpSession

    • Session

    Session接口是Catalina内部的Session的标准定义。全路径org.apache.catalina.Session

    • StandardSession

    Catalina内部Session接口的标准实现,基础功能的提供类,需要重点分析,全路径org.apache.catalina.session.StandardSession

    • StandardSessionFacade

    StandardSession的外观类,也是Catalina内部的类,之所以存在这个类是因为不想直接让开发人员操作到StandardSession,这样可以保证类的安全。该类的全路径org.apache.catalina.session.StandardSession

    在Catalina中,Session由Session管理器组件负责管理,例如创建和销毁Session管理器是org.apache.catalina.Manager接口的实例。我们来看看Manager管理器相关的类图。

    Manager

    Catalina中session管理器概念的顶层接口。其中定义了一些对session的基本操作,如创建,查找,添加,移除以及对特定session对象的属性的修改。除了基本操作以外还定义了一些特殊的例如session的序列化反序列化等等。

    ManagerBase

    抽象类,对Manager接口作了基本实现,方便继承类的复写以及实现,就像其他xxxBase类起到了一样的作用。

    StandardManager

    Catalina中默认的Session管理器的实现类,具体功能我们下面分析。

    PersistentManagerBase

    Session管理器中打标持久化Session的父类,虽然StandardManager也可以将Session持久化,但是只是将Session持久化为一个文件(后面会说到),PersistentManagerBase类和StandardManager类的区别在于前者的存储器的表现形式可以有多种,比如数据库,文件等(具体后面讨论)

    PersistentManager

    PersistentManager基础上增加了两个属性。

    DistributedManager

    PersistentManagerBaseBackupManager类中抽象出来的一个接口,这个接口表示了两者一个共同的属性:不会持有所有session的对象,但是能找到所有的session。例如PersistentManagerBase可以将session同时存放在内存和持久化介质中。

    ClusterManager

    分布式集群session处理器父类,定义了一些基本方法例如获取所有tomcat集群的机器,获取其他集群的信息等基本功能。

    ClusterManagerBase

    抽象类,对ClusterManager作了基本实现。

    BackupManager

    集群间session复制策略的一种实现,会话数据只有一个备份节点,这个备份节点的位置集群中所有节点都可见。

    DeltaManager

    集群建session复制策略的一种实现,采用的方式是只复制差异部分,是分布式集群session同步中最好的同步方式。

    了解一些基本信息后,我们来查看我们最常用的getSession()方法,从而来了解session运作的方式。

    在Servlet中我们使用HttpServletRequestgetSession()方法来获取session对象,而真正执行getSession方法的其实是org.apache.catalina.connector.RequestFacade对象

    public class RequestFacade implements HttpServletRequest
    

    RequestFacade对象实现了HttpServletRequest内部封装了org.apache.catalina.connector.Request对象,我们查看getSession()方法:

     @Override
    public HttpSession getSession() {
        if (request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        }
        return getSession(true);
    }
    
    @Override
    public HttpSession getSession(boolean create) {
        if (request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        }
    
        if (SecurityUtil.isPackageProtectionEnabled()){
            return AccessController.doPrivileged(new GetSessionPrivilegedAction(create));
        } else {
            return request.getSession(create);
        }
    }
    

    可以看出最终调用的还是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()方法,由于源码过长,删减了部分内容,保留了核心代码。

    protected Session doGetSession(boolean create) {
    	//判断context
        if (context == null) {
            return (null);
        }
    	//判断session是否有效
        if ((session != null) && !session.isValid()) {
            session = null;
        }
        if (session != null) {
            return (session);
        }
    	//获取跟context绑定的sessionManager  这里默认返回的是StandardManager
        Manager manager = context.getManager();
        if (manager == null) {
            return null;        // Sessions are not supported
        }
        if (requestedSessionId != null) {
            try {
    			//根据requestSessionId 查询指定的session
    			//111111111111
                session = manager.findSession(requestedSessionId);
            } catch (IOException e) {
                session = null;
            }
    		//判断获取到的session是否有效
            if ((session != null) && !session.isValid()) {
                session = null;
            }
    		
            if (session != null) {
    			//session有效 增加访问时间和次数
                session.access();
                return (session);
            }
        }
    	
        //如果没有找到已存在的session并且 要求创建新session
       	//获取此次request对应的sessionid
        String sessionId = getRequestedSessionId();
      	//。。。略部分代码
    	//222222222222
        session = manager.createSession(sessionId);
    
       	//创建cookie
        Cookie cookie =ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(),isSecure());
    	//将cookie设置到response 中
        response.addSessionCookieInternal(cookie);
    	
        if (session == null) {
            return null;
        }
    	//增加访问时间和次数
        session.access();
        return session;
    }
    

    代码很简单,思路也很清晰,很容易理解,这里有3处需要关注下。

    在代码1的地方,调用了manager.findSession(),requestedSessionId是个String类型的字符串(稍后说如何创建一个这样的字符串)。在Catalina中默认的是StandardManager,所以查看StandardManagerfindSession()方法,最后在其父类ManagerBase中找到相关方法:

        @Override
    public Session findSession(String id) throws IOException {
        if (id == null) {
            return null;
        }
        return sessions.get(id);
    }
    
     /**
     * The set of currently active Sessions for this Manager, keyed by
     * session identifier.
     */
    protected Map<String, Session> sessions = new ConcurrentHashMap<String, Session>();
    

    从源码可以得出结论,一个类型的session管理器,他都有一个总的session容器,就是一个ConcurrentHashMap实例,key是sessionId,value是对应的session对象。

    在代码2的地方由于在已有的session的map中没有找到session,所以需要创建一个新的session,createSession()源码如下(在ManagerBase类中):

     @Override
    public Session createSession(String sessionId) {
        //判断可容纳session数量
        if ((maxActiveSessions >= 0) &&
                (getActiveSessions() >= maxActiveSessions)) {
            rejectedSessions++;
            throw new TooManyActiveSessionsException(
                    sm.getString("managerBase.createSession.ise"),
                    maxActiveSessions);
        }
        //创建一个全新的session对象
        Session session = createEmptySession();
    
    	//设置基本属性
        session.setNew(true);
        session.setValid(true);
        session.setCreationTime(System.currentTimeMillis());
        session.setMaxInactiveInterval(((Context) getContainer()).getSessionTimeout() * 60);
        String id = sessionId;
    	//设置sessionId 唯一标识负
        if (id == null) {
    		//如果id为空 新生成一个sessionId
            id = generateSessionId();
        }
        session.setId(id);
    	//session数量+1
        sessionCounter++;
    	
        SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
        synchronized (sessionCreationTiming) {
            sessionCreationTiming.add(timing);
            sessionCreationTiming.poll();
        }
        return (session);
    
    }
    

    需要关注下创建新的session对象其实就是new 了一个StandardSession对象,还有generateSessionId()方法是生成一个唯一的sessionId,有兴趣的可以自行查看。最后在代码session.setId(id)中往下查看可以看到:

    sessions.put(session.getIdInternal(), session);
    

    也就是把新创建的session放入到管理器的session容器中(ConcurrentHashMap)对象。

    doGetSession方法中最后需要关注的就是requestedSessionId是如何生成的,我们先看下定义

        /**
     * The requested session ID (if any) for this request.
     */
    protected String requestedSessionId = null;
    

    可以看出是个字符串类型的标识符并且和request有关,由于tomcat的请求流程我们还没梳理到,所以这里直接给出生成的方法。

    首先,这个所谓的requestedSessionId对应的就是我们每次请求都有一个唯一session标识符,为了以后同一个用户再次请求的时候可以使用同一个session(会话)。既然跟request有关,那么就可以查看request相关方法,而之前在讲解启动的时候提到过Connector是专门来处理请求的,而对应http请求的ConnectorHttp11Processor。首先用户发送一个http请求传递给Http11Processor,经由Http11Processor解析封装在 org.apache.coyote.Request然后传递给CoyoteAdaptercoyoteAdapter是一个适配器,将coyote框 架封装的org.apache.coyote.Request适配给org.apache.catalina.connector.Request,而解析的过程就在CoyoteAdapter中。

    解析requestSessionId分为两步,一个是从请求url中解析,另一步是从cookie中解析,之所以需要解析两次是因为有的用户会禁用cookie,而禁用cookie的时候sessionId则会带在请求url中(其实就是我们熟知的jsessionId)

    从请求url中解析(只保留了核心代码)

    首先如果sessionId保留在url中是以如下形式

    http://xxx.com?a=1&b=1&c=1;k1=v1;k2=v2;jsessionId=xxxx
    

    可以看出requestSessionId是保存在;之后的,之所以解释下 是方便理解。

      protected void parsePathParameters(org.apache.coyote.Request req,
            Request request) {
    
        //转码uri 
        req.decodedURI().toBytes();
        ByteChunk uriBC = req.decodedURI().getByteChunk();
    	//查询;位置
        int semicolon = uriBC.indexOf(';', 0);
    	//设置编码
        String enc = connector.getURIEncoding();
        if (enc == null) {
            enc = "ISO-8859-1";
        }
        Charset charset = null;
        try {
            charset = B2CConverter.getCharset(enc);
        } catch (UnsupportedEncodingException e1) {
            log.warn(sm.getString("coyoteAdapter.parsePathParam",
                    enc));
        }
    	//省略部分代码
    	//循环遍历,设置所有的kv		
        while (semicolon > -1) {
            // 11111111111111
            int start = uriBC.getStart();
            int end = uriBC.getEnd();
    
            int pathParamStart = semicolon + 1;
            int pathParamEnd = ByteChunk.findBytes(uriBC.getBuffer(),
                    start + pathParamStart, end,
                    new byte[] {';', '/'});
    
            String pv = null;
    
            if (pathParamEnd >= 0) {
                if (charset != null) {
                    pv = new String(uriBC.getBuffer(), start + pathParamStart,
                                pathParamEnd - pathParamStart, charset);
                }
                // Extract path param from decoded request URI
                byte[] buf = uriBC.getBuffer();
                for (int i = 0; i < end - start - pathParamEnd; i++) {
                    buf[start + semicolon + i]
                        = buf[start + i + pathParamEnd];
                }
                uriBC.setBytes(buf, start,
                        end - start - pathParamEnd + semicolon);
            } else {
                if (charset != null) {
                    pv = new String(uriBC.getBuffer(), start + pathParamStart,
                                (end - start) - pathParamStart, charset);
                }
                uriBC.setEnd(start + semicolon);
            }
    
    		//22222222222
            if (pv != null) {
                int equals = pv.indexOf('=');
                if (equals > -1) {
                    String name = pv.substring(0, equals);
                    String value = pv.substring(equals + 1);
                    request.addPathParameter(name, value);
                }
            }
    
            semicolon = uriBC.indexOf(';', semicolon);
        }
    }
    

    代码还算容易理解,比较清楚的都标注在代码中了,从标注1到标注2的地方主要做的就是获取一对kv结构(k1=v1这样的结构)。而在代码2的判断的地方可以看到很明显request.addPathParameter(name, value);,request把kv都设置进去了,这其中就包括了jsessionId,以上就是在解析url的时候第一次设置requestSessionId

    从cookie中解析

    protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) {
    
        //如果cookie被禁用 直接返回
        Context context = (Context) request.getMappingData().context;
        if (context != null && !context.getServletContext()
                .getEffectiveSessionTrackingModes().contains(
                        SessionTrackingMode.COOKIE)) {
            return;
        }
    
    	//获取cookie
        Cookies serverCookies = req.getCookies();
        int count = serverCookies.getCookieCount();
        if (count <= 0) {
            return;
        }
    	//获取cookie中session的key
        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());
    				//设置requestSessionId
                    request.setRequestedSessionId(scookie.getValue().toString());
                    request.setRequestedSessionCookie(true);
                    request.setRequestedSessionURL(false);
                  
                } else {
                    if (!request.isRequestedSessionIdValid()) {
                        // Replace the session id until one is valid
                        convertMB(scookie.getValue());
                        request.setRequestedSessionId
                            (scookie.getValue().toString());
                    }
                }
            }
        }
    }
    

    代码比较简单,以上就是requestSessionId是如何设置的。

    看到这里,doGetSession()方法我们就看完了,可以看到它返回的是一个StandardSession对象,最后在getSession()方法中返回的是StandardSession对象的getSession()方法,查看下源码。

      @Override
    public HttpSession getSession() {
        Session session = doGetSession(true);
        if (session == null) {
            return null;
        }
    
        return session.getSession();
    }
    
     /**
     * Return the <code>HttpSession</code> for which this object
     * is the facade.
     */
    @Override
    public HttpSession getSession() {
        if (facade == null){
            if (SecurityUtil.isPackageProtectionEnabled()){
                final StandardSession fsession = this;
                facade = AccessController.doPrivileged(
                        new PrivilegedAction<StandardSessionFacade>(){
                    @Override
                    public StandardSessionFacade run(){
                        return new StandardSessionFacade(fsession);
                    }
                });
            } else {
                facade = new StandardSessionFacade(this);
            }
        }
        return (facade);
    
    }
    

    可以看出最后getSession()方法返回的并不是StandardSession对象,而是StandardSession对象的外观类StandardSessionFacade。这也和我们一开始介绍session类图的时候描述的一致。

  • 相关阅读:
    pat甲级 1155 Heap Paths (30 分)
    pat甲级 1152 Google Recruitment (20 分)
    蓝桥杯 基础练习 特殊回文数
    蓝桥杯 基础练习 十进制转十六进制
    蓝桥杯 基础练习 十六进制转十进制
    蓝桥杯 基础练习 十六进制转八进制
    51nod 1347 旋转字符串
    蓝桥杯 入门训练 圆的面积
    蓝桥杯 入门训练 Fibonacci数列
    链表相关
  • 原文地址:https://www.cnblogs.com/coldridgeValley/p/6016211.html
Copyright © 2011-2022 走看看