Jetty9源码剖析 - Handler组件 - SessionHandler

转载自ph0ly:http://www.ph0ly.com

一、SessionHandler的概念

作为一个现代的Web应用,Web容器必须能支持Session管理,Jetty自然也实现了自己的一套Session管理,也就是接下来要介绍的SessionHandler。SessionHandler集成了各个组件,实现了Session完整的生命周期管理

二、应用场景

SessionHandler用来实现Session的生命周期管理,当应用需要用到Session会话时,需要集成该Handler,通常作为一个完整的Web应用,我们会选择WebAppContext来作为应用的Handler,它会集成SessionHandler

三、继承体系

SessionHandler组件继承体系

可以看到SessionHandler其实是我们前面介绍的ScopedHandler,是一个容器,并具有容器的生命周期,也具有Handler的特性,同时也可以成为一个链节点

四、总体交互逻辑

总体架构及交互逻辑

从上面可以看出SessionHandler主要依赖5大组件实现,分别为SessionIdManager(默认为DefaultSessionIdManager)、HouseKeeper、SessionCache(默认为DefaultSessionCache)、SessionDataStore(默认为NullSessionDataStore)、Session(实现了HttpSession)。交互关系如图,SessionHandler.doStart会启动SessionIdManager、SessionCache,而SessionIdManager会启动HouseKeeper,SessionCache会启动SessionDataStore,Session作为一个模型对象不具有生命周期,因此也就没有doStart的说法

Session的生命周期:

创建:由业务层代码触发(HttpServletRequest.getSession()),这时会调用SessionHandler.newHttpSession,这里会调用SessionIdManager.newSessionId生成sessionId,并将sessionId和Session放到SessionCache做映射

更新:在doScope发现客户端传来的sessionId在服务端存在,则会调用Session.access更新Session的有效期

 

失效:每个Session自带一个定时器,当Session超过之前预设的定时时间,会触发判断是否过期,如果过期就会将自己加入到SessionHandler._candidateSessionIdsForExpiry集合,否则重新设置下一次定时器

清理:HouseKeeper定时执行scavenge方法,会查找Server内的所有SessionHandler,并执行SessionHandler.scavenge,SessionHandler.scavenge会扫描SessionHandler._candidateSessionIdsForExpiry集合的sessionId是否过期,如果是过期了,就会让Server内的所有SessionHandler都移除该sessionId的Session

五、源码剖析

SessionHandler.doStart方法实现

doStart:

获取当前应用的ServletContext,后续需要从ServletContext拿一些配置信息

获取当前线程的Context ClassLoader,这里获取当前线程类加载器,以便后续恢复

初始化SessionCache

如果设置了SessionCacheFactory,则使用SessionCacheFactory获取SessionCache,否则使用默认的DefaultSessionCache。在我们的应用中通常情况下都是DefaultSessionCache

如果设置了SessionDataStoreFactory,则从该工厂获取SessionDataStore,否则将会被设置为NullSessionDataStore,在我们的应用中通常情况下都是使用的NullSessionDataStore,Session并不会被持久化,如果重启就会丢失(所以通常我们需要使用分布式缓存来集中管理Session)

初始化SessionIdManager

如果SessionIdManager是空,就会自动创建一个,并将Server的sessionIdManager赋值,同时放到Server的容器进行托管,然后显式启动(这里显式启动其实没有太大意义,setIdManager其实就加到Server容器中了,已经将SessionIdManager启动完成,所以个人觉得这里比较鸡肋一点,详见github:https://github.com/eclipse/jetty.project/issues/2660)。特别注意创建之前先获取Server本身的类加载器,在sessionIdManager启动完成后,还原了线程上下文类加载器为之前,大家可以思考下为什么

初始化Scheduler

这里先查看Server容器里面是否包含一个Scheduler定时线程池,如果存在就用已有的,否则不存在则创建一个ScheduledExecutorScheduler(Jetty自行定制的),并启动。注意这里的定时线程池将会用于后续的Session定时过期检测

初始化Session相关配置,例如Session Id在cookie或url中的追踪名称

创建SessionContext,并设置为SessionCache变量

调用父类ScopedHandler.doStart,构建链,启动自己及容器中的对象

SessionHandler.doScope方法实现

doScope:

获取Request上绑定的SessionHandler,注意这里主要是在Server容器中存在多个SessionHandler的场景的情况下,一个Request可能会由多个SessionHandler处理,所以会先把老的SessionHandler记录下来,后面在还原回老的SessionHandler

获取Request上旧的Session,这里也是起到记录,后续还原数据

如果old_session_manager不是当前对象,就把当前Request的sessionHandler设为自己,同时Session置为空,再从cookie或者url提取Session Id,并设置该id对应的Session到Request

existingSession从Request拿到Session,如果有就激活一次Session有效期,并且在access里面判断是否需要刷新cookie,如果access后发现cookie需要刷新,则将cookie放回Response

如果当前存在下一节点,则执行下一节点的doScope

如果不存在下一节点,但顶级节点存在,则执行顶级节点的doHandle

否则执行自己的doHandle方法(这里就是前面说的ScopedHandler的执行逻辑了)

调用老Session的complete方法,完成Session,Session请求引用计数减少

最后还原Request数据(在Jetty里面Request和Response在同一条连接上是复用的)

SessionHandler.checkRequestedSessionId方法实现

checkRequestedSessionId:

查看Request里面是否包含sessionId,如果存在sessionId则尝试获取这个sessionId对应的Session,如果存在并且有效就设置为当前Request的Session

如果不包含sessionId,并且这个请求不是直接调用(DispatcherType.REQUEST),就不做处理

 

如果当前使用了cookie作为存储sessionId的追踪方式,则遍历当前请求的所有cookie,查找是否包含配置的Session名称,并提取出sessionId,同时在SessionCache里面拿到Session

如果cookie都拿不到,就从url里面拿,默认格式为 ;jsessionid=sessionId;(#或?或/),提取到就从SessionCache拿Session

根据是从cookie还是url将session获取方式设置到Request

如果Session有效,则设置到Request

SessionHandler.doHandle方法实现

doHandle:

调用父类ScopedHandler.nextHandle执行下一节点处理

SessionHandler.scavenge方法实现

scavenge:

扫描_candidateSessionIdsForExpiry集合,调用SessionCache.checkExpiration拿到真实的需要过期的sessionId,调用DefaultSessionIdManager.expireAll方法,该方法会通知Server容器内所有的SessionHandler将该sessionId的Session过期

SessionHandler.sessionInactivityTimerExpired方法实现

sessionInactivityTimerExpired:

再一次校验Session是否已经过期,如果过期则将其加到_candidateSessionIdsForExpiry待过期集合,否则校验该Session是否需要被驱逐出SessionCache(驱逐的意思是到了定时器周期,直接删掉,同时是否需要持久化)。注意这个方法是每个Session的定时任务调用过来的,这样就能和上述的scavenge扫描方法配合完成Session过期的处理了。另外Session过期是一个批量清理的机制

SessionHandler.newHttpSession方法实现

newHttpSession:

利用sessionIdManager生成一个session id(具体id的生成方式将会在SessionHandler组件篇分析)

利用sessionCache创建一个Session(Servlet规范中的HttpSession)

设置session的扩展id,这里是session id + ‘.’ + workName(当前机器对应的别名)

将Session对象里面实际的sessionData设置为节点名(workerName)

建立该session的id和Session的映射关系

增加一次session创建数计数

如果是https请求,则记录当前session是一个https的session

通知Session观察者,Session被创建了

返回创建好的Session

该方法其实是当我们的应用去调用HttpServletRequest.getSession()时,发现Session为空会调用该方法默认创建一个

五、总结

SessionHandler作为一个Session总管理者,当一个请求(Request)到来,Server.handle时会触发WebAppContext.handle,这个时候会按照ScopedHandler的执行逻辑,让SessionHandler、ServletHandler分别执行doScope、doHandle,最后完成一个请求的Session准备,到达业务层DispatcherServlet(或其他自定义Servlet),业务代码就可以拿到老的或者创建一个新的Session。Session的更新会在前置SessionHandler完成,而销毁将会是批量清理,并不是实时清理(对于持久化的Session相对会低效)。当然Session的细节还存在一些,这些将会在后续的文章完善。接下来的文章我会带领大家探索ContextHandler以及ServletHandler的Filter链式执行逻辑,请持续关注~