Tomcat 是如何管理Session的方法示例
学了ConcurrentHashMap却不知如何应用?用了Tomcat的Session却不知其是如何实现的,Session是怎么被创建和销毁的?往下看你就知道了。
Session结构
不多废话,直接上图
仔细观察上图,我们可以得出以下结论
- HttpSession是JavaEE标准中操作Session的接口类,因此我们实际上操作的是StandardSessionFacade类
- Session保存数据所使用的数据结构是ConcurrentHashMap,如你在图上看到的我们往Session中保存了一个msg
为什么需要使用ConcurrentHashMap呢?原因是,在处理Http请求并不是只有一个线程会访问这个Session,现代Web应用访问一次页面,通常需要同时执行多次请求,而这些请求可能会在同一时刻内被Web容器中不同线程同时执行,因此如果采用HashMap的话,很容易引发线程安全的问题。
让我们先来看看HttpSession的包装类。
StandardSessionFacade
在此类中我们可以学习到外观模式(Facde)的实际应用。其定义如下所示。
publicclassStandardSessionFacadeimplementsHttpSession
那么此类是如何实现Session的功能呢?观察以下代码不难得出,此类并不是HttpSession的真正实现类,而是将真正的HttpSession实现类进行包装,只暴露HttpSession接口中的方法,也就是设计模式中的外观(Facde)模式。
privatefinalHttpSessionsession; publicStandardSessionFacade(HttpSessionsession){ this.session=session; }
那么我们为什么不直接使用HttpSession的实现类呢?
根据图1,我们可以知道HttpSession的真正实现类是StandardSession,假设在该类内定义了一些本应由Tomcat调用而非由程序调用的方法,那么由于Java的类型系统我们将可以直接操作该类,这将会带来一些不可预见的问题,如以下代码所示。
而如果我们将StandardSession再包装一层,上图代码执行的时候将会发生错误。如下图所示,将会抛出类型转换的异常,从而阻止此处非法的操作。
再进一步,我们由办法绕外观类直接访问StandardSession吗?
事实上是可以的,我们可以通过反射机制来获取StandardSession,但你最好清楚自己在干啥。代码如下所示
@GetMapping("/s") publicStringsessionTest(HttpSessionhttpSession)throwsClassNotFoundException,NoSuchFieldException,IllegalAccessException{ StandardSessionFacadesession=(StandardSessionFacade)httpSession; ClasstargetClass=Class.forName(session.getClass().getName()); //修改可见性 FieldstandardSessionField=targetClass.getDeclaredField("session"); standardSessionField.setAccessible(true); //获取 StandardSessionstandardSession=(StandardSession)standardSessionField.get(session); returnstandardSession.getManager().toString(); }
StandardSession
该类的定义如下
publicclassStandardSessionimplements HttpSession,Session,Serializable
通过其接口我们可以看出此类除了具有JavaEE标准中HttpSession要求实现的功能之外,还有序列化的功能。
在图1中我们已经知道StandardSession是用ConcurrentHashMap来保存的数据,因此接下来我们主要关注StandardSession的序列化以及反序列化的实现,以及监听器的功能。
序列化
还记得上一节我们通过反射机制获取到了StandardSession吗?利用以下代码我们可以直接观察到反序列化出来的StandardSession是咋样的。
@GetMapping("/s") publicvoidsessionTest(HttpSessionhttpSession,HttpServletResponseresponse)throwsClassNotFoundException,NoSuchFieldException,IllegalAccessException,IOException{ StandardSessionFacadesession=(StandardSessionFacade)httpSession; ClasstargetClass=Class.forName(session.getClass().getName()); //修改可见性 FieldstandardSessionField=targetClass.getDeclaredField("session"); standardSessionField.setAccessible(true); //获取 StandardSessionstandardSession=(StandardSession)standardSessionField.get(session); //存点数据以便观察 standardSession.setAttribute("msg","hello,world"); standardSession.setAttribute("user","kesan"); standardSession.setAttribute("password","点赞"); standardSession.setAttribute("tel",10086L); //将序列化的结果直接写到Http的响应中 ObjectOutputStreamobjectOutputStream=newObjectOutputStream(response.getOutputStream()); standardSession.writeObjectData(objectOutputStream); }
如果不出意外,访问此接口浏览器将会执行下载操作,最后得到一个文件
使用WinHex打开分析,如图所示为序列化之后得结果,主要是一大堆分隔符,以及类型信息和值,如图中红色方框标准的信息。
不建议大家去死磕序列化文件是如何组织数据的,因为意义不大
如果你真的有兴趣建议你阅读以下代码org.apache.catalina.session.StandardSession.doWriteObject
监听器
在JavaEE的标准中,我们可以通过配置HttpSessionAttributeListener来监听Session的变化,那么在StandardSession中是如何实现的呢,如果你了解观察者模式,那么想必你已经知道答案了。以setAttribute为例,在调用此方法之后会立即在本线程调用监听器的方法进行处理,这意味着我们不应该在监听器中执行阻塞时间过长的操作。
publicvoidsetAttribute(Stringname,Objectvalue,booleannotify){ //省略无关代码 //获取上文中配置的事件监听器 Objectlisteners[]=context.getApplicationEventListeners(); if(listeners==null){ return; } for(inti=0;iSesssion生命周期
如何保存Session
在了解完Session的结构之后,我们有必要明确StandardSession是在何时被创建的,以及需要注意的点。
首先我们来看看StandardSession的构造函数,其代码如下所示。
publicStandardSession(Managermanager){ //调用Object类的构造方法,默认已经调用了 //此处再声明一次,不知其用意,或许之前此类有父类? super(); this.manager=manager; //是否开启访问计数 if(ACTIVITY_CHECK){ accessCount=newAtomicInteger(); } }在创建StandardSession的时候都必须传入Manager对象以便与此StandardSession关联,因此我们可以将目光转移到Manager,而Manager与其子类之间的关系如下图所示。
我们将目光转移到ManagerBase中可以发现以下代码。 protectedMapsessions=newConcurrentHashMap<>(); Session是Tomcat自定义的接口,StandardSession实现了HttpSession以及Session接口,此接口功能更加丰富,但并不向程序员提供。
查找此属性可以发现,与Session相关的操作都是通过操作sessions来实现的,因此我们可以明确保存Session的数据结构是ConcurrentHashMap。
如何创建Session 那么Session到底是如何创建的呢?我找到了以下方法ManagerBase.creaeSession,总结其流程如下。
- 检查session数是否超过限制,如果有就抛出异常
- 创建StandardSession对象
- 设置session各种必须的属性(合法性,最大超时时间,sessionId)
- 生成SessionId,Tomcat支持不同的SessionId算法,本人调试过程其所使用的SessionId生成算法是LazySessionIdGenerator(此算法与其他算法不同之处就在于并不会在一开始就加载随机数数组,而是在用到的时候才加载,此处的随机数组并不是普通的随机数组而是SecureRandom,相关信息可以阅读大佬的文章)
- 增加session的计数,由于Tomcat的策略是只计算100个session的创建速率,因此sessionCreationTiming是固定大小为100的链表(一开始为100个值为null的元素),因此在将新的数据添加到链表中时必须要将旧的数据移除链表以保证其固定的大小。session创建速率计算公式如下
(1000*60*counter)/(int)(now-oldest)
其中
- now为获取统计数据时的时间System.currentTimeMillis()
- oldest为队列中最早创建session的时间
- counter为队列中值不为null的元素的数量
- 由于计算的是每分钟的速率因此在此处必须将1000乘以60(一分钟内有60000毫秒)
publicSessioncreateSession(StringsessionId){ //检查Session是否超过限制,如果是则抛出异常 if((maxActiveSessions>=0)&& (getActiveSessions()>=maxActiveSessions)){ rejectedSessions++; thrownewTooManyActiveSessionsException( sm.getString("managerBase.createSession.ise"), maxActiveSessions); } //该方法会创建StandardSession对象 Sessionsession=createEmptySession(); //初始化Session中必要的属性 session.setNew(true); //session是否可用 session.setValid(true); //创建时间 session.setCreationTime(System.currentTimeMillis()); //设置session最大超时时间 session.setMaxInactiveInterval(getContext().getSessionTimeout()*60); Stringid=sessionId; if(id==null){ id=generateSessionId(); } session.setId(id); sessionCounter++; //记录创建session的时间,用于统计数据session的创建速率 //类似的还有ExpireRate即Session的过期速率 //由于可能会有其他线程对sessionCreationTiming操作因此需要加锁 SessionTimingtiming=newSessionTiming(session.getCreationTime(),0); synchronized(sessionCreationTiming){ //sessionCreationTiming是LinkedList //因此poll会移除链表头的数据,也就是最旧的数据 sessionCreationTiming.add(timing); sessionCreationTiming.poll(); } returnsession; }Session的销毁
要销毁Session,必然要将Session从ConcurrentHashMap中移除,顺藤摸瓜我们可以发现其移除session的代码如下所示。
@Override publicvoidremove(Sessionsession,booleanupdate){ //检查是否需要将统计过期的session的信息 if(update){ longtimeNow=System.currentTimeMillis(); inttimeAlive= (int)(timeNow-session.getCreationTimeInternal())/1000; updateSessionMaxAliveTime(timeAlive); expiredSessions.incrementAndGet(); SessionTimingtiming=newSessionTiming(timeNow,timeAlive); synchronized(sessionExpirationTiming){ sessionExpirationTiming.add(timing); sessionExpirationTiming.poll(); } } //将session从Map中移除 if(session.getIdInternal()!=null){ sessions.remove(session.getIdInternal()); } }被销毁的时机
主动销毁
我们可以通过调用HttpSession.invalidate()方法来执行session销毁操作。此方法最终调用的是StandardSession.invalidate()方法,其代码如下,可以看出使session销毁的关键方法是StandardSession.expire()
publicvoidinvalidate(){ if(!isValidInternal()) thrownewIllegalStateException (sm.getString("standardSession.invalidate.ise")); //Causethissessiontoexpire expire(); }expire方法的代码如下
@Override publicvoidexpire(){ expire(true); } publicvoidexpire(booleannotify){ //省略代码 //将session从ConcurrentHashMap中移除 manager.remove(this,true); //被省略的代码主要是将session被销毁的消息通知 //到各个监听器上 }超时销毁
除了主动销毁之外,我们可以为session设置一个过期时间,当时间到达之后session会被后台线程主动销毁。我们可以为session设置一个比较短的过期时间,然后通过JConsole来追踪其调用栈,其是哪个对象哪个线程执行了销毁操作。
如下图所示,我们为session设置了一个30秒的超时时间。
然后我们在ManagerBase.remove 方法上打上断点,等待30秒之后,如下图所示
Tomcat会开启一个后台线程,来定期执行子组件的backgroundProcess方法(前提是子组件被Tomcat管理且实现了Manager接口)
@Override publicvoidbackgroundProcess(){ count=(count+1)%processExpiresFrequency; if(count==0) processExpires(); } publicvoidprocessExpires(){ longtimeNow=System.currentTimeMillis(); Sessionsessions[]=findSessions(); intexpireHere=0; if(log.isDebugEnabled()) log.debug("Startexpiresessions"+getName()+"at"+timeNow+"sessioncount"+sessions.length); //从JConsole的图中可以看出isValid可能导致expire方法被调用 for(inti=0;i我们可以来看看接口中Manager.backgroundProcess中注释,简略翻译一下就是backgroundProcess会被容器定期的执行,可以用来执行session清理任务等。
/** *Thismethodwillbeinvokedbythecontext/containeronaperiodic *basisandallowsthemanagertoimplement *amethodthatexecutesperiodictasks,suchasexpiringsessionsetc. */ publicvoidbackgroundProcess();总结
Session的数据结构如下图所示,简单来说就是用ConcurrentHashMap来保存Session,而Session则用ConcurrentHashMap来保存键值对,其结构如下图所示。.jpg
这意味着,不要拼命的往Session里面添加离散的数据,把离散的数据封装成一个对象性能会更加好如下所示
//bad httpSession.setAttribute("user","kesan"); httpSession.setAttribute("nickname","点赞"); httpSession.setAttribute("sex","男"); ....//good Userkesan=userDao.getUser() httpSession.setAttribute("user",kesan);如果你为Session配置了监听器,那么对Session执行任何变更都将直接在当前线程执行监听器的方法,因此最好不要在监听器中执行可能会发生阻塞的方法。
Tomcat会开启一个后台线程来定期执行ManagerBase.backgroundProcess方法用来检测过期的Session并将其销毁。
思想迁移
对象生成速率算法此算法设计比较有趣,并且也可以应用到其他项目中,因此做如下总结。
首先生成一个固定大小的链表(比如说100),然后以null元素填充。当创建新的对象时,将创建时间加入链表末尾中(当然是封装后的对象),然后将链表头节点移除,此时被移除的对象要么是null节点要么是最早加入链表的节点当要计算对象生成速率时,统计链表中不为null的元素的数量除以当前的时间与最早创建对象的时间的差,便可以得出其速率。(注意时间单位的转换)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。