Spring Session原理解析

阿波羅的手發表於2022-03-20

前景提要:

@EnableRedisHttpSession匯入RedisHttpSessionConfiguration.class
Ⅰ、被RedisHttpSessionConfiguration繼承的SpringHttpSessionConfiguration中新增了SessionRepositoryFilter(session過濾器);
Ⅱ、SessionRepositoryFilter建立時自動獲取到SessionRepository;
Ⅲ、SessionRepositoryFilterdoFilterInternal方法把原生的request和response被包裝成wrappedRequestwrappedResponse,以後獲取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過濾鏈中,因為filterChain.doFilter(wrappedRequest, wrappedResponse)方法debugg流程太長,這裡只提主要功能:

①、獲取當前請求並進行解析為HttpMethod,根據請求方式(GET\POST\PUT\DELETE)分派給在對應的doMethod方法,;

②、當HandlerMethod處理完請求後就會觸發SessionFlashMapManager,從HTTP會話中檢索儲存的FlashMap例項(如果有的話)。FlashMap為一個請求提供了一種方法來儲存用於另一個請求的屬性。當從一個URL重定向到另一個URL時,這是最常見的需要——例如Post/Redirect/Get模式。一個FlashMap在重定向之前被儲存(通常在會話中),在重定向之後可用並立即刪除。

③、因為②的原因,服務端會獲取一次請求的session,這時候就會使用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中的格式:

最開始步驟也是執行filterChain.doFilter(wrappedRequest, wrappedResponse)方法,之後開始執行我們自己編寫的session.setAttribute(AuthServerConstant.LONG_USER,result);(程式碼2-1)

    //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執行原理到此結束,如果本人註釋有問題或則理解有偏差請在評論區指出。

相關文章