您好,登錄后才能下訂單哦!
小編給大家分享一下Tomcat是怎么管理Session的示例方法,希望大家閱讀完這篇文章之后都有所收獲,下面讓我們一起去探討吧!
Session結構
不多廢話,直接上圖
仔細觀察上圖,我們可以得出以下結論
HttpSession
是JavaEE標準中操作Session的接口類,因此我們實際上操作的是 StandardSessionFacade
類
Session
保存數據所使用的數據結構是 ConcurrentHashMap
, 如你在圖上看到的我們往 Session
中保存了一個msg
為什么需要使用 ConcurrentHashMap
呢?原因是,在處理Http請求并不是只有一個線程會訪問這個Session, 現代Web應用訪問一次頁面,通常需要同時執行多次請求, 而這些請求可能會在同一時刻內被Web容器中不同線程同時執行,因此如果采用 HashMap
的話,很容易引發線程安全的問題。
讓我們先來看看HttpSession的包裝類。
StandardSessionFacade
在此類中我們可以學習到外觀模式(Facde)的實際應用。其定義如下所示。
public class StandardSessionFacade implements HttpSession
那么此類是如何實現Session的功能呢?觀察以下代碼不難得出,此類并不是HttpSession的真正實現類,而是將真正的HttpSession實現類進行包裝,只暴露HttpSession接口中的方法,也就是設計模式中的外觀(Facde)模式。
private final HttpSession session; public StandardSessionFacade(HttpSession session) { this.session = session; }
那么我們為什么不直接使用HttpSession的實現類呢?
根據圖1,我們可以知道HttpSession的真正實現類是 StandardSession
,假設在該類內定義了一些本應由Tomcat調用而非由程序調用的方法,那么由于Java的類型系統我們將可以直接操作該類,這將會帶來一些不可預見的問題,如以下代碼所示。
而如果我們將 StandardSession
再包裝一層,上圖代碼執行的時候將會發生錯誤。如下圖所示,將會拋出類型轉換的異常,從而阻止此處非法的操作。
再進一步,我們由辦法繞外觀類直接訪問 StandardSession
嗎?
事實上是可以的,我們可以通過反射機制來獲取 StandardSession
,但你最好清楚自己在干啥。代碼如下所示
@GetMapping("/s") public String sessionTest(HttpSession httpSession) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { StandardSessionFacade session = (StandardSessionFacade) httpSession; Class targetClass = Class.forName(session.getClass().getName()); //修改可見性 Field standardSessionField = targetClass.getDeclaredField("session"); standardSessionField.setAccessible(true); //獲取 StandardSession standardSession = (StandardSession) standardSessionField.get(session); return standardSession.getManager().toString(); }
StandardSession
該類的定義如下
public class StandardSession implements HttpSession, Session, Serializable
通過其接口我們可以看出此類除了具有JavaEE標準中 HttpSession
要求實現的功能之外,還有序列化的功能。
在圖1中我們已經知道 StandardSession
是用 ConcurrentHashMap
來保存的數據,因此接下來我們主要關注 StandardSession
的序列化以及反序列化的實現,以及監聽器的功能。
序列化
還記得上一節我們通過反射機制獲取到了 StandardSession
嗎?利用以下代碼我們可以直接觀察到反序列化出來的 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()); //修改可見性 Field standardSessionField = targetClass.getDeclaredField("session"); standardSessionField.setAccessible(true); //獲取 StandardSession standardSession = (StandardSession) standardSessionField.get(session); //存點數據以便觀察 standardSession.setAttribute("msg","hello,world"); standardSession.setAttribute("user","kesan"); standardSession.setAttribute("password", "點贊"); standardSession.setAttribute("tel", 10086L); //將序列化的結果直接寫到Http的響應中 ObjectOutputStream objectOutputStream = new ObjectOutputStream(response.getOutputStream()); standardSession.writeObjectData(objectOutputStream); }
如果不出意外,訪問此接口瀏覽器將會執行下載操作,最后得到一個文件
使用 WinHex
打開分析,如圖所示為序列化之后得結果,主要是一大堆分隔符,以及類型信息和值,如圖中紅色方框標準的信息。
不建議大家去死磕序列化文件是如何組織數據的,因為意義不大
如果你真的有興趣建議你閱讀以下代碼 org.apache.catalina.session.StandardSession.doWriteObject
監聽器
在JavaEE的標準中,我們可以通過配置 HttpSessionAttributeListener
來監聽Session的變化,那么在 StandardSession
中是如何實現的呢,如果你了解觀察者模式,那么想必你已經知道答案了。 以setAttribute為例,在調用此方法之后會立即在本線程調用監聽器的方法進行處理,這意味著我們不應該在監聽器中執行阻塞時間過長的操作。
public void setAttribute(String name, Object value, boolean notify) { //省略無關代碼 //獲取上文中配置的事件監聽器 Object listeners[] = context.getApplicationEventListeners(); if (listeners == null) { return; } for (int i = 0; i < listeners.length; i++) { //只有HttpSessionAttributeListener才可以執行 if (!(listeners[i] instanceof HttpSessionAttributeListener)) { continue; } HttpSessionAttributeListener listener = (HttpSessionAttributeListener) listeners[i]; try { //在當前線程調用監聽器的處理方法 if (unbound != null) { if (unbound != value || manager.getNotifyAttributeListenerOnUnchangedValue()) { //如果是某個鍵的值被修改則調用監聽器的attributeReplaced方法 context.fireContainerEvent("beforeSessionAttributeReplaced", listener); if (event == null) { event = new HttpSessionBindingEvent(getSession(), name, unbound); } listener.attributeReplaced(event); context.fireContainerEvent("afterSessionAttributeReplaced", listener); } } else { //如果是新添加某個鍵則執行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的結構之后,我們有必要明確 StandardSession
是在何時被創建的,以及需要注意的點。
首先我們來看看 StandardSession
的構造函數, 其代碼如下所示。
public StandardSession(Manager manager) { //調用Object類的構造方法,默認已經調用了 //此處再聲明一次,不知其用意,或許之前此類有父類? super(); this.manager = manager; //是否開啟訪問計數 if (ACTIVITY_CHECK) { accessCount = new AtomicInteger(); } }
在創建 StandardSession
的時候都必須傳入 Manager
對象以便與此 StandardSession
關聯,因此我們可以將目光轉移到 Manager
,而 Manager
與其子類之間的關系如下圖所示。
我們將目光轉移到 ManagerBase
中可以發現以下代碼。
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
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毫秒)
public Session createSession(String sessionId) { //檢查Session是否超過限制,如果是則拋出異常 if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) { rejectedSessions++; throw new TooManyActiveSessionsException( sm.getString("managerBase.createSession.ise"), maxActiveSessions); } //該方法會創建StandardSession對象 Session session = createEmptySession(); //初始化Session中必要的屬性 session.setNew(true); //session是否可用 session.setValid(true); //創建時間 session.setCreationTime(System.currentTimeMillis()); //設置session最大超時時間 session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60); String id = sessionId; if (id == null) { id = generateSessionId(); } session.setId(id); sessionCounter++; //記錄創建session的時間,用于統計數據session的創建速率 //類似的還有ExpireRate即Session的過期速率 //由于可能會有其他線程對sessionCreationTiming操作因此需要加鎖 SessionTiming timing = new SessionTiming(session.getCreationTime(), 0); synchronized (sessionCreationTiming) { //sessionCreationTiming是LinkedList //因此poll會移除鏈表頭的數據,也就是最舊的數據 sessionCreationTiming.add(timing); sessionCreationTiming.poll(); } return session; }
Session的銷毀
要銷毀Session,必然要將Session從 ConcurrentHashMap
中移除,順藤摸瓜我們可以發現其移除session的代碼如下所示。
@Override public void remove(Session session, boolean update) { //檢查是否需要將統計過期的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()); } }
被銷毀的時機
主動銷毀
我們可以通過調用 HttpSession.invalidate()
方法來執行session銷毀操作。此方法最終調用的是 StandardSession.invalidate()
方法,其代碼如下,可以看出使 session
銷毀的關鍵方法是 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被銷毀的消息通知 //到各個監聽器上 }
超時銷毀
除了主動銷毀之外,我們可以為session設置一個過期時間,當時間到達之后session會被后臺線程主動銷毀。我們可以為session設置一個比較短的過期時間,然后通過 JConsole
來追蹤其調用棧,其是哪個對象哪個線程執行了銷毀操作。
如下圖所示,我們為session設置了一個30秒的超時時間。
然后我們在 ManagerBase.remove
方法上打上斷點,等待30秒之后,如下圖所示
Tomcat會開啟一個后臺線程,來定期執行子組件的 backgroundProcess
方法(前提是子組件被Tomcat管理且實現了 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可能導致expire方法被調用 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 ); }
我們可以來看看接口中 Manager.backgroundProcess
中注釋,簡略翻譯一下就是 backgroundProcess
會被容器定期的執行,可以用來執行session清理任務等。
/** * 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();
總結
Session的數據結構如下圖所示,簡單來說就是用 ConcurrentHashMap
來保存 Session
,而 Session
則用 ConcurrentHashMap
來保存鍵值對,其結構如下圖所示。 .jpg
這意味著,不要拼命的往Session里面添加離散的數據, 把離散的數據封裝成一個對象性能會更加好 如下所示
//bad httpSession.setAttribute("user","kesan"); httpSession.setAttribute("nickname","點贊"); httpSession.setAttribute("sex","男"); ....
//good User kesan = userDao.getUser() httpSession.setAttribute("user", kesan);
如果你為Session配置了監聽器,那么對Session執行任何變更都將直接在當前線程執行監聽器的方法, 因此最好不要在監聽器中執行可能會發生阻塞的方法 。
Tomcat會開啟一個后臺線程來定期執行 ManagerBase.backgroundProcess
方法用來檢測過期的Session并將其銷毀。
思想遷移
對象生成速率算法此算法設計比較有趣,并且也可以應用到其他項目中,因此做如下總結。
首先生成一個固定大小的鏈表(比如說100),然后以null元素填充。 當創建新的對象時,將創建時間加入鏈表末尾中(當然是封裝后的對象),然后將鏈表頭節點移除,此時被移除的對象要么是null節點要么是最早加入鏈表的節點 當要計算對象生成速率時,統計鏈表中不為null的元素的數量除以當前的時間與最早創建對象的時間的差,便可以得出其速率。(注意時間單位的轉換)
看完了這篇文章,相信你對“Tomcat是怎么管理Session的示例方法”有了一定的了解,如果想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。