长期以来,session 管理就是企业级 Java 中的一部分,以至于咱们潜意识就认为它是已经解决的问题,在最近的记忆中,咱们没有看到这个领域有很大的革新。html
可是,现代的趋势是微服务以及可水平扩展的原生云应用(cloud native application),它们会挑战过去 20 多年来咱们设计和构建 session 管理器时的前提假设,而且暴露了现代化 session 管理器的不足。html5
本文将会阐述最近发布的 Spring Session API 如何帮助咱们克服眼下 session 管理方式中的一些不足,在企业级 Java 中,传统上都会采用这种旧的方式。咱们首先会简单阐述一下当前 session 管理中的问题,而后深刻介绍 Spring Session 是如何解决这些问题的。在文章的最后,将会详细展现 Spring Session 是如何运行的,以及在项目中怎样使用它。java
Spring Session 为企业级 Java 应用的 session 管理带来了革新,使得如下的功能更加容易实现:git
须要说明的很重要的一点就是,Spring Session 的核心项目并不依赖于 Spring 框架,因此,咱们甚至可以将其应用于不使用 Spring 框架的项目中。github
传统的 JavaEE session 管理会有各类问题,这刚好是 Spring Session 所要试图解决的。这些问题在下面以样例的形式进行了阐述。web
在原生的云应用架构中,会假设应用可以进行扩展,这是经过在 Linux 容器中运行更多的应用程序实例实现的,这些容器会位于一个大型的虚拟机池中。例如,咱们能够很容易地将一个“.war”文件部署到位于 Cloud Foundry 或 Heroku 的 Tomcat 中,而后在几秒钟的时间内就能扩展到 100 个应用实例,每一个实例能够具备 1GB RAM。咱们还能够配置云平台,基于用户的需求自动增长和减小应用实例的数量。redis
在不少的应用服务器中,都会将 HTTP session 状态保存在 JVM 中,这个 JVM 与运行应用程序代码的 JVM 是同一个,由于这样易于实现,而且速度很快。当新的应用服务器实例加入或离开集群时,HTTP session 会基于现有的应用服务器实例进行从新平衡。在弹性的云环境中,咱们会拥有上百个应用服务器实例,而且实例的数量可能在任意时刻增长或减小,这样的话,咱们就会遇到一些问题:spring
所以,更为高效的办法是将 HTTP session 状态保存在独立的数据存储中,这个存储位于运行应用程序代码的 JVM 以外。例如,咱们能够将 100 个 Tomcat 实例配置为使用 Redis 来存储 session 状态,当 Tomcat 实例增长或减小的时候,Redis 中所存储的 session 并不会受到影响。同时,由于 Redis 是使用 C 语言编写的,因此它可使用上百 GB 甚至 TB 级别的 RAM,它不会涉及到垃圾收集的问题。数据库
对于像 Tomcat 这样的开源服务器,很容易找到 session 管理器的替代方案,这些替代方案可使用外部的数据存储,如 Redis 或 Memcached。可是,这些配置过程可能会比较复杂,并且每种应用服务器都有所差异。对于闭源的产品,如 WebSphere 和 Weblogic,寻找它们的 session 管理器替代方案不只很是困难,在有些时候,甚至是没法实现的。后端
Spring Session 提供了一种独立于应用服务器的方案,这种方案可以在 Servlet 规范以内配置可插拔的 session 数据存储,不依赖于任何应用服务器的特定 API。这就意味着 Spring Session 可以用于实现了 servlet 规范的全部应用服务器之中(Tomcat、Jetty、 WebSphere、WebLogic、JBoss 等),它可以很是便利地在全部应用服务器中以彻底相同的方式进行配置。咱们还能够选择任意最适应需求的外部 session 数据存储。这使得 Spring Session 成为一个很理想的迁移工具,帮助咱们将传统的 JavaEE 应用转移到云中,使其成为知足 12-factor 的应用。
假设咱们在 example.com 上运行面向公众的 Web 应用,在这个应用中有些用户会建立多个帐号。例如,用户 Jeff Lebowski 可能会有两个帐户 thedude@example.com 和 lebowski@example.com。和其余 Java Web 应用同样,咱们会使用HttpSession
来跟踪应用的状态,如当前登陆的用户。因此,当用户但愿从 thedude@example.com 切换到 lebowski@example.com 时,他必需要首先退出,而后再从新登陆回来。
借助 Spring Session,为每一个用户配置多个 HTTP session 会很是容易,这样用户在 thedude@example.com 和 lebowski@example.com 之间切换的时候,就不须要退出和从新登陆了。
假设咱们正在构建的 Web 应用有一个复杂、自定义的权限功能,其中应用的 UI 会基于用户所授予的角色和权限实现自适应。
例如,假设应用有四个安全级别:public、confidential、secret 和 top secret。当用户登陆应用以后,系统会判断用户所具备的最高安全级别而且只会显示该级别和该级别之下的数据。因此,具备 public 权限的用户只能看到 public 级别的文档,具备 secret 权限的用户可以看到 public、confidential 和 secret 级别的文档,诸如此类。为了保证用户界面更加友好,应用程序应该容许用户预览在较低的安全级别条件下页面是什么样子的。例如,top secret 权限的用户可以将应用从 top secret 模式切换到 secret 模式,这样就能站在具备 secret 权限用户的视角上,查看应用是什么样子的。
典型的 Web 应用会将当前用户的标识及其角色保存在 HTTP session 中,但由于在 Web 应用中,每一个登陆的用户只能有一个 session,所以除了用户退出并从新登陆进来,咱们并无办法在角色之间进行切换,除非咱们为每一个用户自行实现多个 session 的功能。
借助 Spring Session,能够很容易地为每一个登陆用户建立多个 session,这些 session 之间是彻底独立的,所以实现上述的预览功能是很是容易的。例如,当前用户以 top secret 角色进行了登陆,那么应用能够建立一个新的 session,这个 session 的最高安全角色是 secret 而不是 top secret,这样的话,用户就能够在 secret 模式预览应用了。
假设用户登陆了 example.com 上的 Web 应用,那么他们可使用 HTML5 的 chat 客户端实现聊天的功能,这个客户端构建在 websocket 之上。按照 servlet 规范,经过 websocket 传入的请求并不能保持 HTTP session 处于活跃状态,因此当用户在聊天的过程当中,HTTP session 的倒数计时器会在不断地流逝。即使站在用户的立场上,他们一直在使用应用程序,HTTP session 最终也可能会出现过时。当 HTTP session 过时时,websocket 链接将会关闭。
借助 Spring Session,对于系统中的用户,咱们可以很容易地实现 websocket 请求和常规的 HTTP 请求都能保持 HTTP session 处于活跃状态。
假设咱们的应用提供了两种访问方式:一种使用基于 HTTP 的 REST API,而另外一种使用基于 RabbitMQ 的 AMQP 消息。执行消息处理代码的线程将没法访问应用服务器的 HttpSession,因此咱们必需要以一种自定义的方案来获取 HTTP session 中的数据,这要经过自定义的机制来实现。
经过使用 Spring Session,只要咱们可以知道 session 的 id,就能够在应用的任意线程中访问 Spring Session。所以,Spring Session 具有比 Servlet HTTP session 管理器更为丰富的 API,只要知道了 session id,咱们就能获取任意特定的 session。例如,在一个传入的消息中可能会包含用户 id 的 header 信息,借助它,咱们就能够直接获取 session 了。
咱们已经讨论了在传统的应用服务器中,HTTP session 管理存在不足的各类场景,接下来看一下 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 的支持是经过标准的 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 的功能,而无需修改已有的代码,固然若是你使用javax.servlet.http.HttpSessionListener
的话,就另当别论了。Spring Session 1.0 并不支持HttpSessionListener
,可是 Spring Session 1.1 M1 发布版本已经添加了对它的支持,你能够经过该地址了解更多细节信息。
在 Web 项目中配置 Spring Session 分为四步:
Spring Session 自带了对 Redis 的支持。搭建和安装 redis 的细节能够参考该地址。
有两种常见的方式可以完成上述的 Spring Session 配置步骤。第一种方式是使用 Spring Boot 来自动配置 Spring Session。第二种配置 Spring Session 的方式是手动完成上述的每个配置步骤。
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>1.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency>
其中,spring-boot-starter-redis
依赖可以确保使用 redis 所需的全部 jar 都会包含在应用中,因此它们能够借助 Spring Boot 进行自动装配。spring-session 依赖将会引入 Spring Session
的 jar。
至于 Spring Session Servlet filter 的配置,能够经过 Spring Boot 的自动配置来实现,这只须要在 Spring Boot 的配置类上使用 @EnableRedisHttpSession
注解就能够了,以下面的代码片断所示。
@SpringBootApplication @EnableRedisHttpSession public class ExampleApplication { public static void main(String[] args) { SpringApplication.run(ExampleApplication.class, args); } }
至于 Spring Session 到 Redis 链接的配置,能够添加以下配置到 Spring Boot 的 application.properties 文件中:
spring.redis.host=localhost spring.redis.password=secret spring.redis.port=6379
Spring Boot 提供了大量的基础设施用来配置到 Redis 的链接,定义到 Redis 数据库链接的各类方式均可以用在这里。你能够参考该地址的逐步操做指南,来了解如何使用Spring Session 和Spring Boot。
在传统的 web 应用中,能够参考该指南来了解如何经过web.xml 来使用Spring Session。
在传统的 war 文件中,能够参考该指南来了解如何不使用web.xml 进行配置。
默认状况下,Spring Session 会使用 HTTP cookie 来存储 session id,可是咱们也能够配置 Spring Session 使用自定义的 HTTP header 信息,如x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3
,当构建 REST API 的时候,这种方式是颇有用的。完整的指南能够参考该地址。
Spring Session 配置完成以后,咱们就可使用标准的 Servlet API 与之交互了。例如,以下的代码定义了一个 servlet,它使用标准的 Servlet session API 来访问 session。
@WebServlet("/example") public class Example extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 使用正常的 servlet API 获取 session,在底层, // session 是经过 Spring Session 获得的,而且会存储到 Redis 或 // 其余你所选择的数据源中 HttpSession session = request.getSession(); String value = session.getAttribute("someAttribute"); } }
Spring Session 会为每一个用户保留多个 session,这是经过使用名为“_s
”的 session 别名参数实现的。例如,若是到达的请求为 http://example.com/doSomething?_s=0 ,那么 Spring Session 将会读取“_s”参数的值,并经过它肯定这个请求所使用的是默认 session。
若是到达的请求是 http://example.com/doSomething?_s=1
的话,那么 Spring Session 就能知道这个请求所要使用的 session 别名为 1. 若是请求没有指定“_s
”参数的话,例如 http://example.com/doSomething,那么 Spring Session 将其视为使用默认的 session,也就是说_s=0
。
要为某个浏览器建立新的 session,只须要调用javax.servlet.http.HttpServletRequest.getSession()
就能够了,就像咱们一般所作的那样,Spring Session 将会返回正确的 session 或者按照标准 Servlet 规范的语义建立一个新的 session。下面的表格描述了针对同一个浏览器窗口,getSession()
面对不一样 url 时的行为。
HTTP 请求 URL |
Session 别名 |
getSession() 的行为 |
example.com/resource |
若是存在 session 与别名 0 关联的话,就返回该 session,不然的话建立一个新的 session 并将其与别名 0 关联。 |
|
example.com/resource?_s=1 |
1 |
若是存在 session 与别名 1 关联的话,就返回该 session,不然的话建立一个新的 session 并将其与别名 1 关联。 |
example.com/resource?_s=0 |
若是存在 session 与别名 0 关联的话,就返回该 session,不然的话建立一个新的 session 并将其与别名 0 关联。 |
|
example.com/resource?_s=abc |
abc |
若是存在 session 与别名 abc 关联的话,就返回该 session,不然的话建立一个新的 session 并将其与别名 abc 关联。 |
如上面的表格所示,session 别名不必定必须是整型,它只须要区别于其余分配给用户的 session 别名就能够了。可是,整型的 session 别名多是最易于使用的,Spring Session 提供了HttpSessionManager
接口,这个接口包含了一些使用 session 别名的工具方法。
咱们能够在HttpServletRequest
中,经过名为“org.springframework.session.web.http.HttpSessionManager”
的属性获取当前的HttpSessionManager
。以下的样例代码阐述了如何获得 HttpSessionManager,而且在样例注释中描述了其关键方法的行为。
@WebServlet("/example") public class Example extends HttpServlet { @Override protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException { /* * 在请求中,根据名为 org.springframework.session.web.http.HttpSessionManager 的 key * 得到 Spring Session session 管理器的引用 */ HttpSessionManager sessionManager=(HttpSessionManager)request.getAttribute( "org.springframework.session.web.http.HttpSessionManager"); /* * 使用 session 管理器找出所请求 session 的别名。 * 默认状况下,session 别名会包含在 url 中,而且请求参数的名称为“_s”。 * 例如,http://localhost:8080/example?_s=1 * 将会使以下的代码打印出“Requested Session Alias is: 1” */ String requestedSessionAlias=sessionManager.getCurrentSessionAlias(request); System.out.println("Requested Session Alias is: " + requestedSessionAlias); /* 返回一个惟一的 session 别名 id,这个别名目前没有被浏览器用来发送请求。 * 这个方法并不会建立新的 session, * 咱们须要调用 request.getSession() 来建立新 session。 */ String newSessionAlias = sessionManager.getNewSessionAlias(request); /* 使用新建立的 session 别名来创建 URL,这个 URL 将会包含 * “_s”参数。例如,若是 newSessionAlias 的值为 2 的话, * 那么以下的方法将会返回“/inbox?_s=2” */ String encodedURL = sessionManager.encodeURL("/inbox", newSessionAlias); System.out.println(encodedURL); /* 返回 session 别名与 session id 所组成的 Map, * 它们是由浏览器发送请求所造成的。 */ Map < String, String > sessionIds = sessionManager.getSessionIds(request); } }
Spring Session 为企业级 Java 的 session 管理带来了革新,使得以下的任务变得更加容易:
若是你想抛弃传统的重量级应用服务器,但受制于已经使用了这些应用服务器的 session 集群特性,那么 Spring Session 将是帮助你迈向更加轻量级容器的重要一步,这些轻量级的容器包括 Tomcat、Jetty 或 Undertow。