前景提要:
@EnableRedisHttpSession匯入RedisHttpSessionConfiguration.class
Ⅰ、被RedisHttpSessionConfiguration繼承的SpringHttpSessionConfiguration中新增了SessionRepositoryFilter(session過濾器);
Ⅱ、SessionRepositoryFilter建立時自動獲取到SessionRepository;
Ⅲ、SessionRepositoryFilter的doFilterInternal方法把原生的request和response被包裝成wrappedRequest和wrappedResponse,以後獲取session將不再通過原生的request.session()方法而是通過wrappedRequest.getsession(),wrappedRequest.getsession()方法,wrappedRequest.getsession()的session是從SessionRepository獲取得到的,做到從redis獲取session。
一:getSession流程:
SessionRepositoryFilter :負責用一個由SessionRepository支援的HttpSession實現包裝HttpServletRequest。
當請求進來後會被先進入過濾器進行過濾,SessionRepositoryFilter類的doFilterInternal方法就會生效(程式碼1-1):
public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class.getName(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 設定SessionRepository至Request的“會話儲存庫請求屬性名稱【SessionRepository】”屬性中。 request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); // 包裝原始HttpServletRequest響應至SessionRepositoryRequestWrapper SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response); // 包裝原始HttpServletResponse響應至SessionRepositoryResponseWrapper SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,response); try { //將封裝好後的request、response置入過濾鏈中 filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { wrappedRequest.commitSession(); } }
在將request、response封裝好後進入doFilter過濾鏈中,因為
request.getSession(false)方法【獲取但如果不存在不會進行建立】,這時候就會呼叫SessionRepositoryFilter類中的getSession(boolean create)方法(程式碼1-2):
@Override public HttpSessionWrapper getSession(boolean create) { //獲取當前會話中CURRENT_SESSION_ATTR的session屬性 HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } //獲取請求會話的session S requestedSession = getRequestedSession(); //如果請求會話session不為空 if (requestedSession != null) { //如果sessionId存在且當前request的attribute中的session未失效 if (getAttribute(INVALID_SESSION_ID_ATTR) == null) { //設定新的最後訪問時間 requestedSession.setLastAccessedTime(Instant.now()); //表示此sessionId的session屬性是有效的 this.requestedSessionIdValid = true; //將session和Servlet上下文環境置入生成currentSession currentSession = new HttpSessionWrapper(requestedSession, geServletContext()); //表示當前會話並不是第一次建立,防止同一個request頻繁訪問儲存器DB中獲取session(有點類似快取) currentSession.markNotNew(); //把當前的請求session置入,以便下一次 setCurrentSession(currentSession); return currentSession; } }
......
} @SuppressWarnings("unchecked") private HttpSessionWrapper getCurrentSession() { //獲取當前會話中CURRENT_SESSION_ATTR的屬性 return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR); } private void setCurrentSession(HttpSessionWrapper currentSession) { //如果當前會話不存在,則清除掉attribute中CURRENT_SESSION_ATTR的屬性 if (currentSession == null) { removeAttribute(CURRENT_SESSION_ATTR); } //如果會話存在,則設定CURRENT_SESSION_ATTR的屬性 else { setAttribute(CURRENT_SESSION_ATTR, currentSession); } } private S getRequestedSession() { //這是針對getRequestedSession()在處理請求期間被呼叫多次的情況的一種優化。它將立即返回找到的會話,而不是在會話儲存庫中再次查詢它以節省時間。 if (!this.requestedSessionCachedhed) { //如果requestedSessionCached為false,解析出當前的HttpServletRequest關聯的所有會話id。 List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this); for (String sessionId : sessionIds) { if (this.requestedSessionId == null) { this.requestedSessionId = sessionId; } //SessionRepository是管理Spring Session的模組,利用sessionId獲取session資訊 S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId); if (session != null) { this.requestedSession = session; this.requestedSessionId = sessionId; break; } } this.requestedSessionCached = true; } return this.requestedSession; }
在以上步驟執行完之後才會正式進入我們編寫的自定義攔截器中request.getSession().getAttribute(AuthServerConstant.LONG_USER):
在執行request.getSession()中會再次執行public HttpSessionWrapper getSession(boolean create)方法,因為上一步已經執行了setCurrentSession(HttpSessionWrapper currentSession)方法,即此時currentSession不再為空直接返回。
最後呼叫SessionRepositryFilter.commitSession將發生變更的seesion提交到redis中進行儲存,實現分散式會話(程式碼1-3):
//SessionRepositoryFilter類中的commitSession方法
private void commitSession() { //獲取當前封裝的wrappedSession HttpSessionWrapper wrappedSession = getCurrentSession(); if (wrappedSession == null) { if (isInvalidateClientSession()) { // 如果沒有session,並且已經被標記為失效時,指令客戶端結束當前會話。當會話無效時呼叫此方法,並應通知客戶端會話id不再有效. SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response); } } else { //獲取session S session = wrappedSession.getSession(); //將requestedSessionCached置為false,表明當前會話為第一次置入或者已經進行修改 clearRequestedSessionCache(); //呼叫RedisIndexedSessionRepository中的方法對進行的session在redis中進行儲存,儲存已更改的所有屬性,並更新此會話的到期時間。 SessionRepositoryFilter.this.sessionRepository.save(session); //獲取session唯一Id String sessionId = session.getId(); //判斷之前的session是否失效 或則 當前的sessionId與之前的sessionId不一致 if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) { //讓Cookie中的seesionId失效 SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId); } } }
二:setSession流程:
SpringSession在redis中的格式:
最開始步驟也是執行
//HttpSessionAdapte類中的方法和屬性 private static final Log logger = LogFactory.getLog(HttpSessionAdapter.class); private S session; private final ServletContext servletContext; private boolean invalidated; private boolean old; @Override public void setAttribute(String name, Object value) { checkState(); //根據傳過來的屬性名先在伺服器記憶體中儲存的session中獲取舊的屬性值 Object oldValue = this.session.getAttribute(name); //新置入伺服器內的session this.session.setAttribute(name, value); //如果新值與舊值不同表明session已經更新 if (value != oldValue) { if (oldValue instanceof HttpSessionBindingListener) { try { ((HttpSessionBindingListener) oldValue) .valueUnbound(new HttpSessionBindingEvent(this, name, oldValue)); } catch (Throwable th) { logger.error("Error invoking session binding event listener", th); } } if (value instanceof HttpSessionBindingListener) { try { ((HttpSessionBindingListener) value).valueBound(new HttpSessionBindingEvent(this, name, value)); } catch (Throwable th) { logger.error("Error invoking session binding event listener", th); } } } }
這裡重點講解黃色標記的this.session.setAttribute(name, value)方法,它呼叫的是RedisIndexedSessionRepository類中的setAttribute(String attributeName, Object attributeValue)方法(程式碼2-2):
//RedisIndexedSessionRepository類中方法 //使用建構函式設定session中基本引數 RedisSession(MapSession cached, boolean isNew) { this.cached = cached; this.isNew = isNew; this.originalSessionId = cached.getId(); Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this); this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME); //此處可以對比上面的截圖 if (this.isNew) { this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli()); this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) cached.getMaxInactiveInterval().getSeconds()); this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli()); //Ⅰ:如果seesion為新的,則將如上的引數置入名為delta的Map函式中 if (this.isNew || (RedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) { getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName), cached.getAttribute(attributeName))); } } static String getSessionAttrNameKey(String attributeName) { return RedisSessionMapper.ATTRIBUTE_PREFIX + attributeName; } public void setAttribute(String attributeName, Object attributeValue) { ///呼叫MapSeesion類中setAttribute(String attributeName, Object attributeValue)方法,也是一個Map型別。這裡之所以整一個cached是為了起到暫存的作用 this.cached.setAttribute(attributeName, attributeValue); //Ⅱ:將以sessionAttr:attributName為Map的key,attribute值為value以Map形式儲存在實體記憶體中
//getSessionAttrNameKey方法是將key格式拼接成sessionAttr:attributName this.delta.put(getSessionAttrNameKey(attributeName), attributeValue); flushImmediateIfNecessary(); } //MapSeesion類中 public void setAttribute(String attributeName, Object attributeValue) { if (attributeValue == null) { removeAttribute(attributeName); } else { //也是以Map方法將以attributName為Map的key,attribute值為value儲存在實體記憶體中 this.sessionAttrs.put(attributeName, attributeValue); } }
在此之後依然是呼叫SessionRepositryFilter.commitSession方法對seesion進行提交到redis進行分散式會話儲存,下面著重說以下提交流程:
在SessionRepositryFilter.commitSession中呼叫了RedisIndexedSessionRepository類中的save(session)方法(具體可以檢視程式碼1-3),關於此方法如下(程式碼2-3):
public void save(RedisSession session) { session.save(); //當seesion更新的時候 if (session.isNew) { //獲取會話通道,session.getId()獲取的是cached.Id String sessionCreatedKey = getSessionCreatedChannel(session.getId()); //根據Key找到通到釋出訊息,告訴訂閱者此sessionId更新了:delta此處為空, this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); //置為false下次無更新不需要再進行上面操作 session.isNew = false; } } private void save() { //儲存發生了變化的session saveChangeSessionId(); //儲存session saveDelta(); } private void saveDelta() { if (this.delta.isEmpty()) { return; } String sessionId = getId(); //將實體記憶體中以sessionAttr:attributName為Map的key,attribute值為value儲存入redis中,redis的儲存鍵格式為:spring:session:sessions:UUID getSessionBoundHashOperations(sessionId).putAll(this.delta); ...... //將delta置空 this.delta = new HashMap<>(this.delta.size()); } private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) { String key = getSessionKey(sessionId); return this.sessionRedisOperations.boundHashOps(key); } String getSessionKey(String sessionId) { // namespace=spring:session return this.namespace + "sessions:" + sessionId; }
至此整個SrpingSession執行原理到此結束,如果本人註釋有問題或則理解有偏差請在評論區指出。