Tomcat 是如何管理Session的方法示例
學(xué)了 ConcurrentHashMap 卻不知如何應(yīng)用?用了Tomcat的Session卻不知其是如何實(shí)現(xiàn)的,Session是怎么被創(chuàng)建和銷(xiāo)毀的?往下看你就知道了。
Session結(jié)構(gòu)
不多廢話(huà),直接上圖

仔細(xì)觀(guān)察上圖,我們可以得出以下結(jié)論
HttpSession是JavaEE標(biāo)準(zhǔn)中操作Session的接口類(lèi),因此我們實(shí)際上操作的是StandardSessionFacade類(lèi)Session保存數(shù)據(jù)所使用的數(shù)據(jù)結(jié)構(gòu)是ConcurrentHashMap, 如你在圖上看到的我們往Session中保存了一個(gè)msg
為什么需要使用 ConcurrentHashMap 呢?原因是,在處理Http請(qǐng)求并不是只有一個(gè)線(xiàn)程會(huì)訪(fǎng)問(wèn)這個(gè)Session, 現(xiàn)代Web應(yīng)用訪(fǎng)問(wèn)一次頁(yè)面,通常需要同時(shí)執(zhí)行多次請(qǐng)求, 而這些請(qǐng)求可能會(huì)在同一時(shí)刻內(nèi)被Web容器中不同線(xiàn)程同時(shí)執(zhí)行,因此如果采用 HashMap 的話(huà),很容易引發(fā)線(xiàn)程安全的問(wèn)題。
讓我們先來(lái)看看HttpSession的包裝類(lèi)。
StandardSessionFacade
在此類(lèi)中我們可以學(xué)習(xí)到外觀(guān)模式(Facde)的實(shí)際應(yīng)用。其定義如下所示。
public class StandardSessionFacade implements HttpSession
那么此類(lèi)是如何實(shí)現(xiàn)Session的功能呢?觀(guān)察以下代碼不難得出,此類(lèi)并不是HttpSession的真正實(shí)現(xiàn)類(lèi),而是將真正的HttpSession實(shí)現(xiàn)類(lèi)進(jìn)行包裝,只暴露HttpSession接口中的方法,也就是設(shè)計(jì)模式中的外觀(guān)(Facde)模式。
private final HttpSession session;
public StandardSessionFacade(HttpSession session) {
this.session = session;
}
那么我們?yōu)槭裁床恢苯邮褂肏ttpSession的實(shí)現(xiàn)類(lèi)呢?
根據(jù)圖1,我們可以知道HttpSession的真正實(shí)現(xiàn)類(lèi)是 StandardSession ,假設(shè)在該類(lèi)內(nèi)定義了一些本應(yīng)由Tomcat調(diào)用而非由程序調(diào)用的方法,那么由于Java的類(lèi)型系統(tǒng)我們將可以直接操作該類(lèi),這將會(huì)帶來(lái)一些不可預(yù)見(jiàn)的問(wèn)題,如以下代碼所示。

而如果我們將 StandardSession 再包裝一層,上圖代碼執(zhí)行的時(shí)候?qū)?huì)發(fā)生錯(cuò)誤。如下圖所示,將會(huì)拋出類(lèi)型轉(zhuǎn)換的異常,從而阻止此處非法的操作。

再進(jìn)一步,我們由辦法繞外觀(guān)類(lèi)直接訪(fǎng)問(wèn) StandardSession 嗎?
事實(shí)上是可以的,我們可以通過(guò)反射機(jī)制來(lái)獲取 StandardSession ,但你最好清楚自己在干啥。代碼如下所示
@GetMapping("/s")
public String sessionTest(HttpSession httpSession) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
StandardSessionFacade session = (StandardSessionFacade) httpSession;
Class targetClass = Class.forName(session.getClass().getName());
//修改可見(jiàn)性
Field standardSessionField = targetClass.getDeclaredField("session");
standardSessionField.setAccessible(true);
//獲取
StandardSession standardSession = (StandardSession) standardSessionField.get(session);
return standardSession.getManager().toString();
}
StandardSession
該類(lèi)的定義如下
public class StandardSession implements HttpSession, Session, Serializable
通過(guò)其接口我們可以看出此類(lèi)除了具有JavaEE標(biāo)準(zhǔn)中 HttpSession 要求實(shí)現(xiàn)的功能之外,還有序列化的功能。
在圖1中我們已經(jīng)知道 StandardSession 是用 ConcurrentHashMap 來(lái)保存的數(shù)據(jù),因此接下來(lái)我們主要關(guān)注 StandardSession 的序列化以及反序列化的實(shí)現(xiàn),以及監(jiān)聽(tīng)器的功能。
序列化
還記得上一節(jié)我們通過(guò)反射機(jī)制獲取到了 StandardSession 嗎?利用以下代碼我們可以直接觀(guān)察到反序列化出來(lái)的 StandardSession 是咋樣的。
@GetMapping("/s")
public void sessionTest(HttpSession httpSession, HttpServletResponse response) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, IOException {
StandardSessionFacade session = (StandardSessionFacade) httpSession;
Class targetClass = Class.forName(session.getClass().getName());
//修改可見(jiàn)性
Field standardSessionField = targetClass.getDeclaredField("session");
standardSessionField.setAccessible(true);
//獲取
StandardSession standardSession = (StandardSession) standardSessionField.get(session);
//存點(diǎn)數(shù)據(jù)以便觀(guān)察
standardSession.setAttribute("msg","hello,world");
standardSession.setAttribute("user","kesan");
standardSession.setAttribute("password", "點(diǎn)贊");
standardSession.setAttribute("tel", 10086L);
//將序列化的結(jié)果直接寫(xiě)到Http的響應(yīng)中
ObjectOutputStream objectOutputStream = new ObjectOutputStream(response.getOutputStream());
standardSession.writeObjectData(objectOutputStream);
}
如果不出意外,訪(fǎng)問(wèn)此接口瀏覽器將會(huì)執(zhí)行下載操作,最后得到一個(gè)文件

使用 WinHex 打開(kāi)分析,如圖所示為序列化之后得結(jié)果,主要是一大堆分隔符,以及類(lèi)型信息和值,如圖中紅色方框標(biāo)準(zhǔn)的信息。

不建議大家去死磕序列化文件是如何組織數(shù)據(jù)的,因?yàn)橐饬x不大
如果你真的有興趣建議你閱讀以下代碼 org.apache.catalina.session.StandardSession.doWriteObject
監(jiān)聽(tīng)器
在JavaEE的標(biāo)準(zhǔn)中,我們可以通過(guò)配置 HttpSessionAttributeListener 來(lái)監(jiān)聽(tīng)Session的變化,那么在 StandardSession 中是如何實(shí)現(xiàn)的呢,如果你了解觀(guān)察者模式,那么想必你已經(jīng)知道答案了。 以setAttribute為例,在調(diào)用此方法之后會(huì)立即在本線(xiàn)程調(diào)用監(jiān)聽(tīng)器的方法進(jìn)行處理,這意味著我們不應(yīng)該在監(jiān)聽(tīng)器中執(zhí)行阻塞時(shí)間過(guò)長(zhǎng)的操作。
public void setAttribute(String name, Object value, boolean notify) {
//省略無(wú)關(guān)代碼
//獲取上文中配置的事件監(jiān)聽(tīng)器
Object listeners[] = context.getApplicationEventListeners();
if (listeners == null) {
return;
}
for (int i = 0; i < listeners.length; i++) {
//只有HttpSessionAttributeListener才可以執(zhí)行
if (!(listeners[i] instanceof HttpSessionAttributeListener)) {
continue;
}
HttpSessionAttributeListener listener = (HttpSessionAttributeListener) listeners[i];
try {
//在當(dāng)前線(xiàn)程調(diào)用監(jiān)聽(tīng)器的處理方法
if (unbound != null) {
if (unbound != value || manager.getNotifyAttributeListenerOnUnchangedValue()) {
//如果是某個(gè)鍵的值被修改則調(diào)用監(jiān)聽(tīng)器的attributeReplaced方法
context.fireContainerEvent("beforeSessionAttributeReplaced", listener);
if (event == null) {
event = new HttpSessionBindingEvent(getSession(), name, unbound);
}
listener.attributeReplaced(event);
context.fireContainerEvent("afterSessionAttributeReplaced", listener);
}
} else {
//如果是新添加某個(gè)鍵則執(zhí)行attributeAdded方法
context.fireContainerEvent("beforeSessionAttributeAdded", listener);
if (event == null) {
event = new HttpSessionBindingEvent(getSession(), name, value);
}
listener.attributeAdded(event);
context.fireContainerEvent("afterSessionAttributeAdded", listener);
}
} catch (Throwable t) {
//異常處理
}
}
}
Sesssion生命周期
如何保存Session
在了解完Session的結(jié)構(gòu)之后,我們有必要明確 StandardSession 是在何時(shí)被創(chuàng)建的,以及需要注意的點(diǎn)。
首先我們來(lái)看看 StandardSession 的構(gòu)造函數(shù), 其代碼如下所示。
public StandardSession(Manager manager) {
//調(diào)用Object類(lèi)的構(gòu)造方法,默認(rèn)已經(jīng)調(diào)用了
//此處再聲明一次,不知其用意,或許之前此類(lèi)有父類(lèi)?
super();
this.manager = manager;
//是否開(kāi)啟訪(fǎng)問(wèn)計(jì)數(shù)
if (ACTIVITY_CHECK) {
accessCount = new AtomicInteger();
}
}
在創(chuàng)建 StandardSession 的時(shí)候都必須傳入 Manager 對(duì)象以便與此 StandardSession 關(guān)聯(lián),因此我們可以將目光轉(zhuǎn)移到 Manager ,而 Manager 與其子類(lèi)之間的關(guān)系如下圖所示。

我們將目光轉(zhuǎn)移到 ManagerBase中可以發(fā)現(xiàn)以下代碼。
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
Session 是Tomcat自定義的接口, StandardSession 實(shí)現(xiàn)了 HttpSession 以及 Session 接口,此接口功能更加豐富,但并不向程序員提供。
查找此屬性可以發(fā)現(xiàn),與Session相關(guān)的操作都是通過(guò)操作 sessions 來(lái)實(shí)現(xiàn)的,因此我們可以明確保存Session的數(shù)據(jù)結(jié)構(gòu)是 ConcurrentHashMap 。

如何創(chuàng)建Session
那么Session到底是如何創(chuàng)建的呢?我找到了以下方法 ManagerBase.creaeSession , 總結(jié)其流程如下。
- 檢查session數(shù)是否超過(guò)限制,如果有就拋出異常
- 創(chuàng)建StandardSession對(duì)象
- 設(shè)置session各種必須的屬性(合法性, 最大超時(shí)時(shí)間, sessionId)
- 生成SessionId, Tomcat支持不同的SessionId算法,本人調(diào)試過(guò)程其所使用的SessionId生成算法是LazySessionIdGenerator(此算法與其他算法不同之處就在于并不會(huì)在一開(kāi)始就加載隨機(jī)數(shù)數(shù)組,而是在用到的時(shí)候才加載,此處的隨機(jī)數(shù)組并不是普通的隨機(jī)數(shù)組而是SecureRandom,相關(guān)信息可以閱讀大佬的文章)
- 增加session的計(jì)數(shù),由于Tomcat的策略是只計(jì)算100個(gè)session的創(chuàng)建速率,因此sessionCreationTiming是固定大小為100的鏈表(一開(kāi)始為100個(gè)值為null的元素),因此在將新的數(shù)據(jù)添加到鏈表中時(shí)必須要將舊的數(shù)據(jù)移除鏈表以保證其固定的大小。session創(chuàng)建速率計(jì)算公式如下
(1000*60*counter)/(int)(now - oldest)
其中
- now為獲取統(tǒng)計(jì)數(shù)據(jù)時(shí)的時(shí)間System.currentTimeMillis()
- oldest為隊(duì)列中最早創(chuàng)建session的時(shí)間
- counter為隊(duì)列中值不為null的元素的數(shù)量
- 由于計(jì)算的是每分鐘的速率因此在此處必須將1000乘以60(一分鐘內(nèi)有60000毫秒)
public Session createSession(String sessionId) {
//檢查Session是否超過(guò)限制,如果是則拋出異常
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
//該方法會(huì)創(chuàng)建StandardSession對(duì)象
Session session = createEmptySession();
//初始化Session中必要的屬性
session.setNew(true);
//session是否可用
session.setValid(true);
//創(chuàng)建時(shí)間
session.setCreationTime(System.currentTimeMillis());
//設(shè)置session最大超時(shí)時(shí)間
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);
sessionCounter++;
//記錄創(chuàng)建session的時(shí)間,用于統(tǒng)計(jì)數(shù)據(jù)session的創(chuàng)建速率
//類(lèi)似的還有ExpireRate即Session的過(guò)期速率
//由于可能會(huì)有其他線(xiàn)程對(duì)sessionCreationTiming操作因此需要加鎖
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
//sessionCreationTiming是LinkedList
//因此poll會(huì)移除鏈表頭的數(shù)據(jù),也就是最舊的數(shù)據(jù)
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session;
}
Session的銷(xiāo)毀
要銷(xiāo)毀Session,必然要將Session從 ConcurrentHashMap 中移除,順藤摸瓜我們可以發(fā)現(xiàn)其移除session的代碼如下所示。
@Override
public void remove(Session session, boolean update) {
//檢查是否需要將統(tǒng)計(jì)過(guò)期的session的信息
if (update) {
long timeNow = System.currentTimeMillis();
int timeAlive =
(int) (timeNow - session.getCreationTimeInternal())/1000;
updateSessionMaxAliveTime(timeAlive);
expiredSessions.incrementAndGet();
SessionTiming timing = new SessionTiming(timeNow, timeAlive);
synchronized (sessionExpirationTiming) {
sessionExpirationTiming.add(timing);
sessionExpirationTiming.poll();
}
}
//將session從Map中移除
if (session.getIdInternal() != null) {
sessions.remove(session.getIdInternal());
}
}
被銷(xiāo)毀的時(shí)機(jī)
主動(dòng)銷(xiāo)毀
我們可以通過(guò)調(diào)用 HttpSession.invalidate() 方法來(lái)執(zhí)行session銷(xiāo)毀操作。此方法最終調(diào)用的是 StandardSession.invalidate() 方法,其代碼如下,可以看出使 session 銷(xiāo)毀的關(guān)鍵方法是 StandardSession.expire()
public void invalidate() {
if (!isValidInternal())
throw new IllegalStateException
(sm.getString("standardSession.invalidate.ise"));
// Cause this session to expire
expire();
}
expire 方法的代碼如下
@Override
public void expire() {
expire(true);
}
public void expire(boolean notify) {
//省略代碼
//將session從ConcurrentHashMap中移除
manager.remove(this, true);
//被省略的代碼主要是將session被銷(xiāo)毀的消息通知
//到各個(gè)監(jiān)聽(tīng)器上
}
超時(shí)銷(xiāo)毀
除了主動(dòng)銷(xiāo)毀之外,我們可以為session設(shè)置一個(gè)過(guò)期時(shí)間,當(dāng)時(shí)間到達(dá)之后session會(huì)被后臺(tái)線(xiàn)程主動(dòng)銷(xiāo)毀。我們可以為session設(shè)置一個(gè)比較短的過(guò)期時(shí)間,然后通過(guò) JConsole 來(lái)追蹤其調(diào)用棧,其是哪個(gè)對(duì)象哪個(gè)線(xiàn)程執(zhí)行了銷(xiāo)毀操作。
如下圖所示,我們?yōu)閟ession設(shè)置了一個(gè)30秒的超時(shí)時(shí)間。

然后我們?cè)?ManagerBase.remove
方法上打上斷點(diǎn),等待30秒之后,如下圖所示

Tomcat會(huì)開(kāi)啟一個(gè)后臺(tái)線(xiàn)程,來(lái)定期執(zhí)行子組件的 backgroundProcess 方法(前提是子組件被Tomcat管理且實(shí)現(xiàn)了 Manager接口)
@Override
public void backgroundProcess() {
count = (count + 1) % processExpiresFrequency;
if (count == 0)
processExpires();
}
public void processExpires() {
long timeNow = System.currentTimeMillis();
Session sessions[] = findSessions();
int expireHere = 0 ;
if(log.isDebugEnabled())
log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);
//從JConsole的圖中可以看出isValid可能導(dǎo)致expire方法被調(diào)用
for (int i = 0; i < sessions.length; i++) {
if (sessions[i]!=null && !sessions[i].isValid()) {
expireHere++;
}
}
long timeEnd = System.currentTimeMillis();
if(log.isDebugEnabled())
log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere);
processingTime += ( timeEnd - timeNow );
}
我們可以來(lái)看看接口中 Manager.backgroundProcess 中注釋,簡(jiǎn)略翻譯一下就是 backgroundProcess 會(huì)被容器定期的執(zhí)行,可以用來(lái)執(zhí)行session清理任務(wù)等。
/** * This method will be invoked by the context/container on a periodic * basis and allows the manager to implement * a method that executes periodic tasks, such as expiring sessions etc. */ public void backgroundProcess();
總結(jié)
Session的數(shù)據(jù)結(jié)構(gòu)如下圖所示,簡(jiǎn)單來(lái)說(shuō)就是用 ConcurrentHashMap 來(lái)保存 Session ,而 Session 則用 ConcurrentHashMap 來(lái)保存鍵值對(duì),其結(jié)構(gòu)如下圖所示。 .jpg

這意味著,不要拼命的往Session里面添加離散的數(shù)據(jù), 把離散的數(shù)據(jù)封裝成一個(gè)對(duì)象性能會(huì)更加好 如下所示
//bad
httpSession.setAttribute("user","kesan");
httpSession.setAttribute("nickname","點(diǎn)贊");
httpSession.setAttribute("sex","男");
....
//good
User kesan = userDao.getUser()
httpSession.setAttribute("user", kesan);
如果你為Session配置了監(jiān)聽(tīng)器,那么對(duì)Session執(zhí)行任何變更都將直接在當(dāng)前線(xiàn)程執(zhí)行監(jiān)聽(tīng)器的方法, 因此最好不要在監(jiān)聽(tīng)器中執(zhí)行可能會(huì)發(fā)生阻塞的方法 。
Tomcat會(huì)開(kāi)啟一個(gè)后臺(tái)線(xiàn)程來(lái)定期執(zhí)行 ManagerBase.backgroundProcess 方法用來(lái)檢測(cè)過(guò)期的Session并將其銷(xiāo)毀。
思想遷移
對(duì)象生成速率算法此算法設(shè)計(jì)比較有趣,并且也可以應(yīng)用到其他項(xiàng)目中,因此做如下總結(jié)。
首先生成一個(gè)固定大小的鏈表(比如說(shuō)100),然后以null元素填充。 當(dāng)創(chuàng)建新的對(duì)象時(shí),將創(chuàng)建時(shí)間加入鏈表末尾中(當(dāng)然是封裝后的對(duì)象),然后將鏈表頭節(jié)點(diǎn)移除,此時(shí)被移除的對(duì)象要么是null節(jié)點(diǎn)要么是最早加入鏈表的節(jié)點(diǎn) 當(dāng)要計(jì)算對(duì)象生成速率時(shí),統(tǒng)計(jì)鏈表中不為null的元素的數(shù)量除以當(dāng)前的時(shí)間與最早創(chuàng)建對(duì)象的時(shí)間的差,便可以得出其速率。(注意時(shí)間單位的轉(zhuǎn)換)
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Tomcat配置HTTPS訪(fǎng)問(wèn)的實(shí)現(xiàn)步驟
本文主要介紹了Tomcat配置HTTPS訪(fǎng)問(wèn)的實(shí)現(xiàn)步驟,在tomcat中存在兩種證書(shū)驗(yàn)證情況單向驗(yàn)證和雙向驗(yàn)證,下面就詳細(xì)的介紹一下這兩種情況的配置,感興趣的可以了解一下2022-07-07
在Tomcat中部署Web項(xiàng)目的操作方法(必看篇)
下面小編就為大家?guī)?lái)一篇在Tomcat中部署Web項(xiàng)目的操作方法(必看篇)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
Tomcat服務(wù)器啟動(dòng)失敗的一些原因及解決辦法總結(jié)
Tomcat是常用的應(yīng)用服務(wù)器之一,主要用于開(kāi)發(fā)和測(cè)試,也有少量用戶(hù)用在生產(chǎn)系統(tǒng)中,這篇文章主要給大家介紹了關(guān)于Tomcat服務(wù)器啟動(dòng)失敗的一些原因及解決辦法的相關(guān)資料,需要的朋友可以參考下2023-12-12
Tomcat啟動(dòng)時(shí)如何設(shè)置JVM參數(shù)
這篇文章主要介紹了Tomcat啟動(dòng)時(shí)如何設(shè)置JVM參數(shù)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
Tomcat?Catalina為什么不new出來(lái)原理解析
這篇文章主要為大家介紹了Tomcat?Catalina為什么不new出來(lái)原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
HBuilderX配置tomcat外部服務(wù)器查看編輯jsp界面的方法詳解
這篇文章主要介紹了HBuilderX配置tomcat外部服務(wù)器查看編輯jsp界面的方法,本文通過(guò)實(shí)例圖文相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
Eclipse部署Tomcat實(shí)現(xiàn)JSP運(yùn)行的超詳細(xì)教程
這篇文章主要介紹了Eclipse部署Tomcat實(shí)現(xiàn)JSP運(yùn)行,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08
Tomcat 類(lèi)加載器的實(shí)現(xiàn)方法及實(shí)例代碼
這篇文章主要介紹了Tomcat 類(lèi)加載器的實(shí)現(xiàn)方法及實(shí)例代碼,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05
tomcat啟動(dòng)報(bào)錯(cuò):java.util.zip.ZipException的解決方法
這篇文章主要給大家介紹了關(guān)于tomcat啟動(dòng)報(bào):java.util.zip.ZipException錯(cuò)誤的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),同樣遇到這個(gè)問(wèn)題的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08

