Tomcat session的實(shí)現(xiàn):線程安全與管理
本文所說的session是單機(jī)版本的session, 事實(shí)上在當(dāng)前的互聯(lián)網(wǎng)實(shí)踐中已經(jīng)不太存在這種定義了。我們主要討論的是其安全共享的實(shí)現(xiàn),只從理論上來討論,不必太過在意實(shí)用性問題。
1:session 的意義簡說
大概就是一個(gè)會話的的定義,客戶端有cookie記錄,服務(wù)端session定義。用于確定你就是你的一個(gè)東西。
每個(gè)用戶在一定范圍內(nèi)共享某個(gè)session信息,以實(shí)現(xiàn)登錄狀態(tài),操作的鑒權(quán)保持等。
我們將會借助tomcat的實(shí)現(xiàn),剖析session管理的一些實(shí)現(xiàn)原理。
2. tomcat 中 session 什么時(shí)候創(chuàng)建?
session 信息會在兩個(gè)地方調(diào)用,一是每次請求進(jìn)來時(shí),框架會嘗試去加載原有對應(yīng)的session信息(不會新建)。二是應(yīng)用自己調(diào)用getSession()時(shí),此時(shí)如果不存在session信息,則創(chuàng)建一個(gè)新的session對象,代表應(yīng)用后續(xù)會使用此功能。即框架不會自動支持session相關(guān)功能,只是在你需要的時(shí)候進(jìn)行輔助操作。
// case1. 框架自行調(diào)用session信息,不會主動創(chuàng)建session// org.springframework.web.servlet.support.SessionFlashMapManager#retrieveFlashMaps/*** Retrieves saved FlashMap instances from the HTTP session, if any.*/@Override@SuppressWarnings("unchecked")protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {HttpSession session = request.getSession(false);return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null);}// case2. 應(yīng)用主動調(diào)用session信息,不存在時(shí)會創(chuàng)建新的session, 以滿足業(yè)務(wù)連續(xù)性需要@GetMapping("sessionTest")public Object sessionTest(HttpServletRequest request, HttpServletResponse response) {// 主動獲取session信息HttpSession session = request.getSession();String sid = session.getId();System.out.println("sessionId:" + sid);return ResponseInfoBuilderUtil.success(sid);}
在tomcat中,HttpServletRequest的實(shí)際類都是 RequestFacade, 所以獲取session信息也是以其為入口進(jìn)行。
// org.apache.catalina.connector.RequestFacade#getSession()@Overridepublic HttpSession getSession() {if (request == null) {throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));}// 如果不存在session則創(chuàng)建一個(gè)// session 的實(shí)現(xiàn)有兩種:一是基于內(nèi)存的實(shí)現(xiàn),二是基于文件的實(shí)現(xiàn)。return getSession(true);}@Overridepublic HttpSession getSession(boolean create) {if (request == null) {throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));}if (SecurityUtil.isPackageProtectionEnabled()){return AccessController.doPrivileged(new GetSessionPrivilegedAction(create));} else {// RequestFacade 是個(gè)外觀模式實(shí)現(xiàn),核心請求還是會傳遞給 Request處理的// org.apache.catalina.connector.Requestreturn request.getSession(create);}}// org.apache.catalina.connector.Request#getSession(boolean)/*** @return the session associated with this Request, creating one* if necessary and requested.** @param create Create a new session if one does not exist*/@Overridepublic HttpSession getSession(boolean create) {// 由 create 字段決定是否需要創(chuàng)建新的session, 如果不存在的話。// Session 是tomcat的一個(gè)會話實(shí)現(xiàn)類,并非對接規(guī)范接口類,其會包裝一個(gè)HttpSession,以便統(tǒng)一交互// 因?yàn)橹挥?HttpSession 才是 Servlet 的接口規(guī)范,在tomcat中會以 StandardSessionFacade 實(shí)現(xiàn)接口,其也是一個(gè)外觀模式的實(shí)現(xiàn),具體工作由 StandardSession 處理。Session session = doGetSession(create);if (session == null) {return null;}// 包裝 Session 為 HttpSession 規(guī)范返回return session.getSession();}// org.apache.catalina.connector.Request#doGetSessionprotected Session doGetSession(boolean create) {// There cannot be a session if no context has been assigned yet// mappingData.context;Context context = getContext();if (context == null) {return (null);}// Return the current session if it exists and is valid// 此處檢查session有效性時(shí),也會做部分清理工作if ((session != null) && !session.isValid()) {session = null;}if (session != null) {return (session);}// Return the requested session if it exists and is valid// 獲取manager 實(shí)例,即真正進(jìn)行 Session 管理的類,其實(shí)主要分兩種:1. 基于內(nèi)存;2. 基于文件的持久化;Manager manager = context.getManager();if (manager == null) {return (null); // Sessions are not supported}if (requestedSessionId != null) {try {// 如果不是第一次請求,則會帶上服務(wù)返回的 sessionId, 就會主動查找原來的session// 從 sessions 中查找即可session = manager.findSession(requestedSessionId);} catch (IOException e) {session = null;}if ((session != null) && !session.isValid()) {session = null;}// 后續(xù)請求,每次請求都會更新有效時(shí)間if (session != null) {session.access();return (session);}}// Create a new session if requested and the response is not committed// 主動請求session時(shí),才會繼續(xù)后續(xù)邏輯if (!create) {return (null);}if (response != null&& context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)&& response.getResponse().isCommitted()) {throw new IllegalStateException(sm.getString("coyoteRequest.sessionCreateCommitted"));}// Re-use session IDs provided by the client in very limited// circumstances.String sessionId = getRequestedSessionId();if (requestedSessionSSL) {// If the session ID has been obtained from the SSL handshake then// use it.} else if (("/".equals(context.getSessionCookiePath())&& isRequestedSessionIdFromCookie())) {/* This is the common(ish) use case: using the same session ID with* multiple web applications on the same host. Typically this is* used by Portlet implementations. It only works if sessions are* tracked via cookies. The cookie must have a path of "/" else it* won't be provided for requests to all web applications.** Any session ID provided by the client should be for a session* that already exists somewhere on the host. Check if the context* is configured for this to be confirmed.*/if (context.getValidateClientProvidedNewSessionId()) {boolean found = false;for (Container container : getHost().findChildren()) {Manager m = ((Context) container).getManager();if (m != null) {try {if (m.findSession(sessionId) != null) {found = true;break;}} catch (IOException e) {// Ignore. Problems with this manager will be// handled elsewhere.}}}if (!found) {sessionId = null;}}} else {// 當(dāng)session無效時(shí),需要將原來的seesionId置空,刪除并新創(chuàng)建一個(gè)使用sessionId = null;}// 創(chuàng)建session, StandardManager -> ManagerBasesession = manager.createSession(sessionId);// Creating a new session cookie based on that sessionif (session != null&& context.getServletContext().getEffectiveSessionTrackingModes().contains(SessionTrackingMode.COOKIE)) {// 創(chuàng)建cookie信息,與session對應(yīng)Cookie cookie =ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure());// 添加到response中,在響應(yīng)結(jié)果一起返回給客戶端response.addSessionCookieInternal(cookie);}if (session == null) {return null;}// 每次請求session時(shí),必然刷新激活時(shí)間,以便判定會話是否超時(shí)session.access();return session;}
從上面我們可以看到,session的流程大概是這樣的:
1. 先查找是否有session信息存在,如果有則判斷是否失效;
2. 如果不存在session或已失效,則使用一個(gè)新的sessionId(非必須)創(chuàng)建一個(gè)session實(shí)例;
3. session創(chuàng)建成功,則將sessionId寫入到cookie信息中,以便客戶端后續(xù)使用;
4. 每次請求完session,必定刷新下訪問時(shí)間以續(xù)期;
session的管理主要有兩種實(shí)現(xiàn)方式,類圖如下:

我們先主要以基于內(nèi)存的實(shí)現(xiàn)來理解下session的管理過程。實(shí)際上StandardManager基本就依托于 ManagerBase 就實(shí)現(xiàn)了Session管理功能,下面我們來看一下其創(chuàng)建session如何?
// org.apache.catalina.session.ManagerBase#createSession@Overridepublic Session createSession(String sessionId) {// 首先來個(gè)安全限制,允許同時(shí)存在多少會話// 這個(gè)會話實(shí)際上代表的是一段時(shí)間的有效性,并非真正的用戶有效使用在線,所以該值一般要求比預(yù)計(jì)的數(shù)量大些才好if ((maxActiveSessions >= 0) &&(getActiveSessions() >= maxActiveSessions)) {rejectedSessions++;throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"),maxActiveSessions);}// Recycle or create a Session instance// 創(chuàng)建空的session 容器 return new StandardSession(this);Session session = createEmptySession();// Initialize the properties of the new session and return it// 默認(rèn)30分鐘有效期session.setNew(true);session.setValid(true);session.setCreationTime(System.currentTimeMillis());session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);String id = sessionId;if (id == null) {// sessionId 為空時(shí),生成一個(gè),隨機(jī)idid = generateSessionId();}// 設(shè)置sessionId, 注意此處不僅僅是set這么簡單,其同時(shí)會將自身session注冊到全局session管理器中.如下文session.setId(id);sessionCounter++;SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);synchronized (sessionCreationTiming) {// LinkedList, 添加一個(gè),刪除一個(gè)?sessionCreationTiming.add(timing);sessionCreationTiming.poll();}return (session);}// org.apache.catalina.session.StandardSession#setId/*** Set the session identifier for this session.** @param id The new session identifier*/@Overridepublic void setId(String id) {setId(id, true);}@Overridepublic void setId(String id, boolean notify) {// 如果原來的id不為空,則先刪除原有的if ((this.id != null) && (manager != null))manager.remove(this);this.id = id;// 再將自身會話注冊到 manager 中,即 sessions 中if (manager != null)manager.add(this);// 通知監(jiān)聽者,這是框架該做好的事(擴(kuò)展點(diǎn)),不過不是本文的方向,忽略if (notify) {tellNew();}}// org.apache.catalina.session.ManagerBase#add@Overridepublic void add(Session session) {// 取出 sessionId, 添加到 sessions 容器,統(tǒng)一管理sessions.put(session.getIdInternal(), session);int size = getActiveSessions();// 刷新最大活躍數(shù),使用雙重鎖優(yōu)化更新該值if( size > maxActive ) {synchronized(maxActiveUpdateLock) {if( size > maxActive ) {maxActive = size;}}}}// 查找session也是異常簡單,只管從 ConcurrentHashMap 中查找即可// org.apache.catalina.session.ManagerBase#findSession@Overridepublic Session findSession(String id) throws IOException {if (id == null) {return null;}return sessions.get(id);}
有興趣的同學(xué)可以看一下sessionId的生成算法:主要保證兩點(diǎn):1. 隨機(jī)性;2.不可重復(fù)性;
// org.apache.catalina.session.ManagerBase#generateSessionId/*** Generate and return a new session identifier.* @return a new session id*/protected String generateSessionId() {String result = null;do {if (result != null) {// Not thread-safe but if one of multiple increments is lost// that is not a big deal since the fact that there was any// duplicate is a much bigger issue.duplicates++;}// 使用 sessionIdGenerator 生成sessionIdresult = sessionIdGenerator.generateSessionId();// 如果已經(jīng)存在該sessionId, 則重新生成一個(gè)// session 是一個(gè) ConcurrentHashMap 結(jié)構(gòu)數(shù)據(jù)} while (sessions.containsKey(result));return result;}// org.apache.catalina.util.SessionIdGeneratorBase#generateSessionId/*** Generate and return a new session identifier.*/@Overridepublic String generateSessionId() {return generateSessionId(jvmRoute);}// org.apache.catalina.util.StandardSessionIdGenerator#generateSessionId@Overridepublic String generateSessionId(String route) {byte random[] = new byte[16];// 默認(rèn)16int sessionIdLength = getSessionIdLength();// Render the result as a String of hexadecimal digits// Start with enough space for sessionIdLength and medium route size// 創(chuàng)建雙倍大小的stringBuilder, 容納sessionIdStringBuilder buffer = new StringBuilder(2 * sessionIdLength + 20);int resultLenBytes = 0;//while (resultLenBytes < sessionIdLength) {getRandomBytes(random);for (int j = 0;j < random.length && resultLenBytes < sessionIdLength;j++) {// 轉(zhuǎn)換為16進(jìn)制byte b1 = (byte) ((random[j] & 0xf0) >> 4);byte b2 = (byte) (random[j] & 0x0f);if (b1 < 10)buffer.append((char) ('0' + b1));elsebuffer.append((char) ('A' + (b1 - 10)));if (b2 < 10)buffer.append((char) ('0' + b2));elsebuffer.append((char) ('A' + (b2 - 10)));resultLenBytes++;}}if (route != null && route.length() > 0) {buffer.append('.').append(route);} else {String jvmRoute = getJvmRoute();if (jvmRoute != null && jvmRoute.length() > 0) {buffer.append('.').append(jvmRoute);}}return buffer.toString();}// org.apache.catalina.util.SessionIdGeneratorBase#getRandomBytesprotected void getRandomBytes(byte bytes[]) {// 使用 random.nextBytes(), 預(yù)生成 randomSecureRandom random = randoms.poll();if (random == null) {random = createSecureRandom();}random.nextBytes(bytes);// 添加到 ConcurrentLinkedQueue 隊(duì)列中,事實(shí)上該 random 將會被反復(fù)循環(huán)使用, poll->addrandoms.add(random);}
創(chuàng)建好session后,需要進(jìn)行隨時(shí)的維護(hù):我們看下tomcat是如何刷新訪問時(shí)間的?可能比預(yù)想的簡單,其僅是更新一個(gè)訪問時(shí)間字段,再無其他。
// org.apache.catalina.session.StandardSession#access/*** Update the accessed time information for this session. This method* should be called by the context when a request comes in for a particular* session, even if the application does not reference it.*/@Overridepublic void access() {// 更新訪問時(shí)間this.thisAccessedTime = System.currentTimeMillis();// 訪問次數(shù)統(tǒng)計(jì),默認(rèn)不啟用if (ACTIVITY_CHECK) {accessCount.incrementAndGet();}}
最后,還需要看下 HttpSession 是如何被包裝返回的?
// org.apache.catalina.session.StandardSession#getSession/*** Return the <code>HttpSession</code> for which this object* is the facade.*/@Overridepublic HttpSession getSession() {if (facade == null){if (SecurityUtil.isPackageProtectionEnabled()){final StandardSession fsession = this;facade = AccessController.doPrivileged(new PrivilegedAction<StandardSessionFacade>(){@Overridepublic StandardSessionFacade run(){return new StandardSessionFacade(fsession);}});} else {// 直接使用 StandardSessionFacade 包裝即可facade = new StandardSessionFacade(this);}}return (facade);}
再最后,要說明的是,整個(gè)sessions的管理使用一個(gè) ConcurrentHashMap 來存放全局會話信息,sessionId->session實(shí)例。
對于同一次http請求中,該session會被存儲在當(dāng)前的Request棧org.apache.catalina.connector.Request#session字段中,從而無需每次深入獲取。每個(gè)請求進(jìn)來后,會將session保存在當(dāng)前的request信息中。
3. 過期session清理?
會話不可能不過期,不過期的也不叫會話了。
會話過期的觸發(fā)時(shí)機(jī)主要有三個(gè):1. 每次進(jìn)行會話調(diào)用時(shí),會主動有效性isValid()驗(yàn)證,此時(shí)如果發(fā)現(xiàn)過期可以主動清理:2. 后臺定時(shí)任務(wù)觸發(fā)清理;3. 啟動或停止應(yīng)用的時(shí)候清理;(這對于非內(nèi)存式的存儲會更有用些)
// case1. 請求時(shí)驗(yàn)證,如前面所述// org.apache.catalina.connector.Request#doGetSessionprotected Session doGetSession(boolean create) {...// Return the current session if it exists and is validif ((session != null) && !session.isValid()) {session = null;}if (session != null) {return (session);}...}// case2. 后臺定時(shí)任務(wù)清理// org.apache.catalina.session.ManagerBase#backgroundProcess@Overridepublic void backgroundProcess() {// 并非每次定時(shí)任務(wù)到達(dá)時(shí)都會進(jìn)行清理,而是要根據(jù)其清理頻率設(shè)置來運(yùn)行// 默認(rèn)是 6count = (count + 1) % processExpiresFrequency;if (count == 0)processExpires();}/*** Invalidate all sessions that have expired.*/public void processExpires() {long timeNow = System.currentTimeMillis();// 找出所有的sessions, 轉(zhuǎn)化為數(shù)組遍歷Session sessions[] = findSessions();int expireHere = 0 ;if(log.isDebugEnabled())log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length);for (int i = 0; i < sessions.length; i++) {// 事實(shí)上后臺任務(wù)也是調(diào)用 isValid() 方法 進(jìn)行過期任務(wù)清理的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 );}//case3. start/stop 時(shí)觸發(fā)過期清理(生命周期事件)// org.apache.catalina.session.StandardManager#startInternal/*** Start this component and implement the requirements* of {@link org.apache.catalina.util.LifecycleBase#startInternal()}.** @exception LifecycleException if this component detects a fatal error* that prevents this component from being used*/@Overrideprotected synchronized void startInternal() throws LifecycleException {super.startInternal();// Load unloaded sessions, if anytry {// doLoad() 調(diào)用load();} catch (Throwable t) {ExceptionUtils.handleThrowable(t);log.error(sm.getString("standardManager.managerLoad"), t);}setState(LifecycleState.STARTING);}/*** Load any currently active sessions that were previously unloaded* to the appropriate persistence mechanism, if any. If persistence is not* supported, this method returns without doing anything.** @exception ClassNotFoundException if a serialized class cannot be* found during the reload* @exception IOException if an input/output error occurs*/protected void doLoad() throws ClassNotFoundException, IOException {if (log.isDebugEnabled()) {log.debug("Start: Loading persisted sessions");}// Initialize our internal data structuressessions.clear();// Open an input stream to the specified pathname, if anyFile file = file();if (file == null) {return;}if (log.isDebugEnabled()) {log.debug(sm.getString("standardManager.loading", pathname));}Loader loader = null;ClassLoader classLoader = null;Log logger = null;try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());BufferedInputStream bis = new BufferedInputStream(fis)) {Context c = getContext();loader = c.getLoader();logger = c.getLogger();if (loader != null) {classLoader = loader.getClassLoader();}if (classLoader == null) {classLoader = getClass().getClassLoader();}// Load the previously unloaded active sessionssynchronized (sessions) {try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger,getSessionAttributeValueClassNamePattern(),getWarnOnSessionAttributeFilterFailure())) {Integer count = (Integer) ois.readObject();int n = count.intValue();if (log.isDebugEnabled())log.debug("Loading " + n + " persisted sessions");for (int i = 0; i < n; i++) {StandardSession session = getNewSession();session.readObjectData(ois);session.setManager(this);sessions.put(session.getIdInternal(), session);session.activate();if (!session.isValidInternal()) {// If session is already invalid,// expire session to prevent memory leak.// 主動調(diào)用 expiresession.setValid(true);session.expire();}sessionCounter++;}} finally {// Delete the persistent storage fileif (file.exists()) {file.delete();}}}} catch (FileNotFoundException e) {if (log.isDebugEnabled()) {log.debug("No persisted data file found");}return;}if (log.isDebugEnabled()) {log.debug("Finish: Loading persisted sessions");}}// stopInternal() 事件到達(dá)時(shí)清理 sessions/*** Save any currently active sessions in the appropriate persistence* mechanism, if any. If persistence is not supported, this method* returns without doing anything.** @exception IOException if an input/output error occurs*/protected void doUnload() throws IOException {if (log.isDebugEnabled())log.debug(sm.getString("standardManager.unloading.debug"));if (sessions.isEmpty()) {log.debug(sm.getString("standardManager.unloading.nosessions"));return; // nothing to do}// Open an output stream to the specified pathname, if anyFile file = file();if (file == null) {return;}if (log.isDebugEnabled()) {log.debug(sm.getString("standardManager.unloading", pathname));}// Keep a note of sessions that are expiredArrayList<StandardSession> list = new ArrayList<>();try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());BufferedOutputStream bos = new BufferedOutputStream(fos);ObjectOutputStream oos = new ObjectOutputStream(bos)) {synchronized (sessions) {if (log.isDebugEnabled()) {log.debug("Unloading " + sessions.size() + " sessions");}// Write the number of active sessions, followed by the detailsoos.writeObject(Integer.valueOf(sessions.size()));for (Session s : sessions.values()) {StandardSession session = (StandardSession) s;list.add(session);session.passivate();session.writeObjectData(oos);}}}// Expire all the sessions we just wrote// 將所有session失效,實(shí)際上應(yīng)用即將關(guān)閉,失不失效的應(yīng)該也無所謂了if (log.isDebugEnabled()) {log.debug("Expiring " + list.size() + " persisted sessions");}for (StandardSession session : list) {try {session.expire(false);} catch (Throwable t) {ExceptionUtils.handleThrowable(t);} finally {session.recycle();}}if (log.isDebugEnabled()) {log.debug("Unloading complete");}}
接下來我們看下具體如何清理過期的會話?實(shí)際應(yīng)該就是一個(gè)remove的事。
// org.apache.catalina.session.StandardSession#isValid/*** Return the <code>isValid</code> flag for this session.*/@Overridepublic boolean isValid() {if (!this.isValid) {return false;}if (this.expiring) {return true;}if (ACTIVITY_CHECK && accessCount.get() > 0) {return true;}// 超過有效期,主動觸發(fā)清理if (maxInactiveInterval > 0) {int timeIdle = (int) (getIdleTimeInternal() / 1000L);if (timeIdle >= maxInactiveInterval) {expire(true);}}return this.isValid;}// org.apache.catalina.session.StandardSession#expire(boolean)/*** Perform the internal processing required to invalidate this session,* without triggering an exception if the session has already expired.** @param notify Should we notify listeners about the demise of* this session?*/public void expire(boolean notify) {// Check to see if session has already been invalidated.// Do not check expiring at this point as expire should not return until// isValid is falseif (!isValid)return;// 上鎖保證線程安全synchronized (this) {// Check again, now we are inside the sync so this code only runs once// Double check locking - isValid needs to be volatile// The check of expiring is to ensure that an infinite loop is not// entered as per bug 56339if (expiring || !isValid)return;if (manager == null)return;// Mark this session as "being expired"expiring = true;// Notify interested application event listeners// FIXME - Assumes we call listeners in reverse orderContext context = manager.getContext();// The call to expire() may not have been triggered by the webapp.// Make sure the webapp's class loader is set when calling the// listenersif (notify) {ClassLoader oldContextClassLoader = null;try {oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);Object listeners[] = context.getApplicationLifecycleListeners();if (listeners != null && listeners.length > 0) {HttpSessionEvent event =new HttpSessionEvent(getSession());for (int i = 0; i < listeners.length; i++) {int j = (listeners.length - 1) - i;if (!(listeners[j] instanceof HttpSessionListener))continue;HttpSessionListener listener =(HttpSessionListener) listeners[j];try {context.fireContainerEvent("beforeSessionDestroyed",listener);listener.sessionDestroyed(event);context.fireContainerEvent("afterSessionDestroyed",listener);} catch (Throwable t) {ExceptionUtils.handleThrowable(t);try {context.fireContainerEvent("afterSessionDestroyed", listener);} catch (Exception e) {// Ignore}manager.getContext().getLogger().error(sm.getString("standardSession.sessionEvent"), t);}}}} finally {context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);}}if (ACTIVITY_CHECK) {accessCount.set(0);}// Remove this session from our manager's active sessions// 從ManagerBase 中刪除manager.remove(this, true);// Notify interested session event listenersif (notify) {fireSessionEvent(Session.SESSION_DESTROYED_EVENT, null);}// Call the logout methodif (principal instanceof TomcatPrincipal) {TomcatPrincipal gp = (TomcatPrincipal) principal;try {gp.logout();} catch (Exception e) {manager.getContext().getLogger().error(sm.getString("standardSession.logoutfail"),e);}}// We have completed expire of this sessionsetValid(false);expiring = false;// Unbind any objects associated with this sessionString keys[] = keys();ClassLoader oldContextClassLoader = null;try {oldContextClassLoader = context.bind(Globals.IS_SECURITY_ENABLED, null);for (int i = 0; i < keys.length; i++) {removeAttributeInternal(keys[i], notify);}} finally {context.unbind(Globals.IS_SECURITY_ENABLED, oldContextClassLoader);}}}// org.apache.catalina.session.ManagerBase#remove(org.apache.catalina.Session, boolean)@Overridepublic void remove(Session session, boolean update) {// If the session has expired - as opposed to just being removed from// the manager because it is being persisted - update the expired statsif (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();}}// 從sessions中移除sessionif (session.getIdInternal() != null) {sessions.remove(session.getIdInternal());}}
清理工作的核心任務(wù)沒猜錯,還是進(jìn)行remove對應(yīng)的session, 但作為框架必然會設(shè)置很多的擴(kuò)展點(diǎn),為各監(jiān)聽器接入的機(jī)會。這些點(diǎn)的設(shè)計(jì),直接關(guān)系到整個(gè)功能的好壞了。
4. session如何保證線程安全?
實(shí)際是廢話,前面已經(jīng)明顯看出,其使用一個(gè) ConcurrentHashMap 作為session的管理容器,而ConcurrentHashMap本身就是線程安全的,自然也就保證了線程安全了。
不過需要注意的是,上面的線程安全是指的不同客戶端間的數(shù)據(jù)是互不影響的。然而對于同一個(gè)客戶端的重復(fù)請求,以上實(shí)現(xiàn)并未處理,即可能會生成一次session,也可能生成n次session,不過實(shí)際影響不大,因?yàn)榭蛻舳说臓顟B(tài)與服務(wù)端的狀態(tài)都是一致的。
5. 使用持久化方案的session管理實(shí)現(xiàn)
默認(rèn)情況使用內(nèi)存作為session管理工具,一是方便,二是速度相當(dāng)快。但是最大的缺點(diǎn)是,其無法實(shí)現(xiàn)持久化,即可能停機(jī)后信息就丟失了(雖然上面有在停機(jī)時(shí)做了持久化操作,但仍然是不可靠的)。
所以就有了與之相對的存儲方案了:Persistent,它有一個(gè)基類 PersistentManagerBase 繼承了 ManagerBase,做了些特別的實(shí)現(xiàn):
// 1. session的添加// 復(fù)用 ManagerBase// 2. session的查找// org.apache.catalina.session.PersistentManagerBase#findSession/*** {@inheritDoc}* <p>* This method checks the persistence store if persistence is enabled,* otherwise just uses the functionality from ManagerBase.*/@Overridepublic Session findSession(String id) throws IOException {// 復(fù)用ManagerBase, 獲取Session實(shí)例Session session = super.findSession(id);// OK, at this point, we're not sure if another thread is trying to// remove the session or not so the only way around this is to lock it// (or attempt to) and then try to get it by this session id again. If// the other code ran swapOut, then we should get a null back during// this run, and if not, we lock it out so we can access the session// safely.if(session != null) {synchronized(session){session = super.findSession(session.getIdInternal());if(session != null){// To keep any external calling code from messing up the// concurrency.session.access();session.endAccess();}}}if (session != null)return session;// See if the Session is in the Store// 如果內(nèi)存中找不到會話信息,從存儲中查找,這是主要的區(qū)別session = swapIn(id);return session;}// org.apache.catalina.session.PersistentManagerBase#swapIn/*** Look for a session in the Store and, if found, restore* it in the Manager's list of active sessions if appropriate.* The session will be removed from the Store after swapping* in, but will not be added to the active session list if it* is invalid or past its expiration.** @param id The id of the session that should be swapped in* @return restored session, or {@code null}, if none is found* @throws IOException an IO error occurred*/protected Session swapIn(String id) throws IOException {if (store == null)return null;Object swapInLock = null;/** The purpose of this sync and these locks is to make sure that a* session is only loaded once. It doesn't matter if the lock is removed* and then another thread enters this method and tries to load the same* session. That thread will re-create a swapIn lock for that session,* quickly find that the session is already in sessions, use it and* carry on.*/// 額,總之就是有點(diǎn)復(fù)雜synchronized (this) {swapInLock = sessionSwapInLocks.get(id);if (swapInLock == null) {swapInLock = new Object();sessionSwapInLocks.put(id, swapInLock);}}Session session = null;synchronized (swapInLock) {// First check to see if another thread has loaded the session into// the managersession = sessions.get(id);if (session == null) {Session currentSwapInSession = sessionToSwapIn.get();try {if (currentSwapInSession == null || !id.equals(currentSwapInSession.getId())) {// 從存儲中查找sessionsession = loadSessionFromStore(id);sessionToSwapIn.set(session);if (session != null && !session.isValid()) {log.error(sm.getString("persistentManager.swapInInvalid", id));session.expire();removeSession(id);session = null;}// 重新加入到內(nèi)存 sessions 中if (session != null) {reactivateLoadedSession(id, session);}}} finally {sessionToSwapIn.remove();}}}// Make sure the lock is removedsynchronized (this) {sessionSwapInLocks.remove(id);}return session;}private Session loadSessionFromStore(String id) throws IOException {try {if (SecurityUtil.isPackageProtectionEnabled()){return securedStoreLoad(id);} else {// 依賴于store的實(shí)現(xiàn)了,比如 file, jdbc...return store.load(id);}} catch (ClassNotFoundException e) {String msg = sm.getString("persistentManager.deserializeError", id);log.error(msg, e);throw new IllegalStateException(msg, e);}}// store 實(shí)現(xiàn)樣例: fileStore// org.apache.catalina.session.FileStore#load/*** Load and return the Session associated with the specified session* identifier from this Store, without removing it. If there is no* such stored Session, return <code>null</code>.** @param id Session identifier of the session to load** @exception ClassNotFoundException if a deserialization error occurs* @exception IOException if an input/output error occurs*/@Overridepublic Session load(String id) throws ClassNotFoundException, IOException {// Open an input stream to the specified pathname, if anyFile file = file(id);if (file == null) {return null;}if (!file.exists()) {return null;}Context context = getManager().getContext();Log contextLog = context.getLogger();if (contextLog.isDebugEnabled()) {contextLog.debug(sm.getString(getStoreName()+".loading", id, file.getAbsolutePath()));}ClassLoader oldThreadContextCL = context.bind(Globals.IS_SECURITY_ENABLED, null);try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());ObjectInputStream ois = getObjectInputStream(fis)) {StandardSession session = (StandardSession) manager.createEmptySession();session.readObjectData(ois);session.setManager(manager);return session;} catch (FileNotFoundException e) {if (contextLog.isDebugEnabled()) {contextLog.debug("No persisted data file found");}return null;} finally {context.unbind(Globals.IS_SECURITY_ENABLED, oldThreadContextCL);}}private void reactivateLoadedSession(String id, Session session) {if(log.isDebugEnabled())log.debug(sm.getString("persistentManager.swapIn", id));session.setManager(this);// make sure the listeners know about it.((StandardSession)session).tellNew();// 添加回sessionsadd(session);((StandardSession)session).activate();// endAccess() to ensure timeouts happen correctly.// access() to keep access count correct or it will end up// negativesession.access();session.endAccess();}// 3. session 的移除@Overridepublic void remove(Session session, boolean update) {super.remove (session, update);// 和內(nèi)存的實(shí)現(xiàn)差別就是,還要多一個(gè)對外部存儲的管理維護(hù)if (store != null){removeSession(session.getIdInternal());}}
可以看到, PersistentManager 的實(shí)現(xiàn)還是有點(diǎn)復(fù)雜的,主要是在安全性和性能之間的平衡,它和 StandardManager 基本是一種包含關(guān)系,即除了要維護(hù)內(nèi)存session外,還要維護(hù)外部存儲的狀態(tài)。
而現(xiàn)實(shí)情況是,既然已經(jīng)需要自行維護(hù)外部狀態(tài)了,為何還要去使用tomcat自帶的session管理呢?而如果站在框架session管理的設(shè)計(jì)者的角度,這可能也是無可奈何的事。
而在我們自己的session管理實(shí)現(xiàn)中,一般的思路還是相通的,創(chuàng)建 -> 查找 -> 維持 -> 刪除 。 可以基于數(shù)據(jù)庫,緩存,或者其他,而且相信也不是件難事。

騰訊、阿里、滴滴后臺面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學(xué)?那是因?yàn)槟銢]認(rèn)真看完這篇文章

關(guān)注作者微信公眾號 —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識以及最新面試寶典


看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動力
作者:等你歸去來
出處:https://www.cnblogs.com/yougewe/p/12902495.html
