博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
阿里架构师手写Tomcat——Session源码解析
阅读量:7129 次
发布时间:2019-06-28

本文共 15456 字,大约阅读时间需要 51 分钟。

在 web 开发中,我们经常会用到 Session 来保存会话信息,包括用户信息、权限信息,等等。在这篇文章中,我们将分析 tomcat 容器是如何创建 session、销毁 session,又是如何对 HttpSessionListener 进行事件通知

tomcat session 设计分析

tomcat session 组件图如下所示,其中Context对应一个webapp应用,每个webapp有多个HttpSessionListener, 并且每个应用的session是独立管理的,而session的创建、销毁由Manager组件完成,它内部维护了 N 个Session实例对象。在前面的文章中,我们分析了Context组件,它的默认实现是StandardContext,它与Manager是一对一的关系,Manager创建、销毁会话时,需要借助StandardContext获取 HttpSessionListener列表并进行事件通知,而StandardContext的后台线程会对Manager进行过期` Session 的清理工作

org.apache.catalina.Manager接口的主要方法如下所示,它提供了 Context、org.apache.catalina.SessionIdGeneratorgetter/setter接口,以及创建、添加、移除、查找、遍历Session的 API 接口,此外还提供了Session持久化的接口(load/unload) 用于加载/卸载会话信息,当然持久化要看不同的实现类

public interface Manager {	public Context getContext();	public void setContext(Context context);	public SessionIdGenerator getSessionIdGenerator();	public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator);	public void add(Session session);	public void addPropertyChangeListener(PropertyChangeListener listener);	public void changeSessionId(Session session);	public void changeSessionId(Session session, String newId);	public Session createEmptySession();	public Session createSession(String sessionId);	public Session findSession(String id) throws IOException;	public Session[] findSessions();	public void remove(Session session);	public void remove(Session session, Boolean update);	public void removePropertyChangeListener(PropertyChangeListener listener);	public void unload() throws IOException;	public void backgroundProcess();	public Boolean willAttributeDistribute(String name, Object value);}复制代码

tomcat8.5 提供了 4 种实现,默认使用 StandardManager,tomcat 还提供了集群会话的解决方案,但是在实际项目中很少运用,关于 Manager 的详细配置信息请参考 tomcat 官方文档

  • StandardManager:Manager 默认实现,在内存中管理 session,宕机将导致 session 丢失;但是当调用 Lifecycle 的 start/stop 接口时,将采用 jdk 序列化保存 Session 信息,因此当 tomcat 发现某个应用的文件有变更进行 reload 操作时,这种情况下不会丢失 Session 信息
  • DeltaManager:增量 Session 管理器,用于Tomcat集群的会话管理器,某个节点变更 Session 信息都会同步到集群中的所有节点,这样可以保证 Session 信息的实时性,但是这样会带来较大的网络开销
  • BackupManager:用于 Tomcat 集群的会话管理器,与DeltaManager不同的是,某个节点变更 Session 信息的改变只会同步给集群中的另一个 backup 节点
  • PersistentManager:当会话长时间空闲时,将会把 Session 信息写入磁盘,从而限制内存中的活动会话数量;此外,它还支持容错,会定期将内存中的 Session 信息备份到磁盘

Session 相关的类图如下所示,StandardSession 同时实现了 javax.servlet.http.HttpSession、org.apache.catalina.Session 接口,并且对外提供的是 StandardSessionFacade 外观类,保证了 StandardSession 的安全,避免开发人员调用其内部方法进行不当操作。而 org.apache.catalina.connector.Request 实现了 javax.servlet.http.HttpServletRequest 接口,它持有 StandardSession 的引用,对外也是暴露 RequestFacade 外观类。而 StandardManager 内部维护了其创建的 StandardSession,是一对多的关系,并且持有 StandardContext 的引用,而 StandardContext 内部注册了 webapp 所有的 HttpSessionListener 实例。

创建Session

我们以 HttpServletRequest#getSession() 作为切入点,对 Session 的创建过程进行分析

public class SessionExample extends HttpServlet {	public void doGet(HttpServletRequest request, HttpServletResponse response)	        throws IOException, ServletException  {		HttpSession session = request.getSession();		// other code......	}}复制代码

整个流程图如下图所示:

tomcat 创建 session 的流程如上图所示,我们的应用程序拿到的 HttpServletRequest 是 org.apache.catalina.connector.RequestFacade(除非某些 Filter 进行了特殊处理),它是 org.apache.catalina.connector.Request 的门面模式。首先,会判断 Request 对象中是否存在 Session,如果存在并且未失效则直接返回,因为在 tomcat 中 Request 对象是被重复利用的,只会替换部分组件,所以会进行这步判断。此时,如果不存在 Session,则尝试根据 requestedSessionId 查找 Session,而该 requestedSessionId 会在 HTTP Connector 中进行赋值(如果存在的话),如果存在 Session 的话则直接返回,如果不存在的话,则创建新的 Session,并且把 sessionId 添加到 Cookie 中,后续的请求便会携带该 Cookie,这样便可以根据 Cookie 中的sessionId 找到原来创建的 Session 了

在上面的过程中,Session 的查找、创建都是由 Manager 完成的,下面我们分析下 StandardManager 创建 Session 的具体逻辑。首先,我们来看下 StandardManager 的类图,它也是个 Lifecycle 组件,并且 ManagerBase 实现了主要的逻辑。

整个创建 Session 的过程比较简单,就是实例化 StandardSession 对象并设置其基本属性,以及生成唯一的 sessionId,其次就是记录创建时间,关键代码如下所示:

public Session createSession(String sessionId) {	// 限制 session 数量,默认不做限制,maxActiveSessions = -1	if ((maxActiveSessions >= 0) &&	            (getActiveSessions() >= maxActiveSessions)) {		rejectedSessions++;		throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"), maxActiveSessions);	}	// 创建 StandardSession 实例,子类可以重写该方法	Session session = createEmptySession();	// 设置属性,包括创建时间,最大失效时间	session.setNew(true);	session.setValid(true);	session.setCreationTime(System.currentTimeMillis());	// 设置最大不活跃时间(单位s),如果超过这个时间,仍然没有请求的话该Session将会失效	session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);	String id = sessionId;	if (id == null) {		id = generateSessionId();	}	session.setId(id);	sessionCounter++;	// 这个地方不是线程安全的,可能当时开发人员认为计数器不要求那么准确	// 将创建时间添加到LinkedList中,并且把最先添加的时间移除,主要还是方便清理过期session	SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);	synchronized (sessionCreationTiming) {		sessionCreationTiming.add(timing);		sessionCreationTiming.poll();	}	return (session);}复制代码

在 tomcat 中是可以限制 session 数量的,如果需要限制,请指定 Manager 的 maxActiveSessions 参数,默认不做限制,不建议进行设置,但是如果存在恶意攻击,每次请求不携带 Cookie 就有可能会频繁创建 Session,导致 Session 对象爆满最终出现 OOM。另外 sessionId 采用随机算法生成,并且每次生成都会判断当前是否已经存在该 id,从而避免 sessionId 重复。而 StandardManager 是使用 ConcurrentHashMap 存储 session 对象的,sessionId 作为 key,org.apache.catalina.Session 作为 value。此外,值得注意的是 StandardManager 创建的是 tomcat 的 org.apache.catalina.session.StandardSession,同时他也实现了 servlet 的 HttpSession,但是为了安全起见,tomcat 并不会把这个 StandardSession 直接交给应用程序,因此需要调用 org.apache.catalina.Session#getSession() 获取 HttpSession。

我们再来看看 StandardSession 的内部结构

  • attributes:使用 ConcurrentHashMap 解决多线程读写的并发问题
  • creationTime:Session 的创建时间
  • expiring:用于标识 Session 是否过期
  • expiring:用于标识 Session 是否过期
  • lastAccessedTime:上一次访问的时间,用于计算 Session 的过期时间
  • maxInactiveInterval:Session 的最大存活时间,如果超过这个时间没有请求,Session 就会被清理、
  • listeners:这是 tomcat 的 SessionListener,并不是 servlet 的 HttpSessionListener
  • facade:HttpSession 的外观模式,应用程序拿到的是该对象
public class StandardSession implements HttpSession, Session, Serializable {    protected ConcurrentMap
attributes = new ConcurrentHashMap<>(); protected long creationTime = 0L; protected transient volatile boolean expiring = false; protected transient StandardSessionFacade facade = null; protected String id = null; protected volatile long lastAccessedTime = creationTime; protected transient ArrayList
listeners = new ArrayList<>(); protected transient Manager manager = null; protected volatile int maxInactiveInterval = -1; protected volatile boolean isNew = false; protected volatile boolean isValid = false; protected transient Map
notes = new Hashtable<>(); protected transient Principal principal = null;}复制代码

Session清理

Background 线程

前面我们分析了 Session 的创建过程,而 Session 会话是有时效性的,下面我们来看下 tomcat 是如何进行失效检查的。在分析之前,我们先回顾下 Container 容器的 Background 线程。

tomcat 所有容器组件,都是继承至 ContainerBase 的,包括 StandardEngine、StandardHost、StandardContext、StandardWrapper,而 ContainerBase 在启动的时候,如果 backgroundProcessorDelay 参数大于 0 则会开启 ContainerBackgroundProcessor 后台线程,调用自己以及子容器的 backgroundProcess 进行一些后台逻辑的处理,和 Lifecycle 一样,这个动作是具有传递性的,也就是说子容器还会把这个动作传递给自己的子容器,如下图所示,其中父容器会遍历所有的子容器并调用其 backgroundProcess 方法,而 StandardContext 重写了该方法,它会调用 StandardManager#backgroundProcess() 进而完成 Session 的清理工作。看到这里,不得不感慨 tomcat 的责任

关键代码如下所示:

ContainerBase.java(省略了异常处理代码)protected synchronized void startInternal() throws LifecycleException {    // other code......    // 开启ContainerBackgroundProcessor线程用于处理子容器,默认情况下backgroundProcessorDelay=-1,不会启用该线程    threadStart();}protected class ContainerBackgroundProcessor implements Runnable {    public void run() {        // threadDone 是 volatile 变量,由外面的容器控制        while (!threadDone) {            try {                Thread.sleep(backgroundProcessorDelay * 1000L);            } catch (InterruptedException e) {                // Ignore            }            if (!threadDone) {                processChildren(ContainerBase.this);            }        }    }    protected void processChildren(Container container) {        container.backgroundProcess();        Container[] children = container.findChildren();        for (int i = 0; i < children.length; i++) {            // 如果子容器的 backgroundProcessorDelay 参数小于0,则递归处理子容器            // 因为如果该值大于0,说明子容器自己开启了线程处理,因此父容器不需要再做处理            if (children[i].getBackgroundProcessorDelay() <= 0) {                processChildren(children[i]);            }        }    }}复制代码

Session 检查

backgroundProcessorDelay 参数默认值为 -1,单位为秒,即默认不启用后台线程,而 tomcat 的 Container 容器需要开启线程处理一些后台任务,比如监听 jsp 变更、tomcat 配置变动、Session 过期等等,因此 StandardEngine 在构造方法中便将 backgroundProcessorDelay 参数设为 10(当然可以在 server.xml 中指定该参数),即每隔 10s 执行一次。那么这个线程怎么控制生命周期呢?我们注意到 ContainerBase 有个 threadDone 变量,用 volatile 修饰,如果调用 Container 容器的 stop 方法该值便会赋值为 false,那么该后台线程也会退出循环,从而结束生命周期。另外,有个地方需要注意下,父容器在处理子容器的后台任务时,需要判断子容器的 backgroundProcessorDelay 值,只有当其小于等于 0 才进行处理,因为如果该值大于0,子容器自己会开启线程自行处理,这时候父容器就不需要再做处理了

前面分析了容器的后台线程是如何调度的,下面我们重点来看看 webapp 这一层,以及 StandardManager 是如何清理过期会话的。StandardContext 重写了 backgroundProcess 方法,除了对子容器进行处理之外,还会对一些缓存信息进行清理,关键代码如下所示:

StandardContext.java@Overridepublic void backgroundProcess() {    if (!getState().isAvailable())        return;    // 热加载 class,或者 jsp    Loader loader = getLoader();    if (loader != null) {        loader.backgroundProcess();    }    // 清理过期Session    Manager manager = getManager();    if (manager != null) {        manager.backgroundProcess();    }    // 清理资源文件的缓存    WebResourceRoot resources = getResources();    if (resources != null) {        resources.backgroundProcess();    }    // 清理对象或class信息缓存    InstanceManager instanceManager = getInstanceManager();    if (instanceManager instanceof DefaultInstanceManager) {        ((DefaultInstanceManager)instanceManager).backgroundProcess();    }    // 调用子容器的 backgroundProcess 任务    super.backgroundProcess();}复制代码

StandardContext 重写了 backgroundProcess 方法,在调用子容器的后台任务之前,还会调用 Loader、Manager、WebResourceRoot、InstanceManager 的后台任务,这里我们只关心 Manager 的后台任务。弄清楚了 StandardManager 的来龙去脉之后,我们接下来分析下具体的逻辑。

StandardManager 继承至 ManagerBase,它实现了主要的逻辑,关于 Session 清理的代码如下所示。backgroundProcess 默认是每隔10s调用一次,但是在 ManagerBase 做了取模处理,默认情况下是 60s 进行一次 Session 清理。tomcat 对 Session 的清理并没有引入时间轮,因为对 Session 的时效性要求没有那么精确,而且除了通知 SessionListener。

ManagerBase.javapublic void backgroundProcess() {    // processExpiresFrequency 默认值为 6,而backgroundProcess默认每隔10s调用一次,也就是说除了任务执行的耗时,每隔 60s 执行一次    count = (count + 1) % processExpiresFrequency;    if (count == 0) // 默认每隔 60s 执行一次 Session 清理        processExpires();}/** * 单线程处理,不存在线程安全问题 */public void processExpires() {    long timeNow = System.currentTimeMillis();    Session sessions[] = findSessions();    // 获取所有的 Session    int expireHere = 0 ;    for (int i = 0; i < sessions.length; i++) {        // Session 的过期是在 isValid() 里面处理的        if (sessions[i]!=null && !sessions[i].isValid()) {            expireHere++;        }    }    long timeEnd = System.currentTimeMillis();    // 记录下处理时间    processingTime += ( timeEnd - timeNow );}复制代码

清理过期 Session

在上面的代码,我们并没有看到太多的过期处理,只是调用了 sessions[i].isValid(),原来清理动作都在这个方法里面处理的,相当的隐晦。在 StandardSession#isValid() 方法中,如果 now - thisAccessedTime >= maxInactiveInterval则判定当前 Session 过期了,而这个 thisAccessedTime 参数在每次访问都会进行更新

public boolean isValid() {    // other code......    // 如果指定了最大不活跃时间,才会进行清理,这个时间是 Context.getSessionTimeout(),默认是30分钟    if (maxInactiveInterval > 0) {        int timeIdle = (int) (getIdleTimeInternal() / 1000L);        if (timeIdle >= maxInactiveInterval) {            expire(true);        }    }    return this.isValid;}复制代码

而 expire 方法处理的逻辑较繁锁,下面我用伪代码简单地描述下核心的逻辑,由于这个步骤可能会有多线程进行操作,因此使用 synchronized 对当前 Session 对象加锁,还做了双重校验,避免重复处理过期 Session。它还会向 Container 容器发出事件通知,还会调用 HttpSessionListener 进行事件通知,这个也就是我们 web 应用开发的 HttpSessionListener 了。由于 Manager 中维护了 Session 对象,因此还要将其从 Manager 移除。Session 最重要的功能就是存储数据了,可能存在强引用,而导致 Session 无法被 gc 回收,因此还要移除内部的 key/value 数据。由此可见,tomcat 编码的严谨性了,稍有不慎将可能出现并发问题,以及出现内存泄露

public void expire(boolean notify) {    1、校验 isValid 值,如果为 false 直接返回,说明已经被销毁了    synchronized (this) {   // 加锁        2、双重校验 isValid 值,避免并发问题        Context context = manager.getContext();        if (notify) {               Object listeners[] = context.getApplicationLifecycleListeners();            HttpSessionEvent event = new HttpSessionEvent(getSession());            for (int i = 0; i < listeners.length; i++) {            3、判断是否为 HttpSessionListener,不是则继续循环            4、向容器发出Destory事件,并调用 HttpSessionListener.sessionDestroyed() 进行通知            context.fireContainerEvent("beforeSessionDestroyed", listener);            listener.sessionDestroyed(event);            context.fireContainerEvent("afterSessionDestroyed", listener);        }        5、从 manager 中移除该  session        6、向 tomcat 的 SessionListener 发出事件通知,非 HttpSessionListener        7、清除内部的 key/value,避免因为强引用而导致无法回收 Session 对象    }}复制代码

由前面的分析可知,tomcat 会根据时间戳清理过期 Session,那么 tomcat 又是如何更新这个时间戳呢?我们在 StandardSession#thisAccessedTime 的属性上面打个断点,看下调用栈。原来 tomcat 在处理完请求之后,会对 Request 对象进行回收,并且会对 Session 信息进行清理,而这个时候会更新 thisAccessedTime、lastAccessedTime 时间戳。此外,我们通过调用 request.getSession() 这个 API 时,在返回 Session 时会调用 Session#access() 方法,也会更新 thisAccessedTime 时间戳。这样一来,每次请求都会更新时间戳,可以保证 Session 的鲜活时间

方法调用栈如下所示:

关键代码如下所示:

org.apache.catalina.connector.Request.javaprotected void recycleSessionInfo() {    if (session != null) {          session.endAccess();    // 更新时间戳    }    // 回收 Request 对象的内部信息    session = null;    requestedSessionCookie = false;    requestedSessionId = null;    requestedSessionURL = false;    requestedSessionSSL = false;}复制代码

org.apache.catalina.session.StandardSession.java

public void endAccess() {    isNew = false;    if (LAST_ACCESS_AT_START) {     // 可以通过系统参数改变该值,默认为false        this.lastAccessedTime = this.thisAccessedTime;        this.thisAccessedTime = System.currentTimeMillis();    } else {        this.thisAccessedTime = System.currentTimeMillis();        this.lastAccessedTime = this.thisAccessedTime;    }}public void access() {    this.thisAccessedTime = System.currentTimeMillis();}复制代码

HttpSessionListener

创建通知

前面我们分析了 Session 的创建过程,但是在整个创建流程中,似乎没有看到关于 HttpSessionListener 的创建通知。原来,在给 Session 设置 id 的时候会进行事件通知,和 Session 的销毁一样,也是非常的隐晦,个人感觉这一块设计得不是很合理。

创建通知这块的逻辑很简单,首先创建 HttpSessionEvent 对象,然后遍历 Context 内部的 LifecycleListener,并且判断是否为 HttpSessionListener 实例,如果是的话则调用 HttpSessionListener#sessionCreated() 方法进行事件通知。

public void setId(String id, boolean notify) {    // 省略部分代码    if (notify) {        tellNew();    }}public void tellNew() {    // 通知 org.apache.catalina.SessionListener    fireSessionEvent(Session.SESSION_CREATED_EVENT, null);    // 获取 Context 内部的 LifecycleListener,并判断是否为 HttpSessionListener    Context context = manager.getContext();    Object listeners[] = context.getApplicationLifecycleListeners();    if (listeners != null && listeners.length > 0) {        HttpSessionEvent event = new HttpSessionEvent(getSession());        for (int i = 0; i < listeners.length; i++) {            if (!(listeners[i] instanceof HttpSessionListener))                continue;            HttpSessionListener listener = (HttpSessionListener) listeners[i];            context.fireContainerEvent("beforeSessionCreated", listener);   // 通知 Container 容器            listener.sessionCreated(event);            context.fireContainerEvent("afterSessionCreated", listener);        }    }}复制代码

销毁通知

我们在前面分析清理过期 Session时大致分析了 Session 销毁时会触发 HttpSessionListener 的销毁通知,这里不再重复了。

分享免费学习资料


针对于Java程序员,我这边准备免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

为什么某些人会一直比你优秀,是因为他本身就很优秀还一直在持续努力变得更优秀,而你是不是还在满足于现状内心在窃喜!希望读到这的您能点个小赞和关注下我,以后还会更新技术干货,谢谢您的支持!

资料领取方式:加入Java技术交流群963944895,,私信管理员即可免费领取

转载于:https://juejin.im/post/5cc59ff85188252d740bf6b3

你可能感兴趣的文章
Snort2.9.2.3 Installation on CentOS 6.2
查看>>
我的友情链接
查看>>
给软件工程师的自学建议
查看>>
Linux下SVN的备份方式
查看>>
hadoop 3.0.0 alpha1 分布式搭建
查看>>
刘宇凡:从吃饭中的道理领悟SEO
查看>>
1.1办公软件概述
查看>>
python中http的一些编码转换
查看>>
5.继续看W3C的 bootstrap
查看>>
PHP中include和require
查看>>
第一次标题
查看>>
H3C ospf router id 重复真的不能建立邻居吗?
查看>>
【symfoware OPEN】数据库基本操作
查看>>
iphone:截屏
查看>>
Mac 下 VirtualBox ubuntu 共享空间
查看>>
CENTOS6.3利用Keepalived构建双主MySQL+双机热备
查看>>
常用算法收集
查看>>
listview的简单使用(Baseadapter)
查看>>
Oracle导入程序Imp的使用详解
查看>>
******技术宝典Web实战篇(第2版)英文版PDF格式
查看>>