Spring Session的架構
Spring Session定義了一組標準的接口,可以通過實現這些接口間接訪問底層的數據存儲。Spring Session定義了如下核心接口:Session、ExpiringSession以及SessionRepository,針對不同的數據存儲,它們需要分別實現。
- org.springframework.session.Session接口定義了session的基本功能,如設置和移除屬性。這個接口并不關心底層技術,因此能夠比servlet HttpSession適用于更為廣泛的場景中。
- org.springframework.session.ExpiringSession擴展了Session接口,它提供了判斷session是否過期的屬性。RedisSession是這個接口的一個樣例實現。
- org.springframework.session.SessionRepository定義了創建、保存、刪除以及檢索session的方法。將Session實例真正保存到數據存儲的邏輯是在這個接口的實現中編碼完成的。例如,RedisOperationsSessionRepository就是這個接口的一個實現,它會在Redis中創建、存儲和刪除session。
在請求/響應周期中,客戶端和服務器之間需要協商同意一種傳遞session id的方式。例如,如果請求是通過HTTP傳遞進來的,那么session可以通過HTTP cookie或HTTP Header信息與請求進行關聯。
對于HTTP協議來說,Spring Session定義了HttpSessionStrategy接口以及兩個默認實現,即CookieHttpSessionStrategy和HeaderHttpSessionStrategy,其中前者使用HTTP cookie將請求與session id關聯,而后者使用HTTP header將請求與session關聯。
核心思想:
通過 org.springframework.session.web.http.SessionRepositoryFilter
的doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
對所有的請求進行攔截,使用包裝(Wrapper)或者說是裝飾(Decorator)模式對 request, response進行包裝并重寫HttpServletRequest 的 getSession方法,然后通過 filterChain向后傳遞。
** 本文基于 Spring Session 1.3.0.RELEASE 源代碼分析**
首先,看看我們在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>/*</url-pattern>
</filter-mapping>
DelegatingFilterProxy
DelegatingFilterProxy 顧名思義是一個Filter的代理類,其代碼如下:
public class DelegatingFilterProxy extends GenericFilterBean {
private WebApplicationContext webApplicationContext;
private String targetBeanName;
private boolean targetFilterLifecycle = false;
private volatile Filter delegate;
private final Object delegateMonitor = new Object();
public DelegatingFilterProxy() {
}
public DelegatingFilterProxy(Filter delegate) {
Assert.notNull(delegate, "delegate Filter object must not be null");
this.delegate = delegate;
}
public DelegatingFilterProxy(String targetBeanName) {
this(targetBeanName, null);
}
public DelegatingFilterProxy(String targetBeanName, WebApplicationContext wac) {
Assert.hasText(targetBeanName, "target Filter bean name must not be null or empty");
this.setTargetBeanName(targetBeanName);
this.webApplicationContext = wac;
if (wac != null) {
this.setEnvironment(wac.getEnvironment());
}
}
}
DelegatingFilterProxy 繼承自 GenericFilterBean,GenericFilterBean是一個抽象類,分別實現了 Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean接口,繼承關系如下圖:
GenericFilterBean 主要代碼如下:
public abstract class GenericFilterBean implements
Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws ServletException {
initFilterBean();
}
@Override
public final void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");
}
this.filterConfig = filterConfig;
// Set bean properties from init parameters.
try {
PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
String msg = "Failed to set bean properties on filter '" +
filterConfig.getFilterName() + "': " + ex.getMessage();
logger.error(msg, ex);
throw new NestedServletException(msg, ex);
}
// Let subclasses do whatever initialization they like.
initFilterBean();
if (logger.isDebugEnabled()) {
logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
}
}
protected void initFilterBean() throws ServletException {
}
}
由此可見,當DelegatingFilterProxy 在執行Filter的 init 方法時,會調用 initFilterBean方法,如下:
/**
* Spring容器啟動時初始化Filter
*/
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
// 如果targetBeanName為null,則使用當前Filter的名稱
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
// Fetch Spring root application context and initialize the delegate early,
// if possible. If the root application context will be started after this
// filter proxy, we'll have to resort to lazy initialization.
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
getFilterName方法繼承自GenericFilterBean ,如下:
public final FilterConfig getFilterConfig() {
return this.filterConfig;
}
protected final String getFilterName() {
return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
}
首先,會獲取targetBeanName 的值,這里會取當前Filter的名稱,也即我們在web.xml中配置的 <filter-name>
屬性值:springSessionRepositoryFilter,然后調用 initDelegate方法為 delegate賦值,initDelegate方法如下:
/**
* 初始化代理Filter
*/
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
//根據getTargetBeanName() 即 springSessionRepositoryFilter去WebApplicationContext查找bean
Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
if (isTargetFilterLifecycle()) {
//調用代理Filter的init方法
delegate.init(getFilterConfig());
}
return delegate;
}
這里 根據 springSessionRepositoryFilter去WebApplicationContext查找Bean,找到的Filter究竟是誰呢?
還記得我們在 applicationContext.xml中配置的 第一個bean嗎?
<!--spring session-->
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<property name="maxInactiveIntervalInSeconds" value="1800"></property>
</bean>
RedisHttpSessionConfiguration的繼承關系如下:
Spring Session為了減輕我們配置Bean 的負擔,在 RedisHttpSessionConfiguration以及它的父類 SpringHttpSessionConfiguration中 自動生成了許多Bean,我們看看 SpringHttpSessionConfiguration的源碼:
@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy();
private boolean usesSpringSessionRememberMeServices;
private ServletContext servletContext;
private CookieSerializer cookieSerializer;
private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy;
private List<HttpSessionListener> httpSessionListeners = new ArrayList<HttpSessionListener>();
/**
* 我們在web.xml配置的Filter名稱
*/
@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;
}
}
看到這里明白了吧,根據 springSessionRepositoryFilter從 WebApplicationContext取到的是 org.springframework.session.web.http.SessionRepositoryFilter
對象。
接下來,我們來看看 DelegatingFilterProxy 的doFilter方法:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
this.delegate = initDelegate(wac);
}
delegateToUse = this.delegate;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
/**
* 調用代理Filter
*/
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
DelegatingFilterProxy doFilter方法將每次請求都交給 delegate處理,即交給 org.springframework.session.web.http.SessionRepositoryFilter
進行處理。
SessionRepositoryFilter
Spring Session對HTTP的支持所依靠的是一個簡單老式的Servlet Filter,借助servlet規范中標準的特性來實現Spring Session的功能。SessionRepositoryFilter就是 Servlet Filter的一個標準實現,代碼如下:
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);
private final SessionRepository<S> sessionRepository;
private ServletContext servletContext;
private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
/**
* 構造方法
*/
public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
if (sessionRepository == null) {
throw new IllegalArgumentException("sessionRepository cannot be null");
}
this.sessionRepository = sessionRepository;
}
/**
* doFilter方法調用
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
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 {
filterChain.doFilter(strategyRequest, strategyResponse);
}
finally {
wrappedRequest.commitSession();
}
}
}
SessionRepositoryFilter 繼承自OncePerRequestFilter,也是一個標準的Servlet Filter。真正的核心在于它對請求的HttpServletRequest ,HttpServletResponse 對進行包裝了之后,然后調用 filterChain.doFilter(strategyRequest, strategyResponse); 往后傳遞,后面調用者通過 HttpServletRequest.getSession();獲得session的話,得到的將會是Spring Session 提供的 HttpServletSession實例。
其中,SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper是 SessionRepositoryFilter中的 內部類。
我們可以在Controller 中進行 debug 看看我們拿到的 HttpServletRequest 和 HttpSession 到底是什么?
@RestController
public class EchoController {
@RequestMapping(value = "/query", method = RequestMethod.GET)
public User query(String name, HttpServletRequest request, HttpSession session){
System.out.println(session);
User user = new User();
user.setId(15L);
user.setName(name);
user.setPassword("root");
user.setAge(28);
session.setAttribute("user", user);
return user;
}
}
在IDEA中 單步調試,結果如下:
接下來,我們分析一下 SessionRepositoryRequestWrapper 中關于 getSession()實現:
/**
* HttpServletRequest getSession()實現
*/
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
@Override
public HttpSessionWrapper getSession(boolean create) {
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//從當前請求獲取sessionId
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)"));
}
//為當前請求創建session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(System.currentTimeMillis());
//對Spring session 進行包裝(包裝成HttpSession)
currentSession = new HttpSessionWrapper(session, getServletContext());
setCurrentSession(currentSession);
return currentSession;
}
/**
* 根據sessionId獲取session
*/
private S getSession(String sessionId) {
S session = SessionRepositoryFilter.this.sessionRepository
.getSession(sessionId);
if (session == null) {
return null;
}
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
/**
* 從當前請求獲取sessionId
*/
@Override
public String getRequestedSessionId() {
return SessionRepositoryFilter.this.httpSessionStrategy
.getRequestedSessionId(this);
}
private void setCurrentSession(HttpSessionWrapper currentSession) {
if (currentSession == null) {
removeAttribute(CURRENT_SESSION_ATTR);
}
else {
setAttribute(CURRENT_SESSION_ATTR, currentSession);
}
}
/**
* 獲取當前請求session
*/
@SuppressWarnings("unchecked")
private HttpSessionWrapper getCurrentSession() {
return (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
}
注釋寫的很清楚,就不啰嗦了。