【轉載】spring-session負載均衡原理分析
註明轉載:https://www.jianshu.com/p/beaf18704c3c
第一部分:我會用循序漸進的方式來展示原始碼,從大家最熟悉的地方入手,而不是直接從系統啟動來debug原始碼。直接debug原始碼看到後來大家都會一頭霧水。 本文先從request.getSession()開始剖析原始碼,目標是讓讀者清楚的知曉Spring-session的產生過程。
第二部分:再上一部分Spring-session的產生過程的研究中如果讀者清楚了整個過程的脈絡,那麼肯定會產生一些疑惑:Servlet容器如何從預設的Session切換到Spring-session?為什麼request.getSession()會直接呼叫Spring的session管理方案?這一塊研究結束後整個Spring-session的大體原理分析就結束了。
剩下的就是其他一些策略的問題,篇幅有限,不再展開。讀者可以私下研究或者評論區域我們討論。比如
1.CookieHttpSessionStrategy和HeaderHttpSessionStrategy的區別
2.Session建立成功後儲存到session倉庫的具體過程?
...
那麼,先從第一部分開始
一. 提出問題假設
Spring-Session 的思路是替換Servlet容器提供的HttpSession。在web程式中通過呼叫方法 request.getSession()
生成session。Servlet容器裡面預設的request實現是HttpServletRequestWrapper
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中獲取Session
在controller中通過request.getSession()來獲取Session,下圖是此方法執行的過程。
image.png
@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等)
三.spring-session與session是如何做到無縫切換的
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的配置如下)
XML實現方式
<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
[email protected]204ee
。
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原始碼的解讀就這麼粗糙的結束了,一些狀態判斷性的原始碼沒有解讀。我相信只要讀者把主線業務整理明白了,其他方法小菜一碟。