spring-session剖析

1、使用場景java

1)一臺服務器上的軟負載均衡應用mysql

2)分佈式應用web

2、實現方式redis

1session數據存cookiespring

將session存儲至cookie中,每次請求從cookie中讀取session,缺點:不安全,大小有限制sql

2粘性sessionexpress

粘性session是指Ngnix每次都將同一用戶的全部請求轉發至同一臺服務器上,即將用戶與服務器綁定,缺點:某臺服務器不可用時,獲取不到session數據瀏覽器

3session複製安全

每次session發生變化時,建立或者修改,就廣播給全部集羣中的服務器,使全部的服務器上的session相同,缺點:副本數據都同樣,數據冗餘,佔用空間服務器

4session共享

使用redismysql等存儲session

3、使用配置

1)pom.xml引入jar包

2)web.xml配置filter

3)application.xml啓用spring-session

4、流程圖

步驟

1請求被filter過濾器攔截,其實是被SessionRepositoryFilter攔截器處理

2)生成requestresponse包裝類,後續操做中跟requestresponse相關的操做都是調用包裝類的方法

3)業務代碼中調用request.getSession()時,實際調用的是SessionRepositoryRequestWrapper類的方法

4SessionRepositoryRequestWrappergetSession()方法會獲取當前域中的cookie獲取sessionID

5根據sessionIDredis中查找與之對應的RedisSession對象

6)當無RedisSession返回時,建立RedisSession對象,以後調用setAttributegetAttribute方法時,分別是往對象中到map存放和獲取值

7)將RedisSession對象放入request中,供後續使用,如SessionRepositoryFilter$SessionRepositoryRequestWrapper. commitSession()

8)將數據保存至redis實際上保存到是RedisSession對象中的delta屬性,該屬性的數據類型爲Map對應的redis數據結構爲hash

9)將cookie寫入瀏覽器cookie包括sessionID

5、源碼解析

1)加載SessionRepositoryFilter過濾器

web.xml的過濾器爲DelegatingFilterProxy過濾器實現了InitializingBean接口,故會調用afterPropertiesSet()方法,最終對應的是SessionRepositoryFilter

爲何最後對應的filterSessionRepositoryFilter?因爲DelegatingFilterProxy類中的targetBeanName值爲springSessionRepositoryFilterinitDelegate()方法是在spring容器中找到idspringSessionRepositoryFilter的對象即爲filter的具體實現類。回到application.xml文件,該文件中有一行配置:

<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>

查看RedisHttpSessionConfiguration源碼發現其父類有個方法,方法中使用了@Bean註解,該註解相似<bean />,id默認爲方法名稱,方法參數默認依賴spring容器中id爲參數名稱的對象,故該代碼最後會往spring容器中注入SessionRepositoryFilter對象,id=springSessionRepositoryFilter注入的SessionRepositoryFilter對象且參數sessionRepository依賴spring容器中的id=sessionRepository對象,具體代碼以下:

@Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
    sessionRepositoryFilter.setServletContext(this.servletContext);
    if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) {
         sessionRepositoryFilter.setHttpSessionStrategy((MultiHttpSessionStrategy) this.httpSessionStrategy);
    }
    else {
        sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy);
    }
    return sessionRepositoryFilter;
}

2)加載RedisOperationsSessionRepository

application.xml中有以下配置,查看RedisHttpSessionConfiguration源碼,發現Spring容器中id=springSessionRepositoryFilter的類,即爲SessionRepositoryFilter且默認的session持久化容器爲redis

圖中的springSessionRepositoryFilter()方法表示初始化SessionRepositoryFilter對象,注入到spring容器中,且id=springSessionRepositoryFilter其中方法參數sessionRepository表示依賴spring容器中id=sessionRepository的對象。從圖中sessionRepository()方法可知,注入spring容器的id=sessionRepository的對象爲RedisOperationsSessionRepository即默認的session持久化到redis

3)生成request&response包裝對象

spring-session 的核心思想是對HttpServletRequestHttpServletResonse進行包裝,後續全部操做requestresponse的方法均調用包裝對象的方法,生成包裝對象是在SessionRepositoryFilter中進行,通過濾器處理後,controller方法中的HttpServletRequestHttpServletResponse對象均爲SessionRepositoryRequestWrapper和MultiSessionHttpServletResponse,具體代碼以下:

4)request.getSession()解析

1。獲取sessionID

當調用request.getSession()方法時,會從cookie中獲取sessionID代碼以下:

2。根據sessionID查找Session對象

獲取到sessionID且值不爲空,則須要到redis中查找與之對應的session對象

loadSession方法定義在RedisOperationsSessionRepository類中,目的是爲了將redis中鍵爲spring:session:sessions:{sessionId}hash結構的屬性值複製到MapSession,而生成的MapSession對象作爲RedisSession類的構造函數的參數,也就是說在RedisSession對象中保存了MapSession對象的引用,能夠直接操做RedisSession對象獲取redis保存的屬性值,具體代碼以下:

private MapSession loadSession(String id, Map<Object, Object> entries) {
    MapSession loaded = new MapSession(id);
    for (Map.Entry<Object, Object> entry : entries.entrySet()) {
        String key = (String) entry.getKey();
        if (CREATION_TIME_ATTR.equals(key)) {
            loaded.setCreationTime((Long) entry.getValue());
        } else if (MAX_INACTIVE_ATTR.equals(key)) {
           loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue());
        } else if (LAST_ACCESSED_ATTR.equals(key)) {
           loaded.setLastAccessedTime((Long) entry.getValue());
        } else if (key.startsWith(SESSION_ATTR_PREFIX)) {
           loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()),
           entry.getValue());
        }
    }
    return loaded;
}

3。建立Session對象

獲取不到sessionID或者值爲空時,則須要建立Session對象

當調用request.getSession.setAttribute(name,value)時,實際是往RedisSession對象中的delta屬性中設值,具體代碼在RedisOperationsSessionRepository中,以下:

public void setAttribute(String attributeName, Object attributeValue) {
    this.cached.setAttribute(attributeName, attributeValue);
    this.putAndFlush(getSessionAttrNameKey(attributeName), attributeValue);
}

private void putAndFlush(String a, Object v) {
    this.delta.put(a, v);
    this.flushImmediateIfNecessary();
}

4。Session對象保存至Redis

經過以上步驟獲取或建立Session對象後,以後就是將Session對象保存到redis中。而保存操做是在SessionRepositoryFilter類的doFilterInternal方法的finally中執行

五、定時任務清理過時key

雖然redis自帶了過時key的清理,但採用可是按期刪除+懶性刪除方式,若是併發量比較大的時候,redis會存在不少無效key,形成內容浪費。鑑於redis清理key方式的弊端,spring-session開啓了一個定時任務,定時清理redis中過時的key,其具體思路是取得當前時間的時間戳(精確到分)做爲 key,去 redis 中定位到 spring:session:expirations:{當前時間戳} ,這個 set 裏面存放的即是全部過時的 key 。具體實現以下:

org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanupExpiredSessions
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
   this.expirationPolicy.cleanExpiredSessions();
}
org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions
public void cleanExpiredSessions() {
   long now = System.currentTimeMillis();
   // 獲取當前時間戳對應的分
   long prevMin = roundDownMinute(now);
   if (logger.isDebugEnabled()) {
      logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
   }
   // 獲取到spring:session:expirations:{當前時間戳-精確到分}
   String expirationKey = getExpirationKey(prevMin);
   // 取出當前這一分鐘應當過時的session
   Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
   // 刪除spring:session:expirations:{當前時間戳-精確到分}鍵,不是刪除session自己
   this.redis.delete(expirationKey);
   // 遍歷這一分鐘要過時的session
   for (Object session : sessionsToExpire) {
      String sessionKey = getSessionKey((String) session);
      // 訪問session
      touch(sessionKey);
   }
}

private void touch(String key) {
   // 並非刪除key,而只是訪問key
   this.redis.hasKey(key);
}

6、客戶端禁用cookie

當瀏覽器禁用cookie後,是沒法獲取到cookie數據的,也就是說沒法獲取到jsessionID,獲取不到jessionID則沒法得到對應的session對象。爲了解決這個問題,能夠對URL重寫,使用response.encodeURL(url)便可。重寫URL的目的是在url後面加上jsessonID,這樣便能在請求中獲取到jsessionID,進一步得到對應的session對象。測試了一下,當集成spring-session後,使用response.encodeURL(url)重寫URL時,是不會在url後面加上jsessionID參數,這或許是設計spring-session時,就必需要求不能禁用cookie。

相關文章
相關標籤/搜索