摘要:本文目的在介绍tomcat中session相关的架构以及session的查询。
在Servlet开发中,Session代表用户会话,开发人员经常使用Session来临时存储一些信息,那么Session到底是什么,Tomcat中是如何对Session进行管理的,我们今天到源码中查看下。
查看相关资料,我们先看下Session
相关的类图
从图上可以看到Session对应的接口有两个Session
和HttpSession
,StandardSession
实现了这2个接口,StandardSessionFacade
实现了HttpSession
,StandardSessionFacede
包含了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
从PersistentManagerBase
和BackupManager
类中抽象出来的一个接口,这个接口表示了两者一个共同的属性:不会持有所有session的对象,但是能找到所有的session。例如PersistentManagerBase
可以将session同时存放在内存和持久化介质中。
ClusterManager
分布式集群session处理器父类,定义了一些基本方法例如获取所有tomcat集群的机器,获取其他集群的信息等基本功能。
ClusterManagerBase
抽象类,对ClusterManager
作了基本实现。
BackupManager
集群间session复制策略的一种实现,会话数据只有一个备份节点,这个备份节点的位置集群中所有节点都可见。
DeltaManager
集群建session复制策略的一种实现,采用的方式是只复制差异部分,是分布式集群session同步中最好的同步方式。
了解一些基本信息后,我们来查看我们最常用的getSession()
方法,从而来了解session运作的方式。
在Servlet中我们使用HttpServletRequest
的getSession()
方法来获取session对象,而真正执行getSession
方法的其实是org.apache.catalina.connector.RequestFacade
对象
public class RequestFacade implements HttpServletRequest
RequestFacade
对象实现了HttpServletRequest
内部封装了org.apache.catalina.connector.Request
对象,我们查看getSession()
方法:
@Overridepublic HttpSession getSession() { if (request == null) { throw new IllegalStateException(sm.getString("requestFacade.nullRequest")); } return getSession(true);}@Overridepublic 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()
方法,源码如下:
@Overridepublic 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
,所以查看StandardManager
的findSession()
方法,最后在其父类ManagerBase
中找到相关方法:
@Overridepublic 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 Mapsessions = new ConcurrentHashMap ();
从源码可以得出结论,一个类型的session管理器,他都有一个总的session容器,就是一个ConcurrentHashMap
实例,key是sessionId
,value是对应的session对象。
在代码2的地方由于在已有的session的map中没有找到session,所以需要创建一个新的session,createSession()
源码如下(在ManagerBase
类中):
@Overridepublic 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请求的Connector
是Http11Processor
。首先用户发送一个http请求传递给Http11Processor
,经由Http11Processor
解析封装在 org.apache.coyote.Request
然后传递给CoyoteAdapter
,coyoteAdapter
是一个适配器,将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()
方法,查看下源码。
@Overridepublic HttpSession getSession() { Session session = doGetSession(true); if (session == null) { return null; } return session.getSession();} /** * Return theHttpSession
for which this object * is the facade. */@Overridepublic HttpSession getSession() { if (facade == null){ if (SecurityUtil.isPackageProtectionEnabled()){ final StandardSession fsession = this; facade = AccessController.doPrivileged( new PrivilegedAction(){ @Override public StandardSessionFacade run(){ return new StandardSessionFacade(fsession); } }); } else { facade = new StandardSessionFacade(this); } } return (facade);}
可以看出最后getSession()
方法返回的并不是StandardSession
对象,而是StandardSession
对象的外观类StandardSessionFacade
。这也和我们一开始介绍session类图的时候描述的一致。