配置文件
-
pom.xml引入spring-session
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
-
web.xml
<!-- 配置spring-session-->
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
-
Spring-session配置文件, 將該文件單獨(dú)導(dǎo)入Spring配置文件中
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 設(shè)置redis的配置-->
<bean id="redisHttpSessionConfiguration"
class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>
<bean id="defaultCookieSerializer"
class="org.springframework.session.web.http.DefaultCookieSerializer">
<property name="CookieName" value="SESSION_NAME" />
<property name="domainName" value="localhost" />
<property name="useHttpOnlyCookie" value="true" />
<property name="cookiePath" value="/" />
<property name="cookieMaxAge" value="31536000" />
</bean>
<bean id="jedisPoolConfig"
class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="20" />
<property name="maxIdle" value="20" />
<property name="minIdle" value="10" />
</bean>
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="127.0.0.1" />
<property name="port" value="6379" />
<property name="poolConfig" ref="jedisPoolConfig" />
</bean>
</beans>
-
示例
## User 必須實(shí)現(xiàn)Serializable, 否則session寫入不到redis里
@RequestMapping(value = "login.do", method = RequestMethod.GET)
@ResponseBody
public ServerResponse<User> login(String username, String password, HttpSession session) {
ServerResponse<User> serverResponse = iUserService.login(username,
password);
if (serverResponse.isSuccess()) {
session.setAttribute(Const.CURRENT_USER, serverResponse.getData());
// RedisPoolUntil.setEx(session.getId(), JsonUtil.obj2String
// (serverResponse.getData()), 60 * 30);
}
return serverResponse;
}
Spring Session探索
主要從以下兩個(gè)方面來(lái)說(shuō)spring-session:
- 特點(diǎn)
- 工作原理
一.特點(diǎn)
spring-session在無(wú)需綁定web容器的情況下提供對(duì)集群session的支持鼠证。并提供對(duì)以下情況的透明集成:
- HttpSession:容許替換web容器的HttpSession
- WebSocket:使用WebSocket通信時(shí),提供Session的活躍
- WebSession:容許以應(yīng)用中立的方式替換webflux的webSession
二.工作原理
再詳細(xì)閱讀源碼之前先來(lái)看張圖,介紹下spring-session中的核心模塊以及之間的交互疗认。
spring-session分為以下核心模塊:
- SessionRepositoryFilter:Servlet規(guī)范中Filter的實(shí)現(xiàn),用來(lái)切換HttpSession至Spring Session,包裝HttpServletRequest和HttpServletResponse
- HttpServerletRequest/HttpServletResponse/HttpSessionWrapper包裝器:包裝原有的HttpServletRequest、HttpServletResponse和Spring Session管宵,實(shí)現(xiàn)切換Session和透明繼承HttpSession的關(guān)鍵之所在
- Session:Spring Session模塊
- SessionRepository:管理Spring Session的模塊
- HttpSessionStrategy:映射HttpRequst和HttpResponse到Session的策略
1. SessionRepositoryFilter
SessionRepositoryFilter是一個(gè)Filter過(guò)濾器,符合Servlet的規(guī)范定義攀甚,用來(lái)修改包裝請(qǐng)求和響應(yīng)箩朴。這里負(fù)責(zé)包裝切換HttpSession至Spring Session的請(qǐng)求和響應(yīng)。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 設(shè)置SessionRepository至Request的屬性中
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
// 包裝原始HttpServletRequest至SessionRepositoryRequestWrapper
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
// 包裝原始HttpServletResponse響應(yīng)至SessionRepositoryResponseWrapper
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
// 設(shè)置當(dāng)前請(qǐng)求的HttpSessionStrategy策略
HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
// 設(shè)置當(dāng)前響應(yīng)的HttpSessionStrategy策略
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
// 提交session
wrappedRequest.commitSession();
}
}
以上是SessionRepositoryFilter的核心操作秋度,每個(gè)HttpRequest進(jìn)入隧饼,都會(huì)被該Filter包裝成切換Session的請(qǐng)求很響應(yīng)對(duì)象。
Tips:責(zé)任鏈模式
Filter是Servlet規(guī)范中的非常重要的組件静陈,在tomcat的實(shí)現(xiàn)中使用了責(zé)任鏈模式,將多個(gè)Filter組織成鏈?zhǔn)秸{(diào)用诞丽。Filter的作用就是在業(yè)務(wù)邏輯執(zhí)行前后對(duì)請(qǐng)求和響應(yīng)做修改配置鲸拥。配合HttpServletRequestWrapper和HttpServletResponseWrapper使用,可謂威力驚人僧免!
2. SessionRepositoryRequestWrapper
對(duì)于developers獲取HttpSession的api
HttpServletRequest request = ...;
HttpSession session = request.getSession(true);
在spring session中request的實(shí)際類型SessionRepositoryRequestWrapper刑赶。調(diào)用SessionRepositoryRequestWrapper的getSession方法會(huì)觸發(fā)創(chuàng)建spring session,而非web容器的HttpSession懂衩。
SessionRepositoryRequestWrapper用來(lái)包裝原始的HttpServletRequest實(shí)現(xiàn)HttpSession切換至Spring Session撞叨。是透明Spring Session透明集成HttpSession的關(guān)鍵金踪。
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class
.getName();
// 當(dāng)前請(qǐng)求sessionId有效
private Boolean requestedSessionIdValid;
// 當(dāng)前請(qǐng)求sessionId無(wú)效
private boolean requestedSessionInvalidated;
private final HttpServletResponse response;
private final ServletContext servletContext;
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
// 調(diào)用HttpServletRequestWrapper構(gòu)造方法,實(shí)現(xiàn)包裝
super(request);
this.response = response;
this.servletContext = servletContext;
}
}
SessionRepositoryRequestWrapper繼承Servlet規(guī)范中定義的包裝器HttpServletRequestWrapper牵敷。HttpServletRequestWrapper是Servlet規(guī)范api提供的用于擴(kuò)展HttpServletRequest的擴(kuò)張點(diǎn)——即裝飾器模式胡岔,可以通過(guò)重寫一些api達(dá)到功能點(diǎn)的增強(qiáng)和自定義。
Tips:裝飾器模式
裝飾器模式(包裝模式)是對(duì)功能增強(qiáng)的一種絕佳模式枷餐。實(shí)際利用的是面向?qū)ο蟮亩鄳B(tài)性實(shí)現(xiàn)擴(kuò)展靶瘸。Servlet規(guī)范中開放此HttpServletRequestWrapper接口,是讓developers自行擴(kuò)展實(shí)現(xiàn)毛肋。這種使用方式和jdk中的FilterInputStream/FilterInputStream如出一轍怨咪。
HttpServletRequestWrapper中持有一個(gè)HttpServletRequest對(duì)象,然后實(shí)現(xiàn)HttpServletRequest接口的所有方法润匙,所有方法實(shí)現(xiàn)中都是調(diào)用持有的HttpServletRequest對(duì)象的相應(yīng)的方法诗眨。繼承HttpServletRequestWrapper 可以對(duì)其重寫。SessionRepositoryRequestWrapper繼承HttpServletRequestWrapper孕讳,在構(gòu)造方法中將原有的HttpServletRequest通過(guò)調(diào)用super完成對(duì)HttpServletRequestWrapper中持有的HttpServletRequest初始化賦值匠楚,然后重寫和session相關(guān)的方法。這樣就保證SessionRepositoryRequestWrapper的其他方法調(diào)用都是使用原有的HttpServletRequest的數(shù)據(jù)卫病,只有session相關(guān)的是重寫的邏輯油啤。
Tips:
這里的設(shè)計(jì)是否很精妙!一切都多虧與Servlet規(guī)范設(shè)計(jì)的的巧妙绑翱痢益咬!
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
重寫HttpServletRequest的getSession()方法,調(diào)用有參數(shù)getSession(arg)方法帜平,默認(rèn)為true幽告,表示當(dāng)前reques沒有session時(shí)創(chuàng)建session。繼續(xù)看下有參數(shù)getSession(arg)的重寫邏輯.
@Override
public HttpSessionWrapper getSession(boolean create) {
// 從當(dāng)前請(qǐng)求的attribute中獲取session裆甩,如果有直接返回
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
// 獲取當(dāng)前request的sessionId冗锁,這里使用了HttpSessionStrategy
// 決定怎樣將Request映射至Session,默認(rèn)使用Cookie策略嗤栓,即從cookies中解析sessionId
String requestedSessionId = getRequestedSessionId();
// 請(qǐng)求的如果sessionId存在且當(dāng)前request的attribute中的沒有session失效屬性
// 則根據(jù)sessionId獲取spring session
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
S session = getSession(requestedSessionId);
// 如果spring session不為空冻河,則將spring session包裝成HttpSession并
// 設(shè)置到當(dāng)前Request的attribute中,防止同一個(gè)request getsession時(shí)頻繁的到存儲(chǔ)器
//中獲取session茉帅,提高性能
if (session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
// 如果根據(jù)sessionId叨叙,沒有獲取到session,則設(shè)置當(dāng)前request屬性堪澎,此sessionId無(wú)效
// 同一個(gè)請(qǐng)求中獲取session擂错,直接返回?zé)o效
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
}
// 判斷是否創(chuàng)建session
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
// 根據(jù)sessionRepository創(chuàng)建spring session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
// 設(shè)置session的最新訪問時(shí)間
session.setLastAccessedTime(System.currentTimeMillis());
// 包裝成HttpSession透明化集成
currentSession = new HttpSessionWrapper(session, getServletContext());
// 設(shè)置session至Requset的attribute中,提高同一個(gè)request訪問session的性能
setCurrentSession(currentSession);
return currentSession;
}
再來(lái)看下spring session的持久化樱蛤。上述SessionRepositoryFilter在包裝HttpServletRequest后钮呀,執(zhí)行FilterChain中使用finally保證請(qǐng)求的Session始終session會(huì)被提交剑鞍,此提交操作中將sesionId設(shè)置到response的head中并將session持久化至存儲(chǔ)器中。
持久化只持久spring session爽醋,并不是將spring session包裝后的HttpSession持久化蚁署,因?yàn)镠ttpSession不過(guò)是包裝器,持久化沒有意義子房。
/**
* Uses the HttpSessionStrategy to write the session id to the response and
* persist the Session.
*/
private void commitSession() {
// 獲取當(dāng)前session
HttpSessionWrapper wrappedSession = getCurrentSession();
// 如果當(dāng)前session為空形用,則刪除cookie中的相應(yīng)的sessionId
if (wrappedSession == null) {
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionStrategy
.onInvalidateSession(this, this.response);
}
}
else {
// 從HttpSession中獲取當(dāng)前spring session
S session = wrappedSession.getSession();
// 持久化spring session至存儲(chǔ)器
SessionRepositoryFilter.this.sessionRepository.save(session);
// 如果是新創(chuàng)建spring session,sessionId到response的cookie
if (!isRequestedSessionIdValid()
|| !session.getId().equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
this, this.response);
}
}
}
再來(lái)看下包裝的響應(yīng)SessionRepositoryResponseWrapper证杭。
3.SessionRepositoryResponseWrapper
/**
* Allows ensuring that the session is saved if the response is committed.
*
* @author Rob Winch
* @since 1.0
*/
private final class SessionRepositoryResponseWrapper
extends OnCommittedResponseWrapper {
private final SessionRepositoryRequestWrapper request;
/**
* Create a new {@link SessionRepositoryResponseWrapper}.
* @param request the request to be wrapped
* @param response the response to be wrapped
*/
SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
HttpServletResponse response) {
super(response);
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
}
this.request = request;
}
@Override
protected void onResponseCommitted() {
this.request.commitSession();
}
}
上面的注釋已經(jīng)非常詳細(xì)田度,這里不再贅述。這里只講述為什么需要包裝原始的響應(yīng)解愤。從注釋上可以看出包裝響應(yīng)時(shí)為了:確保如果響應(yīng)被提交session能夠被保存镇饺。
這里我有點(diǎn)疑惑:在上述的SessionRepositoryFilter.doFilterInternal方法中不是已經(jīng)request.commitSession()了嗎,F(xiàn)ilterChain執(zhí)行完或者異常后都會(huì)執(zhí)行Finally中的request.commitSession送讲。為什么這里仍然需要包裝響應(yīng)奸笤,為了確保session能夠保存,包裝器中的onResponseCommitted方法可以看出也是做了一次request.commitSession()哼鬓。難道這不是多此一舉监右?
Tips
如果有和我相同疑問的同學(xué),那就說(shuō)明我們的基礎(chǔ)都不扎實(shí)异希,對(duì)Servlet仍然沒有一個(gè)清楚全面的認(rèn)識(shí)健盒。對(duì)于此問題,我特意在github上提了issuse:Why is the request.commitSession() method called repeatedly?称簿。
但是在提完issue后的回家路上扣癣,我思考了下response可以有流方式的寫,會(huì)不會(huì)在response.getOutStream寫的時(shí)候已經(jīng)將響應(yīng)全部返回到客戶端憨降,這時(shí)響應(yīng)結(jié)束父虑。
在家中是,spring sesion作者大大已經(jīng)回復(fù)了我的issue:
Is this causing you problems? The reason is that we need to ensure that the session is created before the response is committed. If the response is already committed there will be no way to track the session (i.e. a cookie cannot be written to the response to keep track of which session id).
他的意思是:我們需要在response被提交之前確保session被創(chuàng)建授药。如果response已經(jīng)被提交士嚎,將沒有辦法追蹤session(例如:無(wú)法將cookie寫入response以跟蹤哪個(gè)session id)。
在此之前我又閱讀了JavaTM Servlet Specification悔叽,規(guī)范中這樣解釋Response的flushBuffer接口:
The isCommitted method returns a boolean value indicating whether any response bytes have been returned to the client. The flushBuffer method forces content in the buffer to be written to the client.
并且看了ServletResponse的flushBuffer的javadocs:
/**
* Forces any content in the buffer to be written to the client. A call to
* this method automatically commits the response, meaning the status code
* and headers will be written.
*
* @throws IOException if an I/O occurs during the flushing of the response
*
* @see #setBufferSize
* @see #getBufferSize
* @see #isCommitted
* @see #reset
*/
public void flushBuffer() throws IOException;
結(jié)合以上兩點(diǎn)航邢,一旦response執(zhí)行flushBuffer方法,迫使Response中在Buffer中任何數(shù)據(jù)都會(huì)被返回至client端骄蝇。這個(gè)方法自動(dòng)提交響應(yīng)中的status code和head。那么如果不包裝請(qǐng)求操骡,監(jiān)聽flushBuffer事件在提交response前九火,將session寫入response和持久化session赚窃,將導(dǎo)致作者大大說(shuō)的無(wú)法追蹤session。
SessionRepositoryResponseWrapper繼承父類OnCommittedResponseWrapper岔激,其中flushBuffer方法如下:
/**
* Makes sure {@link OnCommittedResponseWrapper#onResponseCommitted()} is invoked
* before calling the superclass <code>flushBuffer()</code>.
* @throws IOException if an input or output exception occurred
*/
@Override
public void flushBuffer() throws IOException {
doOnResponseCommitted();
super.flushBuffer();
}
/**
* Calls <code>onResponseCommmitted()</code> with the current contents as long as
* {@link #disableOnResponseCommitted()} was not invoked.
*/
private void doOnResponseCommitted() {
if (!this.disableOnCommitted) {
onResponseCommitted();
disableOnResponseCommitted();
}
}
重寫HttpServletResponse方法勒极,監(jiān)聽response commit,當(dāng)發(fā)生response commit時(shí)虑鼎,可以在commit之前寫session至response中并持久化session辱匿。
Tips:
spring mvc中HttpMessageConverters使用到的jackson即調(diào)用了outstream.flushBuffer(),當(dāng)使用@ResponseBody時(shí)炫彩。
以上做法固然合理匾七,但是如此重復(fù)操作兩次commit,存在兩次persist session?
這個(gè)問題后面涉及SessionRepository時(shí)再詳述江兢!
再看SessionRepository之前昨忆,先來(lái)看下spring session中的session接口。
3.Session接口
spring-session和tomcat中的Session的實(shí)現(xiàn)模式上有很大不同杉允,tomcat中直接對(duì)HttpSession接口進(jìn)行實(shí)現(xiàn)邑贴,而spring-session中則抽象出單獨(dú)的Session層接口,讓后再使用適配器模式將Session適配層Servlet規(guī)范中的HttpSession叔磷。spring-sesion中關(guān)于session的實(shí)現(xiàn)和適配整個(gè)UML類圖如下:
Tips:適配器模式
spring-session單獨(dú)抽象出Session層接口拢驾,可以應(yīng)對(duì)多種場(chǎng)景下不同的session的實(shí)現(xiàn),然后通過(guò)適配器模式將Session適配成HttpSession的接口改基,精妙至極繁疤!
Session是spring-session對(duì)session的抽象,主要是為了鑒定用戶寥裂,為Http請(qǐng)求和響應(yīng)提供上下文過(guò)程嵌洼,該Session可以被HttpSession、WebSocket Session封恰,非WebSession等使用麻养。定義了Session的基本行為:
- getId:獲取sessionId
- setAttribute:設(shè)置session屬性
- getAttribte:獲取session屬性
ExipringSession:提供Session額外的過(guò)期特性。定義了以下關(guān)于過(guò)期的行為:
- setLastAccessedTime:設(shè)置最近Session會(huì)話過(guò)程中最近的訪問時(shí)間
- getLastAccessedTime:獲取最近的訪問時(shí)間
- setMaxInactiveIntervalInSeconds:設(shè)置Session的最大閑置時(shí)間
- getMaxInactiveIntervalInSeconds:獲取最大閑置時(shí)間
- isExpired:判斷Session是否過(guò)期
MapSession:基于java.util.Map的ExpiringSession的實(shí)現(xiàn)
RedisSession:基于MapSession和Redis的ExpiringSession實(shí)現(xiàn)诺舔,提供Session的持久化能力
先來(lái)看下MapSession的代碼源碼片段
public final class MapSession implements ExpiringSession, Serializable {
/**
* Default {@link #setMaxInactiveIntervalInSeconds(int)} (30 minutes).
*/
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
private String id;
private Map<String, Object> sessionAttrs = new HashMap<String, Object>();
private long creationTime = System.currentTimeMillis();
private long lastAccessedTime = this.creationTime;
/**
* Defaults to 30 minutes.
*/
private int maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
MapSession中持有HashMap類型的變量sessionAtts用于存儲(chǔ)Session設(shè)置屬性鳖昌,比如調(diào)用的setAttribute方法的k-v就存儲(chǔ)在該HashMap中。這個(gè)和tomcat內(nèi)部實(shí)現(xiàn)HttpSession的方式類似低飒,tomcat中使用了ConcurrentHashMap存儲(chǔ)许昨。
其中l(wèi)astAccessedTime用于記錄最近的一次訪問時(shí)間,maxInactiveInterval用于記錄Session的最大閑置時(shí)間(過(guò)期時(shí)間-針對(duì)沒有Request活躍的情況下的最大時(shí)間褥赊,即相對(duì)于最近一次訪問后的最大閑置時(shí)間)糕档。
public void setAttribute(String attributeName, Object attributeValue) {
if (attributeValue == null) {
removeAttribute(attributeName);
}
else {
this.sessionAttrs.put(attributeName, attributeValue);
}
}
setAttribute方法極其簡(jiǎn)單,null時(shí)就移除attributeName拌喉,否則put存儲(chǔ)速那。
重點(diǎn)熟悉RedisSession如何實(shí)現(xiàn)Session的行為:setAttribute俐银、persistence等。
/**
* A custom implementation of {@link Session} that uses a {@link MapSession} as the
* basis for its mapping. It keeps track of any attributes that have changed. When
* {@link org.springframework.session.data.redis.RedisOperationsSessionRepository.RedisSession#saveDelta()}
* is invoked all the attributes that have been changed will be persisted.
*
* @author Rob Winch
* @since 1.0
*/
final class RedisSession implements ExpiringSession {
private final MapSession cached;
private Long originalLastAccessTime;
private Map<String, Object> delta = new HashMap<String, Object>();
private boolean isNew;
private String originalPrincipalName;
首先看javadocs端仰,對(duì)于閱讀源碼捶惜,學(xué)會(huì)看javadocs非常重要!
基于MapSession的基本映射實(shí)現(xiàn)的Session荔烧,能夠追蹤發(fā)生變化的所有屬性吱七,當(dāng)調(diào)用saveDelta方法后,變化的屬性將被持久化鹤竭!
在RedisSession中有兩個(gè)非常重要的成員屬性:
- cached:實(shí)際上是一個(gè)MapSession實(shí)例踊餐,用于做本地緩存,每次在getAttribute時(shí)無(wú)需從Redis中獲取诺擅,主要為了improve性能
- delta:用于跟蹤變化數(shù)據(jù)市袖,做持久化
再來(lái)看下RedisSession中最為重要的行為saveDelta——持久化Session至Redis中:
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
// 如果delta為空,則Session中沒有任何數(shù)據(jù)需要存儲(chǔ)
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
// 使用spring data redis將delta中的數(shù)據(jù)保存至Redis中
getSessionBoundHashOperations(sessionId).putAll(this.delta);
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(
SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey)
|| this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(
this.originalPrincipalName);
RedisOperationsSessionRepository.this.sessionRedisOperations
.boundSetOps(originalPrincipalRedisKey).remove(sessionId);
}
String principal = PRINCIPAL_NAME_RESOLVER.resolvePrincipal(this);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisOperationsSessionRepository.this.sessionRedisOperations
.boundSetOps(principalRedisKey).add(sessionId);
}
}
// 清空delta烁涌,代表沒有任何需要持久化的數(shù)據(jù)苍碟。同時(shí)保證
//SessionRepositoryFilter和SessionRepositoryResponseWrapper的onResponseCommitted
//只會(huì)持久化一次Session至Redis中,解決前面提到的疑問
this.delta = new HashMap<String, Object>(this.delta.size());
// 更新過(guò)期時(shí)間撮执,滾動(dòng)至下一個(gè)過(guò)期時(shí)間間隔的時(shí)刻
Long originalExpiration = this.originalLastAccessTime == null ? null
: this.originalLastAccessTime + TimeUnit.SECONDS
.toMillis(getMaxInactiveIntervalInSeconds());
RedisOperationsSessionRepository.this.expirationPolicy
.onExpirationUpdated(originalExpiration, this);
}
從javadoc中可以看出微峰,saveDelta用于存儲(chǔ)Session的屬性:
- 保存Session中的屬性數(shù)據(jù)至Redis中
- 清空delta中數(shù)據(jù),防止重復(fù)提交Session中的數(shù)據(jù)
- 更新過(guò)期時(shí)間至下一個(gè)過(guò)期時(shí)間間隔的時(shí)刻
再看下RedisSession中的其他行為
// 設(shè)置session的存活時(shí)間抒钱,即最大過(guò)期時(shí)間蜓肆。先保存至本地緩存,然后再保存至delta
public void setMaxInactiveIntervalInSeconds(int interval) {
this.cached.setMaxInactiveIntervalInSeconds(interval);
this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
flushImmediateIfNecessary();
}
// 直接從本地緩存獲取過(guò)期時(shí)間
public int getMaxInactiveIntervalInSeconds() {
return this.cached.getMaxInactiveIntervalInSeconds();
}
// 直接從本地緩存中獲取Session中的屬性
@SuppressWarnings("unchecked")
public Object getAttribute(String attributeName) {
return this.cached.getAttribute(attributeName);
}
// 保存Session屬性至本地緩存和delta中
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
除了MapSession和RedisSession還有JdbcSession谋币、MongoExpiringSession仗扬,感興趣的讀者可以自行閱讀。
下面看SessionRepository的邏輯蕾额。SessionRepository是spring session中用于管理spring session的核心組件早芭。
4. SessionRepository
A repository interface for managing {@link Session} instances.
javadoc中描述SessionRepository為管理spring-session的接口實(shí)例。抽象出:
S createSession();
void save(S session);
S getSession(String id);
void delete(String id);
創(chuàng)建诅蝶、保存退个、獲取、刪除Session的接口行為调炬。根據(jù)Session的不同语盈,分為很多種Session操作倉(cāng)庫(kù)。
這里重點(diǎn)介紹下RedisOperationsSessionRepository缰泡。在詳細(xì)介紹其之前刀荒,了解下RedisOperationsSessionRepository的數(shù)據(jù)存儲(chǔ)細(xì)節(jié)。
當(dāng)創(chuàng)建一個(gè)RedisSession,然后存儲(chǔ)在Redis中時(shí)缠借,RedisSession的存儲(chǔ)細(xì)節(jié)如下:
spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000
Redis會(huì)為每個(gè)RedisSession存儲(chǔ)三個(gè)k-v资溃。
- 第一個(gè)k-v用來(lái)存儲(chǔ)Session的詳細(xì)信息,包括Session的過(guò)期時(shí)間間隔烈炭、最近的訪問時(shí)間、attributes等等宝恶。這個(gè)k的過(guò)期時(shí)間為Session的最大過(guò)期時(shí)間 + 5分鐘符隙。如果默認(rèn)的最大過(guò)期時(shí)間為30分鐘,則這個(gè)k的過(guò)期時(shí)間為35分鐘
- 第二個(gè)k-v用來(lái)表示Session在Redis中的過(guò)期垫毙,這個(gè)k-v不存儲(chǔ)任何有用數(shù)據(jù)霹疫,只是表示Session過(guò)期而設(shè)置。這個(gè)k在Redis中的過(guò)期時(shí)間即為Session的過(guò)期時(shí)間間隔
- 第三個(gè)k-v存儲(chǔ)這個(gè)Session的id综芥,是一個(gè)Set類型的Redis數(shù)據(jù)結(jié)構(gòu)丽蝎。這個(gè)k中的最后的1439245080000值是一個(gè)時(shí)間戳,根據(jù)這個(gè)Session過(guò)期時(shí)刻滾動(dòng)至下一分鐘而計(jì)算得出膀藐。
這里不由好奇屠阻,為什么一個(gè)RedisSession卻如此復(fù)雜的存儲(chǔ)。關(guān)于這個(gè)可以參考spring-session作者本人在github上的兩篇回答:
Why does Spring Session use spring:session:expirations?
Clarify Redis expirations and cleanup task
簡(jiǎn)單描述下额各,為什么RedisSession的存儲(chǔ)用到了三個(gè)Key国觉,而非一個(gè)Redis過(guò)期Key。
對(duì)于Session的實(shí)現(xiàn)虾啦,需要支持HttpSessionEvent麻诀,即Session創(chuàng)建、過(guò)期傲醉、銷毀等事件蝇闭。當(dāng)應(yīng)用用監(jiān)聽器設(shè)置監(jiān)聽相應(yīng)事件,Session發(fā)生上述行為時(shí)硬毕,監(jiān)聽器能夠做出相應(yīng)的處理呻引。
Redis的強(qiáng)大之處在于支持KeySpace Notifiction——鍵空間通知。即可以監(jiān)視某個(gè)key的變化昭殉,如刪除苞七、更新、過(guò)期挪丢。當(dāng)key發(fā)生上述行為是蹂风,以便可以接受到變化的通知做出相應(yīng)的處理。具體詳情可以參考:
Redis Keyspace Notifications
但是Redis中帶有過(guò)期的key有兩種方式:
- 當(dāng)訪問時(shí)發(fā)現(xiàn)其過(guò)期
- Redis后臺(tái)逐步查找過(guò)期鍵
當(dāng)訪問時(shí)發(fā)現(xiàn)其過(guò)期乾蓬,會(huì)產(chǎn)生過(guò)期事件惠啄,但是無(wú)法保證key的過(guò)期時(shí)間抵達(dá)后立即生成過(guò)期事件。具體可以參考:Timing of expired events
spring-session為了能夠及時(shí)的產(chǎn)生Session的過(guò)期時(shí)的過(guò)期事件,所以增加了:
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000
spring-session中有個(gè)定時(shí)任務(wù)撵渡,每個(gè)整分鐘都會(huì)查詢相應(yīng)的spring:session:expirations:整分鐘的時(shí)間戳中的過(guò)期SessionId融柬,然后再訪問一次這個(gè)SessionId,即spring:session:sessions:expires:SessionId趋距,以便能夠讓Redis及時(shí)的產(chǎn)生key過(guò)期事件——即Session過(guò)期事件粒氧。
接下來(lái)再看下RedisOperationsSessionRepository中的具體實(shí)現(xiàn)原理
createSession方法:
public RedisSession createSession() {
// new一個(gè)RedisSession實(shí)例
RedisSession redisSession = new RedisSession();
// 如果設(shè)置的最大過(guò)期時(shí)間不為空,則設(shè)置RedisSession的過(guò)期時(shí)間
if (this.defaultMaxInactiveInterval != null) {
redisSession.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);
}
return redisSession;
}
再來(lái)看下RedisSession的構(gòu)造方法:
/**
* Creates a new instance ensuring to mark all of the new attributes to be
* persisted in the next save operation.
*/
RedisSession() {
// 設(shè)置本地緩存為MapSession
this(new MapSession());
// 設(shè)置Session的基本屬性
this.delta.put(CREATION_TIME_ATTR, getCreationTime());
this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
// 標(biāo)記Session的是否為新創(chuàng)建
this.isNew = true;
// 持久化
flushImmediateIfNecessary();
}
save方法:
public void save(RedisSession session) {
// 調(diào)用RedisSession的saveDelta持久化Session
session.saveDelta();
// 如果Session為新創(chuàng)建节腐,則發(fā)布一個(gè)Session創(chuàng)建的事件
if (session.isNew()) {
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
getSession方法:
// 根據(jù)SessionId獲取Session外盯,這里的false代表的參數(shù)
// 指:如果Session已經(jīng)過(guò)期,是否仍然獲取返回
public RedisSession getSession(String id) {
return getSession(id, false);
}
在有些情況下翼雀,Session過(guò)期饱苟,仍然需要能夠獲取到Session。這里先來(lái)看下getSession(String id, boolean allowExpired):
private RedisSession getSession(String id, boolean allowExpired) {
// 根據(jù)SessionId狼渊,從Redis獲取到持久化的Session信息
Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();
// 如果Redis中沒有箱熬,則返回null
if (entries.isEmpty()) {
return null;
}
// 根據(jù)Session信息,加載創(chuàng)建一個(gè)MapSession對(duì)象
MapSession loaded = loadSession(id, entries);
// 判斷是否允許過(guò)期獲取和Session是否過(guò)期
if (!allowExpired && loaded.isExpired()) {
return null;
}
// 根據(jù)MapSession new一個(gè)信息的RedisSession狈邑,此時(shí)isNew為false
RedisSession result = new RedisSession(loaded);
// 設(shè)置最新的訪問時(shí)間
result.originalLastAccessTime = loaded.getLastAccessedTime();
return result;
}
這里需要注意的是loaded.isExpired()和loadSession城须。loaded.isExpired判斷Session是否過(guò)期,如果過(guò)期返回null:
public boolean isExpired() {
// 根據(jù)當(dāng)前時(shí)間判斷是否過(guò)期
return isExpired(System.currentTimeMillis());
}
boolean isExpired(long now) {
// 如果maxInactiveInterval小于0官地,表示Session永不過(guò)期
if (this.maxInactiveInterval < 0) {
return false;
}
// 最大過(guò)期時(shí)間單位轉(zhuǎn)換為毫秒
// 當(dāng)前時(shí)間減去Session的最大有效期間隔以獲取理論上有效的上一次訪問時(shí)間
// 然后在與實(shí)際的上一次訪問時(shí)間進(jìn)行比較
// 如果大于酿傍,表示理論上的時(shí)間已經(jīng)在實(shí)際的訪問時(shí)間之后,那么表示Session已經(jīng)過(guò)期
return now - TimeUnit.SECONDS
.toMillis(this.maxInactiveInterval) >= this.lastAccessedTime;
}
loadSession中驱入,將Redis中存儲(chǔ)的Session信息轉(zhuǎn)換為MapSession對(duì)象赤炒,以便從Session中獲取屬性時(shí)能夠從內(nèi)存直接獲取提高性能:
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;
}
至此,可以看出spring-session中request.getSession(false)的過(guò)期實(shí)現(xiàn)原理亏较。
delete方法:
public void delete(String sessionId) {
// 獲取Session
RedisSession session = getSession(sessionId, true);
if (session == null) {
return;
}
cleanupPrincipalIndex(session);
// 從過(guò)期集合中移除sessionId
this.expirationPolicy.onDelete(session);
String expireKey = getExpiredKey(session.getId());
// 刪除session的過(guò)期鍵
this.sessionRedisOperations.delete(expireKey);
// 設(shè)置session過(guò)期
session.setMaxInactiveIntervalInSeconds(0);
save(session);
}
至此RedisOperationsSessionRepository的核心原理就介紹完畢莺褒。但是RedisOperationsSessionRepository中還包括關(guān)于Session事件的處理和清理Session的定時(shí)任務(wù)。這部分內(nèi)容在后述的SessionEvent部分介紹雪情。
5. HttpSessionStrategy
A strategy for mapping HTTP request and responses to a {@link Session}.
從javadoc中可以看出遵岩,HttpSessionStrategy是建立Request/Response和Session之間的映射關(guān)系的策略。
Tips:策略模式
策略模式是一個(gè)傳神的神奇模式巡通,是java的多態(tài)非常典型應(yīng)用尘执,是開閉原則、迪米特法則的具體體現(xiàn)宴凉。將同類型的一系列的算法封裝在不同的類中誊锭,通過(guò)使用接口注入不同類型的實(shí)現(xiàn),以達(dá)到的高擴(kuò)展的目的弥锄。一般是定義一個(gè)策略接口丧靡,按照不同的場(chǎng)景實(shí)現(xiàn)各自的策略蟆沫。
該策略接口中定義一套策略行為:
// 根據(jù)請(qǐng)求獲取SessionId,即建立請(qǐng)求至Session的映射關(guān)系
String getRequestedSessionId(HttpServletRequest request);
// 對(duì)于新創(chuàng)建的Session温治,通知客戶端
void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response);
// 對(duì)于session無(wú)效饭庞,通知客戶端
void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);
如下UML類圖:
這里主要介紹CookieHttpSessionStrategy,這個(gè)也是默認(rèn)的策略熬荆,可以查看spring-session中類SpringHttpSessionConfiguration舟山,在注冊(cè)SessionRepositoryFilter Bean時(shí)默認(rèn)采用CookieHttpSessionStrategy:
@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;
}
下面來(lái)分析CookieHttpSessionStrategy的原理。該策略使用Cookie來(lái)映射Request/Response至Session卤恳。即request/requset的head中cookie存儲(chǔ)SessionId捏顺,當(dāng)請(qǐng)求至web服務(wù)器,可以解析請(qǐng)求head中的cookie纬黎,然后獲取sessionId,根據(jù)sessionId獲取spring-session劫窒。當(dāng)創(chuàng)建新的session或者session過(guò)期本今,將相應(yīng)的sessionId寫入response的set-cookie或者從respose中移除sessionId。
getRequestedSessionId方法
public String getRequestedSessionId(HttpServletRequest request) {
// 獲取當(dāng)前請(qǐng)求的sessionId:session別名和sessionId映射
Map<String, String> sessionIds = getSessionIds(request);
// 獲取當(dāng)前請(qǐng)求的Session別名
String sessionAlias = getCurrentSessionAlias(request);
// 獲取相應(yīng)別名的sessionId
return sessionIds.get(sessionAlias);
}
接下來(lái)看下具體獲取SessionIds的具體過(guò)程:
public String getRequestedSessionId(HttpServletRequest request) {
// 獲取當(dāng)前請(qǐng)求的sessionId:session別名和sessionId映射
Map<String, String> sessionIds = getSessionIds(request);
// 獲取當(dāng)前請(qǐng)求的Session別名
String sessionAlias = getCurrentSessionAlias(request);
// 獲取相應(yīng)別名的sessionId
return sessionIds.get(sessionAlias);
}
public Map<String, String> getSessionIds(HttpServletRequest request) {
// 解析request中的cookie值
List<String> cookieValues = this.cookieSerializer.readCookieValues(request);
// 獲取sessionId
String sessionCookieValue = cookieValues.isEmpty() ? ""
: cookieValues.iterator().next();
Map<String, String> result = new LinkedHashMap<String, String>();
// 根據(jù)分詞器對(duì)sessionId進(jìn)行分割主巍,因?yàn)閟pring-session支持多session冠息。默認(rèn)情況只有一個(gè)session
StringTokenizer tokens = new StringTokenizer(sessionCookieValue, this.deserializationDelimiter);
// 如果只有一個(gè)session,則設(shè)置默認(rèn)別名為0
if (tokens.countTokens() == 1) {
result.put(DEFAULT_ALIAS, tokens.nextToken());
return result;
}
// 如果有多個(gè)session孕索,則建立別名和sessionId的映射
while (tokens.hasMoreTokens()) {
String alias = tokens.nextToken();
if (!tokens.hasMoreTokens()) {
break;
}
String id = tokens.nextToken();
result.put(alias, id);
}
return result;
}
public List<String> readCookieValues(HttpServletRequest request) {
// 獲取request的cookie
Cookie[] cookies = request.getCookies();
List<String> matchingCookieValues = new ArrayList<String>();
if (cookies != null) {
for (Cookie cookie : cookies) {
// 如果是以SESSION開頭逛艰,則表示是SessionId,畢竟cookie不只有sessionId搞旭,還有可能存儲(chǔ)其他內(nèi)容
if (this.cookieName.equals(cookie.getName())) {
// 決策是否需要base64 decode
String sessionId = this.useBase64Encoding
? base64Decode(cookie.getValue()) : cookie.getValue();
if (sessionId == null) {
continue;
}
if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
sessionId = sessionId.substring(0,
sessionId.length() - this.jvmRoute.length());
}
// 存入list中
matchingCookieValues.add(sessionId);
}
}
}
return matchingCookieValues;
}
再來(lái)看下獲取當(dāng)前request對(duì)應(yīng)的Session的別名方法getCurrentSessionAlias
public String getCurrentSessionAlias(HttpServletRequest request) {
// 如果session參數(shù)為空散怖,則返回默認(rèn)session別名
if (this.sessionParam == null) {
return DEFAULT_ALIAS;
}
// 從request中獲取session別名,如果為空則返回默認(rèn)別名
String u = request.getParameter(this.sessionParam);
if (u == null) {
return DEFAULT_ALIAS;
}
if (!ALIAS_PATTERN.matcher(u).matches()) {
return DEFAULT_ALIAS;
}
return u;
}
spring-session為了支持多session肄渗,才弄出多個(gè)session別名镇眷。當(dāng)時(shí)一般應(yīng)用場(chǎng)景都是一個(gè)session,都是默認(rèn)的session別名0翎嫡。
上述獲取sessionId和別名映射關(guān)系中欠动,也是默認(rèn)別名0。這里返回別名0惑申,所以返回當(dāng)前請(qǐng)求對(duì)應(yīng)的sessionId具伍。
onNewSession方法
public void onNewSession(Session session, HttpServletRequest request,
HttpServletResponse response) {
// 從當(dāng)前request中獲取已經(jīng)寫入Cookie的sessionId集合
Set<String> sessionIdsWritten = getSessionIdsWritten(request);
// 判斷是否包含,如果包含圈驼,表示該sessionId已經(jīng)寫入過(guò)cookie中人芽,則直接返回
if (sessionIdsWritten.contains(session.getId())) {
return;
}
// 如果沒有寫入棉姐,則加入集合蛇尚,后續(xù)再寫入
sessionIdsWritten.add(session.getId());
Map<String, String> sessionIds = getSessionIds(request);
String sessionAlias = getCurrentSessionAlias(request);
sessionIds.put(sessionAlias, session.getId());
// 獲取cookieValue
String cookieValue = createSessionCookieValue(sessionIds);
//將cookieValue寫入Cookie中
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, cookieValue));
}
sessionIdsWritten主要是用來(lái)記錄已經(jīng)寫入Cookie的SessionId,防止SessionId重復(fù)寫入Cookie中硕盹。
onInvalidateSession方法
public void onInvalidateSession(HttpServletRequest request,
HttpServletResponse response) {
// 從當(dāng)前request中獲取sessionId和別名映射
Map<String, String> sessionIds = getSessionIds(request);
// 獲取別名
String requestedAlias = getCurrentSessionAlias(request);
// 移除sessionId
sessionIds.remove(requestedAlias);
String cookieValue = createSessionCookieValue(sessionIds);
// 寫入移除后的sessionId
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, cookieValue));
}
繼續(xù)看下具體的寫入writeCookieValue原理:
public void writeCookieValue(CookieValue cookieValue) {
// 獲取request/respose和cookie值
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
String requestedCookieValue = cookieValue.getCookieValue();
String actualCookieValue = this.jvmRoute == null ? requestedCookieValue
: requestedCookieValue + this.jvmRoute;
// 構(gòu)造servlet規(guī)范中的Cookie對(duì)象,注意這里cookieName為:SESSION祈坠,表示為Session害碾,
// 上述的從Cookie中讀取SessionId,也是使用該cookieName
Cookie sessionCookie = new Cookie(this.cookieName, this.useBase64Encoding
? base64Encode(actualCookieValue) : actualCookieValue);
// 設(shè)置cookie的屬性:secure赦拘、path慌随、domain、httpOnly
sessionCookie.setSecure(isSecureCookie(request));
sessionCookie.setPath(getCookiePath(request));
String domainName = getDomainName(request);
if (domainName != null) {
sessionCookie.setDomain(domainName);
}
if (this.useHttpOnlyCookie) {
sessionCookie.setHttpOnly(true);
}
// 如果cookie值為空躺同,則失效
if ("".equals(requestedCookieValue)) {
sessionCookie.setMaxAge(0);
}
else {
sessionCookie.setMaxAge(this.cookieMaxAge);
}
// 寫入cookie到response中
response.addCookie(sessionCookie);
}
至此阁猜,CookieHttpSessionStrategy介紹結(jié)束。
由于篇幅過(guò)長(zhǎng)蹋艺,關(guān)于spring-session event和RedisOperationSessionRepository清理session并且產(chǎn)生過(guò)期事件的部分后續(xù)文章介紹剃袍。
總結(jié)
spring-session提供集群環(huán)境下HttpSession的透明集成。spring-session的優(yōu)勢(shì)在于開箱即用捎谨,具有較強(qiáng)的設(shè)計(jì)模式民效。且支持多種持久化方式,其中RedisSession較為成熟涛救,與spring-data-redis整合畏邢,可謂威力無(wú)窮。