转载自ph0ly:http://www.ph0ly.com
一、SessionHandler的概念
作为一个现代的Web应用,Web容器必须能支持Session管理,Jetty自然也实现了自己的一套Session管理,也就是接下来要介绍的SessionHandler。SessionHandler集成了各个组件,实现了Session完整的生命周期管理
二、应用场景
SessionHandler用来实现Session的生命周期管理,当应用需要用到Session会话时,需要集成该Handler,通常作为一个完整的Web应用,我们会选择WebAppContext来作为应用的Handler,它会集成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
五、源码剖析
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,构建链,启动自己及容器中的对象
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在同一条连接上是复用的)
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
doHandle:
调用父类ScopedHandler.nextHandle执行下一节点处理
scavenge:
扫描_candidateSessionIdsForExpiry集合,调用SessionCache.checkExpiration拿到真实的需要过期的sessionId,调用DefaultSessionIdManager.expireAll方法,该方法会通知Server容器内所有的SessionHandler将该sessionId的Session过期
sessionInactivityTimerExpired:
再一次校验Session是否已经过期,如果过期则将其加到_candidateSessionIdsForExpiry待过期集合,否则校验该Session是否需要被驱逐出SessionCache(驱逐的意思是到了定时器周期,直接删掉,同时是否需要持久化)。注意这个方法是每个Session的定时任务调用过来的,这样就能和上述的scavenge扫描方法配合完成Session过期的处理了。另外Session过期是一个批量清理的机制
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链式执行逻辑,请持续关注~