Spring Session的架构
当实现session管理器的时候,有两个必须要解决的核心问题。首先,如何创建集群环境下高可用的session,要求能够可靠并高效地存储数据。其次,不管请求是HTTP、WebSocket、AMQP还是其他的协议,对于传入的请求该如何确定该用哪个session实例。实质上,关键问题在于:在发起请求的协议上,session id该如何进行传输?
Spring Session认为第一个问题,也就是在高可用可扩展的集群中存储数据已经通过各种数据存储方案得到了解决,如Redis、GemFire以及Apache Geode等等,因此,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。
Spring Session认为将请求与特定的session实例关联起来的问题是与协议相关的,因为在请求/响应周期中,客户端和服务器之间需要协商同意一种传递session id的方式。例如,如果请求是通过HTTP传递进来的,那么session可以通过HTTP cookie或HTTP Header信息与请求进行关联。如果使用HTTPS的话,那么可以借助SSL session id实现请求与session的关联。如果使用JMS的话,那么JMS的Header信息能够用来存储请求和响应之间的session id。
对于HTTP协议来说,Spring Session定义了HttpSessionStrategy
接口以及两个默认实现,即CookieHttpSessionStrategy
和HeaderHttpSessionStrategy
,其中前者使用HTTP cookie将请求与session id关联,而后者使用HTTP header将请求与session关联。
如下的章节详细阐述了Spring Session使用HTTP协议的细节。
在撰写本文的时候,在当前的Spring Session 1.0.2 GA发布版本中,包含了Spring Session使用Redis的实现,以及基于Map的实现,这个实现支持任意的分布式Map,如Hazelcast。让Spring Session支持某种数据存储是相当容易的,现在有支持各种数据存储的社区实现。
Spring Session对HTTP的支持
Spring Session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它应该是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequest
的getSession()
方法时,都会返回Spring Session的HttpSession
实例,而不是应用服务器默认的HttpSession。
如果要理解它的话,最简单的方式就是查看Spring Session实际所使用的源码。首先,我们了解一下标准servlet扩展点的一些背景知识,在实现Spring Session的时候会使用这些知识。
在2001年,Servlet 2.3规范引入了ServletRequestWrapper
。它的javadoc文档这样写道,ServletRequestWrapper
“提供了ServletRequest
接口的便利实现,开发人员如果希望将请求适配到Servlet的话,可以编写它的子类。这个类实现了包装(Wrapper)或者说是装饰(Decorator)模式。对方法的调用默认会通过包装的请求对象来执行”。如下的代码样例抽取自Tomcat,展现了ServletRequestWrapper是如何实现的。
public class ServletRequestWrapper implements ServletRequest {
private ServletRequest request;
/**
* 创建ServletRequest适配器,它包装了给定的请求对象。
* @throws java.lang.IllegalArgumentException if the request is null
*/
public ServletRequestWrapper(ServletRequest request) {
if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}
this.request = request;
}
public ServletRequest getRequest() {
return this.request;
}
public Object getAttribute(String name) {
return this.request.getAttribute(name);
}
// 为了保证可读性,其他的方法删减掉了
}
Servlet 2.3规范还定义了HttpServletRequestWrapper
,它是ServletRequestWrapper
的子类,能够快速提供HttpServletRequest
的自定义实现,如下的代码是从Tomcat抽取出来的,展现了HttpServletRequesWrapper
类是如何运行的。
public class HttpServletRequestWrapper extends ServletRequestWrapper
implements HttpServletRequest {
public HttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
private HttpServletRequest _getHttpServletRequest() {
return (HttpServletRequest) super.getRequest();
}
public HttpSession getSession(boolean create) {
return this._getHttpServletRequest().getSession(create);
}
public HttpSession getSession() {
return this._getHttpServletRequest().getSession();
}
// 为了保证可读性,其他的方法删减掉了
}
所以,借助这些包装类就能编写代码来扩展HttpServletRequest
,重载返回HttpSession
的方法,让它返回由外部存储所提供的实现。如下的代码是从Spring Session项目中提取出来的,但是我将原来的注释替换为我自己的注释,用来在本文中解释代码,所以在阅读下面的代码片段时,请留意注释。
/*
* 注意,Spring Session项目定义了扩展自
* 标准HttpServletRequestWrapper的类,用来重载
* HttpServletRequest中与session相关的方法。
*/
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
private HttpSessionWrapper currentSession;
private Boolean requestedSessionIdValid;
private boolean requestedSessionInvalidated;
private final HttpServletResponse response;
private final ServletContext servletContext;
/*
* 注意,这个构造器非常简单,它接受稍后会用到的参数,
* 并且委托给它所扩展的HttpServletRequestWrapper
*/
private SessionRepositoryRequestWrapper(
HttpServletRequest request,
HttpServletResponse response,
ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}
/*
* 在这里,Spring Session项目不再将调用委托给
* 应用服务器,而是实现自己的逻辑,
* 返回由外部数据存储作为支撑的HttpSession实例。
*
* 基本的实现是,先检查是不是已经有session了。如果有的话,
* 就将其返回,否则的话,它会检查当前的请求中是否有session id。
* 如果有的话,将会根据这个session id,从它的SessionRepository中加载session。
* 如果session repository中没有session,或者在当前请求中,
* 没有当前session id与请求关联的话,
* 那么它会创建一个新的session,并将其持久化到session repository中。
*/
@Override
public HttpSession getSession(boolean create) {
if(currentSession != null) {
return currentSession;
}
String requestedSessionId = getRequestedSessionId();
if(requestedSessionId != null) {
S session = sessionRepository.getSession(requestedSessionId);
if(session != null) {
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(session, getServletContext());
currentSession.setNew(false);
return currentSession;
}
}
if(!create) {
return null;
}
S session = sessionRepository.createSession();
currentSession = new HttpSessionWrapper(session, getServletContext());
return currentSession;
}
@Override
public HttpSession getSession() {
return getSession(true);
}
}
Spring Session定义了SessionRepositoryFilter
,它实现了 Servlet Filter
接口。我抽取了这个filter的关键部分,将其列在下面的代码片段中,我还添加了一些注释,用来在本文中阐述这些代码,所以,同样的,请阅读下面代码的注释部分。
/*
* SessionRepositoryFilter只是一个标准的ServletFilter,
* 它的实现扩展了一个helper基类。
*/
public class SessionRepositoryFilter < S extends ExpiringSession >
extends OncePerRequestFilter {
/*
* 这个方法是魔力真正发挥作用的地方。这个方法创建了
* 我们上文所述的封装请求对象和
* 一个封装的响应对象,然后调用其余的filter链。
* 这里,关键在于当这个filter后面的应用代码执行时,
* 如果要获得session的话,得到的将会是Spring Session的
* HttpServletSession实例,它是由后端的外部数据存储作为支撑的。
*/
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository);
SessionRepositoryRequestWrapper wrappedRequest =
new SessionRepositoryRequestWrapper(request,response,servletContext);
SessionRepositoryResponseWrapper wrappedResponse =
new SessionRepositoryResponseWrapper(wrappedRequest, response);
HttpServletRequest strategyRequest =
httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse);
HttpServletResponse strategyResponse =
httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse);
try {
filterChain.doFilter(strategyRequest, strategyResponse);
} finally {
wrappedRequest.commitSession();
}
}
}
我们从这一章节得到的关键信息是,Spring Session对HTTP的支持所依靠的是一个简单老式的ServletFilter
,借助servlet规范中标准的特性来实现Spring Session的功能。因此,我们能够让已有的war文件使用Spring Session的功能,而无需修改已有的代码