註明轉載:https://www.jianshu.com/p/beaf18704c3chtml
第一部分:我會用按部就班的方式來展現源碼,從你們最熟悉的地方入手,而不是直接從系統啓動來debug源碼。直接debug源碼看到後來你們都會一頭霧水。 本文先從request.getSession()開始剖析源碼,目標是讓讀者清楚的知曉Spring-session的產生過程。html5
第二部分:再上一部分Spring-session的產生過程的研究中若是讀者清楚了整個過程的脈絡,那麼確定會產生一些疑惑:Servlet容器如何從默認的Session切換到Spring-session?爲何request.getSession()會直接調用Spring的session管理方案?這一塊研究結束後整個Spring-session的大致原理分析就結束了。java
剩下的就是其餘一些策略的問題,篇幅有限,再也不展開。讀者能夠私下研究或者評論區域咱們討論。好比web
1.CookieHttpSessionStrategy和HeaderHttpSessionStrategy的區別 2.Session建立成功後存儲到session倉庫的具體過程? ...
那麼,先從第一部分開始redis
Spring-Session 的思路是替換Servlet容器提供的HttpSession。在web程序中經過調用方法 request.getSession()
生成session。Servlet容器裏面默認的request實現是HttpServletRequestWrapper
類。那麼爲了替換原始的HttpSession,Spring-Session有兩種方案來重寫getSession()方法 :spring
1.實現`HttpServletRequest`接口 2.繼承`HttpServletRequestWrapper`類
咱們從springmvc的controller進入request.getSession()方法,debug進去後發現getSession方法在這個類SessionRepositoryRequestWrapper
,而且這個類繼承了HttpServletRequestWrapper
。很開心有木有?驗證了咱們上面的想法Spring-Session用第2種繼承的方式來實現HttpSession的自定義。設計模式
/*IndexController.java*/ @Resource HttpServletRequest request; @RequestMapping({ "", "/index" }) public String index(Model model) { HttpSession session = request.getSession(); //方法debug跟蹤 Object user = session.getAttribute("curuser"); if(user == null) return "redirect:login"; model.addAttribute("port", request.getLocalPort()); return "index"; }
/*SessionRepositoryRequestWrapper.java*/ @Override public HttpSessionWrapper getSession() { return getSession(true); }
大概的思路瞭然,那麼getSession(true)究竟是如何運做的呢?getSession()這裏的業務也是最複雜的,存在各類狀態的判斷。開始研究getSession()。瀏覽器
在controller中經過request.getSession()來獲取Session,下圖是此方法執行的過程。tomcat
image.pngcookie
@Override public HttpSessionWrapper getSession(boolean create) { /* 從request中獲取Session,首次訪問返回null 其實這裏至關於request.getAttribute(key); 在Session建立成功後會調用request.setAttribute(key,session); 以便於在同一個request請求中直接獲取session */ HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } /* 從Cookie或者header中獲取SESSIONID,若是咱們用Cookie策略,這也是spring-session默認的。 能夠查看瀏覽器cookie。存在鍵值對 SESSION:XXXXXXXXXXXXXXXX */ String requestedSessionId = getRequestedSessionId(); if (requestedSessionId != null && getAttribute(INVALID_SESSION_ID_ATTR) == null) { /* 根據上文獲得的sessionid從Session倉庫中獲取Session */ S session = getSession(requestedSessionId); if (session != null) {//有效的Session this.requestedSessionIdValid = true; currentSession = new HttpSessionWrapper(session, getServletContext()); currentSession.setNew(false); setCurrentSession(currentSession); return currentSession; }else {//無效的session, if (SESSION_LOGGER.isDebugEnabled()) { SESSION_LOGGER.debug( "No session found by id: Caching result for getSession(false) for this HttpServletRequest."); } //Session無效,在request中增長一個鍵值對 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)")); } /* 首次訪問,則建立Session。 */ S session = SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(System.currentTimeMillis()); currentSession = new HttpSessionWrapper(session, getServletContext()); //將剛建立的session加入到request,以便於本次請求中再次getSession()時直接返回。 setCurrentSession(currentSession); return currentSession; }
至此,咱們在controller中獲取到了Session。能夠存取數據到Session裏面。在controller層response的時候把Session存儲到Session倉庫中(redis、mongo等)
web容器實現session共享的插件也有,好比tomcat-redis-session-manager等,缺點比較多:須要在tomcat作配置,侵入嚴重。
Spring-session用了一個比較聰明又簡單的辦法
1.自定義一個Filter ,springSessionRepositoryFilter,攔截全部請求 2.繼承HttpServletRequestWrapper等類,重寫getSession()等方法。
這裏咱們看看Spring官方文檔
we can create our Spring configuration. The Spring configuration is responsible for creating a Servlet Filter that replaces the HttpSession implementation with an implementation backed by Spring Session. Add the following Spring Configuration:
(咱們能夠建立一個Spring 的配置,這個文件是用來建立一個Filter,這個Filter裏面能夠實現Spring session替換HttpSession的功能。Spring的配置以下)
<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>
DelegatingFilterProxy這個類攔截每次請求,而且尋找到springSessionRepositoryFilter這個bean,而且將它轉換成Filter,用這個Filter處理每一個request請求。
獲取springSessionRepositoryFilter這個bean。
Object obj = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()).getBean("springSessionRepositoryFilter");
debug查看對象obj ,沒錯這就是spring-session最核心的Filter ——SessionReponsitoryFilter
org.springframework.session.web.http.SessionRepositoryFilter@228204ee
。
spring-session重寫的request(SessionRepositoryRequestWrapper),response(SessionRepositoryResponseWrapper)和Session(HttpSessionWrapper)都是SessionReponsitoryFilter類的內部類。第一部分着重說的getSession(boolean)方法就是在SessionRepositoryRequestWrapper這個類裏面重寫的。
//@EnableRedisHttpSession這個註解建立了springSessionRepositoryFilter的Bean。 //而且建立了一個操做Redis的RedisConnectionFactory工廠類 @EnableRedisHttpSession public class Config { @Bean public LettuceConnectionFactory connectionFactory() { return new LettuceConnectionFactory(); } }
上面Config建立了Filter,接下來須要將這個Config加載到Spring。以此來實現每次請求過來首先通過這個Filter。
public class Initializer extends AbstractHttpSessionApplicationInitializer { public Initializer() { super(Config.class); } }
那麼上面兩種配置方式裏的這個SessionReponsitoryFilter究竟是啥樣的?這個Filter纔是Spring-session的核心。咱們來看看
SessionReponsitoryFilter 源代碼
@Order(SessionRepositoryFilter.DEFAULT_ORDER) public class SessionRepositoryFilter<S extends ExpiringSession> extends OncePerRequestFilter { private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class .getName().concat(".SESSION_LOGGER"); private static final Log SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME); /** * The session repository request attribute name. */ public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class .getName(); /** * Invalid session id (not backed by the session repository) request attribute name. */ public static final String INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR + ".invalidSessionId"; private static final String CURRENT_SESSION_ATTR = SESSION_REPOSITORY_ATTR + ".CURRENT_SESSION"; /** * The default filter order. */ public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50; private final SessionRepository<S> sessionRepository; private ServletContext servletContext; private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy(); /** * Creates a new instance. * * @param sessionRepository the <code>SessionRepository</code> to use. Cannot be null. */ public SessionRepositoryFilter(SessionRepository<S> sessionRepository) { if (sessionRepository == null) { throw new IllegalArgumentException("sessionRepository cannot be null"); } this.sessionRepository = sessionRepository; } /** * Sets the {@link HttpSessionStrategy} to be used. The default is a * {@link CookieHttpSessionStrategy}. * * @param httpSessionStrategy the {@link HttpSessionStrategy} to use. Cannot be null. 設置HttpSessionStrategy的策略,默認策略是CookieHttpSessionStrategy。表示從cookie中獲取sessionid。 */ public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) { if (httpSessionStrategy == null) { throw new IllegalArgumentException("httpSessionStrategy cannot be null"); } this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter( httpSessionStrategy); } /** * Sets the {@link MultiHttpSessionStrategy} to be used. The default is a * {@link CookieHttpSessionStrategy}. * * @param httpSessionStrategy the {@link MultiHttpSessionStrategy} to use. Cannot be * null. */ public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) { if (httpSessionStrategy == null) { throw new IllegalArgumentException("httpSessionStrategy cannot be null"); } this.httpSessionStrategy = httpSessionStrategy; } /** 這個方法是典型的模板方法設計模式的運用;SessionRepositoryFilter的父類定義了抽象方法doFilterInternal,而且在doFilter中調用,具體的實現丟給子類。 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository); //封裝request和response SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper( wrappedRequest, response); //這裏的做用是經過方法request.setAttribute(HttpSessionManager.class.getName(), 策略); //把CookieHttpSessionStrategy加入到request。下面的response同樣 HttpServletRequest strategyRequest = this.httpSessionStrategy .wrapRequest(wrappedRequest, wrappedResponse); HttpServletResponse strategyResponse = this.httpSessionStrategy .wrapResponse(wrappedRequest, wrappedResponse); try { filterChain.doFilter(strategyRequest, strategyResponse); } finally { //這裏是response的時候把session加入到session倉庫(redis,MongoDB等),該方法在下面的SessionRepositoryRequestWrapper類 wrappedRequest.commitSession(); } } public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } /** * 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(); } } /** * A {@link javax.servlet.http.HttpServletRequest} that retrieves the * {@link javax.servlet.http.HttpSession} using a * {@link org.springframework.session.SessionRepository}. * * @author Rob Winch * @since 1.0 */ 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. * 將session加入到session倉庫(redis,MongoDB等 */ private void commitSession() { HttpSessionWrapper wrappedSession = getCurrentSession(); if (wrappedSession == null) { 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())) { SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session, this, this.response); } } } //從當前request中獲取session @SuppressWarnings("unchecked") private HttpSessionWrapper getCurrentSession() { return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR); } //將session存儲到當前request請求中 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(); } @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) { S session = SessionRepositoryFilter.this.sessionRepository .getSession(sessionId); if (session == null) { return null; } session.setLastAccessedTime(System.currentTimeMillis()); return session; } @Override public HttpSessionWrapper getSession(boolean create) { HttpSessionWrapper currentSession = getCurrentSession(); if (currentSession != null) { return currentSession; } String requestedSessionId = getRequestedSessionId(); if (requestedSessionId != null && getAttribute(INVALID_SESSION_ID_ATTR) == null) { S session = getSession(requestedSessionId); if (session != null) { this.requestedSessionIdValid = true; currentSession = new HttpSessionWrapper(session, getServletContext()); currentSession.setNew(false); setCurrentSession(currentSession); return currentSession; } 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"); } } 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)")); } 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); } //從session策略中獲取sessionid @Override public String getRequestedSessionId() { return SessionRepositoryFilter.this.httpSessionStrategy .getRequestedSessionId(this); } /** * Allows creating an HttpSession from a Session instance. * * @author Rob Winch * @since 1.0 */ 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()); } } } /** * A delegating implementation of {@link MultiHttpSessionStrategy}. */ static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy { private HttpSessionStrategy delegate; /** * Create a new {@link MultiHttpSessionStrategyAdapter} instance. * @param delegate the delegate HTTP session strategy */ MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) { this.delegate = delegate; } public String getRequestedSessionId(HttpServletRequest request) { return this.delegate.getRequestedSessionId(request); } public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) { this.delegate.onNewSession(session, request, response); } public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) { this.delegate.onInvalidateSession(request, response); } public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) { return request; } public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) { return response; } } }
spring-session源碼的解讀就這麼粗糙的結束了,一些狀態判斷性的源碼沒有解讀。我相信只要讀者把主線業務整理明白了,其餘方法小菜一碟。