spring-session簡介及實現(xiàn)原理源碼分析
一:spring-session介紹
1.簡介
session一直都是我們做集群時需要解決的一個難題,過去我們可以從serlvet容器上解決,比如開源servlet容器-tomcat提供的tomcat-redis-session-manager、memcached-session-manager。
或者通過nginx之類的負(fù)載均衡做ip_hash,路由到特定的服務(wù)器上..
但是這兩種辦法都存在弊端。
spring-session是spring旗下的一個項目,把servlet容器實現(xiàn)的httpSession替換為spring-session,專注于解決 session管理問題??珊唵慰焖偾覠o縫的集成到我們的應(yīng)用中。
2.支持功能
1)輕易把session存儲到第三方存儲容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多種存儲session的容器的方式。
2)同一個瀏覽器同一個網(wǎng)站,支持多個session問題。
3)RestfulAPI,不依賴于cookie??赏ㄟ^header來傳遞jessionID
4)WebSocket和spring-session結(jié)合,同步生命周期管理。
3.集成方式
集成方式非常簡單,直接看官網(wǎng)的samplesandguide。http://docs.spring.io/spring-session/docs/1.3.0.RELEASE/reference/html5/
主要分為以下幾個集成步驟:
1)引入依賴jar包
2)注解方式或者xml方式配置特定存儲容器的存儲方式,如redis的xml配置方式
<context:annotation-config/> /** 初始化一切spring-session準(zhǔn)備,且把springSessionFilter放入IOC **/ <beanclass="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/> /** 這是存儲容器的鏈接池 **/ <beanclass="org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory"/>
3)xml方式配置 web.xml ,配置 springSessionFilter到 filter chain中
<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>/*</url-pattern>
<dispatcher>REQUEST</dispatcher><dispatcher>ERROR</dispatcher>
</filter-mapping>
二:spring-session框架內(nèi)部剖析
1.框架高層抽象結(jié)構(gòu)圖

2.spring-session重寫servlet request 及 redis實現(xiàn)存儲相關(guān)問題
spring-session無縫替換應(yīng)用服務(wù)器的request大概原理是:
1.自定義個Filter,實現(xiàn)doFilter方法
2.繼承 HttpServletRequestWrapper 、HttpServletResponseWrapper 類,重寫getSession等相關(guān)方法(在這些方法里調(diào)用相關(guān)的 session存儲容器操作類)。
3.在 第一步的doFilter中,new 第二步 自定義的request和response的類。并把它們分別傳遞 到 過濾器鏈
4.把該filter配置到 過濾器鏈的第一個位置上
/** 這個類是spring-session的1.30源碼,也是實現(xiàn)上面第一到第三步的關(guān)鍵類 **/
public class SessionRepositoryFilter<S extends ExpiringSession>
extends OncePerRequestFilter {
/** session存儲容器接口,redis、mongoDB、genfire等數(shù)據(jù)庫都是實現(xiàn)該接口 **/
private final SessionRepository<S> sessionRepository;
private ServletContext servletContext;
/**
sessionID的傳遞方式接口。目前spring-session自帶兩個實現(xiàn)類
1.cookie方式 :CookieHttpSessionStrategy
2.http header 方式:HeaderHttpSessionStrategy
當(dāng)然,我們也可以自定義其他方式。
**/
private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
if (sessionRepository == null) {
throw new IllegalArgumentException("sessionRepository cannot be null");
}
this.sessionRepository = sessionRepository;
}
public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
if (httpSessionStrategy == null) {
throw new IllegalArgumentException("httpSessionStrategy cannot be null");
}
/**
通過前面的spring-session功能介紹,我們知道spring-session可以支持單瀏覽器多
session, 就是通過MultiHttpSessionStrategyAdapter來實現(xiàn)的。
每個瀏覽器擁有一個sessionID,但是這個sessionID擁有多個別名(根據(jù)瀏覽器的tab)。如:
別名1 sessionID
別名2 sessionID
...
而這個別名通過url來傳遞,這就是單瀏覽器多session原理了
**/
this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(
httpSessionStrategy);
}
public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
if (httpSessionStrategy == null) {
throw new IllegalArgumentException("httpSessionStrategy cannot be null");
}
this.httpSessionStrategy = httpSessionStrategy;
}
/**
該方法相當(dāng)于重寫了doFilter,只是spring-session又做了多一層封裝。
在這個方法里創(chuàng)建自定義的 request和response,然后傳遞到過濾器鏈filterChain
**/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
/**
spring-session重寫的ServletRequest。這個類繼承了HttpServletRequestWrapper
**/
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
request, response, this.servletContext);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
wrappedRequest, response);
HttpServletRequest strategyRequest = this.httpSessionStrategy
.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse = this.httpSessionStrategy
.wrapResponse(wrappedRequest, wrappedResponse);
try {
/**
傳遞自定義 request和response到鏈中,想象下如果
該spring-sessionFilter位于過濾器鏈的第一個,那么后續(xù)的Filter,
以及到達最后的控制層所獲取的 request和response,是不是就是我們自定義的了?
**/
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
wrappedRequest.commitSession();
}
}
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
這個就是Servlet response的重寫類了
*/
private final class SessionRepositoryResponseWrapper
extends OnCommittedResponseWrapper {
private final SessionRepositoryRequestWrapper request;
SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
HttpServletResponse response) {
super(response);
if (request == null) {
throw new IllegalArgumentException("request cannot be null");
}
this.request = request;
}
/**
這步是持久化session到存儲容器,我們可能會在一個控制層里多次調(diào)用session的操作方法
如果我們每次對session的操作都持久化到存儲容器,必定會帶來性能的影響。比如redis
所以我們可以在整個控制層執(zhí)行完畢了,response返回信息到瀏覽器時,才持久化session
**/
@Override
protected void onResponseCommitted() {
this.request.commitSession();
}
}
/**
spring-session 的request重寫類,這幾乎是最重要的一個重寫類。里面重寫了獲取getSession,Session等方法以及類
*/
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
private final HttpServletResponse response;
private final ServletContext servletContext;
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
/**
* Uses the HttpSessionStrategy to write the session id to the response and
* persist the Session.
*/
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
// session失效,刪除cookie或者header
if (isInvalidateClientSession()) {
SessionRepositoryFilter.this.httpSessionStrategy
.onInvalidateSession(this, this.response);
}
}
else {
S session = wrappedSession.getSession();
SessionRepositoryFilter.this.sessionRepository.save(session);
if (!isRequestedSessionIdValid()
|| !session.getId().equals(getRequestedSessionId())) {
// 把cookie或者header寫回給瀏覽器保存
SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
this, this.response);
}
}
}
@SuppressWarnings("unchecked")
private HttpSessionWrapper getCurrentSession() {
return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
}
private void setCurrentSession(HttpSessionWrapper currentSession) {
if (currentSession == null) {
removeAttribute(CURRENT_SESSION_ATTR);
}
else {
setAttribute(CURRENT_SESSION_ATTR, currentSession);
}
}
@SuppressWarnings("unused")
public String changeSessionId() {
HttpSession session = getSession(false);
if (session == null) {
throw new IllegalStateException(
"Cannot change session ID. There is no session associated with this request.");
}
// eagerly get session attributes in case implementation lazily loads them
Map<String, Object> attrs = new HashMap<String, Object>();
Enumeration<String> iAttrNames = session.getAttributeNames();
while (iAttrNames.hasMoreElements()) {
String attrName = iAttrNames.nextElement();
Object value = session.getAttribute(attrName);
attrs.put(attrName, value);
}
SessionRepositoryFilter.this.sessionRepository.delete(session.getId());
HttpSessionWrapper original = getCurrentSession();
setCurrentSession(null);
HttpSessionWrapper newSession = getSession();
original.setSession(newSession.getSession());
newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
for (Map.Entry<String, Object> attr : attrs.entrySet()) {
String attrName = attr.getKey();
Object attrValue = attr.getValue();
newSession.setAttribute(attrName, attrValue);
}
return newSession.getId();
}
// 判斷session是否有效
@Override
public boolean isRequestedSessionIdValid() {
if (this.requestedSessionIdValid == null) {
String sessionId = getRequestedSessionId();
S session = sessionId == null ? null : getSession(sessionId);
return isRequestedSessionIdValid(session);
}
return this.requestedSessionIdValid;
}
private boolean isRequestedSessionIdValid(S session) {
if (this.requestedSessionIdValid == null) {
this.requestedSessionIdValid = session != null;
}
return this.requestedSessionIdValid;
}
private boolean isInvalidateClientSession() {
return getCurrentSession() == null && this.requestedSessionInvalidated;
}
private S getSession(String sessionId) {
// 從session存儲容器中根據(jù)sessionID獲取session
S session = SessionRepositoryFilter.this.sessionRepository
.getSession(sessionId);
if (session == null) {
return null;
}
// 設(shè)置sesison的最后訪問時間,以防過期
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
/**
這個方法是不是很熟悉,下面還有個getSession()才更加熟悉。沒錯,就是在這里重新獲取session方法
**/
@Override
public HttpSessionWrapper getSession(boolean create) {
//快速獲取session,可以理解為一級緩存、二級緩存這種關(guān)系
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//從httpSessionStratge里面根據(jù)cookie或者header獲取sessionID
String requestedSessionId = getRequestedSessionId();
if (requestedSessionId != null
&& getAttribute(INVALID_SESSION_ID_ATTR) == null) {
//從存儲容器獲取session以及設(shè)置當(dāng)次初始化屬性
S session = getSession(requestedSessionId);
if (session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
setCurrentSession(currentSession);
return currentSession;
}
else {
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");
}
}
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)"));
}
// 如果該瀏覽器或者其他http訪問者是初次訪問服務(wù)器,則為他創(chuàng)建個新的session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(System.currentTimeMillis());
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
@Override
public ServletContext getServletContext() {
if (this.servletContext != null) {
return this.servletContext;
}
// Servlet 3.0+
return super.getServletContext();
}
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
@Override
public String getRequestedSessionId() {
return SessionRepositoryFilter.this.httpSessionStrategy
.getRequestedSessionId(this);
}
/**
HttpSession的重寫類
*/
private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {
HttpSessionWrapper(S session, ServletContext servletContext) {
super(session, servletContext);
}
@Override
public void invalidate() {
super.invalidate();
SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
setCurrentSession(null);
SessionRepositoryFilter.this.sessionRepository.delete(getId());
}
}
}
}
總結(jié)
以上就是本文關(guān)于spring-session簡介及實現(xiàn)原理源碼分析的全部內(nèi)容,希望對大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出!
- springboot中的springSession的存儲和獲取實現(xiàn)
- Springboot中登錄后關(guān)于cookie和session攔截問題的案例分析
- spring-redis-session 自定義 key 和過期時間
- SpringBoot2.x 整合Spring-Session實現(xiàn)Session共享功能
- Springboot實現(xiàn)多服務(wù)器session共享
- 解決前后端分離 vue+springboot 跨域 session+cookie失效問題
- 詳解SpringBoot2 使用Spring Session集群
- SpringCloud實現(xiàn)Redis在各個微服務(wù)的Session共享問題
- spring boot整合redis實現(xiàn)shiro的分布式session共享的方法
- 淺談Spring Session工作原理
相關(guān)文章
一文學(xué)會使用sa-token解決網(wǎng)站權(quán)限驗證
這篇文章主要為大家介紹了使用sa-token解決網(wǎng)站權(quán)限驗證方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07
解決SpringBoot項目啟動后網(wǎng)頁顯示Please sign in的問題
這篇文章主要介紹了解決SpringBoot項目啟動后網(wǎng)頁顯示Please sign in的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-04-04
java中StringBuffer的length()和capacity()方法對比
這篇文章主要介紹了java中StringBuffer的length()和capacity()方法對比,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
java實現(xiàn)pgsql自動更新創(chuàng)建時間與更新時間的兩種方式小結(jié)
本文主要介紹了java實現(xiàn)pgsql自動更新創(chuàng)建時間與更新時間的兩種方式小結(jié),主要包括通過數(shù)據(jù)庫自身實現(xiàn)以及通過mybatisplus的TableField注解添加,具有一定的參考價值,感興趣的可以了解一下2024-01-01
Spring Cloud Gateway全局異常處理的方法詳解
這篇文章主要給大家介紹了關(guān)于Spring Cloud Gateway全局異常處理的相關(guān)資料,需要的朋友可以參考下2018-10-10

