在 web 开发中,咱们常常会用到 Session 来保存会话信息,包括用户信息、权限信息,等等。在这篇文章中,咱们将分析 tomcat 容器是如何建立 session、销毁 session,又是如何对 HttpSessionListener 进行事件通知java
tomcat session 组件图以下所示,其中Context
对应一个webapp
应用,每一个webapp
有多个HttpSessionListener
, 而且每一个应用的session
是独立管理的,而session
的建立、销毁由Manager
组件完成,它内部维护了 N 个Session
实例对象。在前面的文章中,咱们分析了Context
组件,它的默认实现是StandardContext
,它与Manager
是一对一的关系,Manager
建立、销毁会话时,须要借助StandardContext
获取 HttpSessionListener
列表并进行事件通知,而StandardContext
的后台线程会对Manager
进行过时` Session 的清理工做程序员
org.apache.catalina.Manager
接口的主要方法以下所示,它提供了 Contex
t、org.apache.catalina.SessionIdGenerator
的getter/setter
接口,以及建立、添加、移除、查找、遍历Session
的 API 接口,此外还提供了Session
持久化的接口(load/unload) 用于加载/卸载会话信息,固然持久化要看不一样的实现类web
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 官方文档面试
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 实例。算法
咱们以 HttpServletRequest#getSession() 做为切入点,对 Session 的建立过程进行分析sql
public class SessionExample extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
HttpSession session = request.getSession();
// other code......
}
}
复制代码
整个流程图以下图所示:apache
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 实现了主要的逻辑。tomcat
整个建立 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 的内部结构
public class StandardSession implements HttpSession, Session, Serializable {
protected ConcurrentMap<String, Object> 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<SessionListener> 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<String, Object> notes = new Hashtable<>();
protected transient Principal principal = null;
}
复制代码
前面咱们分析了 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]);
}
}
}
}
复制代码
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
@Override
public 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.java
public 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 );
}
复制代码
在上面的代码,咱们并无看到太多的过时处理,只是调用了 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) {
一、校验 isValid 值,若是为 false 直接返回,说明已经被销毁了
synchronized (this) { // 加锁
二、双重校验 isValid 值,避免并发问题
Context context = manager.getContext();
if (notify) {
Object listeners[] = context.getApplicationLifecycleListeners();
HttpSessionEvent event = new HttpSessionEvent(getSession());
for (int i = 0; i < listeners.length; i++) {
三、判断是否为 HttpSessionListener,不是则继续循环
四、向容器发出Destory事件,并调用 HttpSessionListener.sessionDestroyed() 进行通知
context.fireContainerEvent("beforeSessionDestroyed", listener);
listener.sessionDestroyed(event);
context.fireContainerEvent("afterSessionDestroyed", listener);
}
五、从 manager 中移除该 session
六、向 tomcat 的 SessionListener 发出事件通知,非 HttpSessionListener
七、清除内部的 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.java
protected 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();
}
复制代码
前面咱们分析了 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
,点击加入群聊,私信管理员便可免费领取