长期以来,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管理带来了革新,使得如下的功能更加容易实现:github
须要说明的很重要的一点就是,Spring Session的核心项目并不依赖于Spring框架,因此,咱们甚至可以将其应用于不使用Spring框架的项目中。web
传统的JavaEE session管理会有各类问题,这刚好是Spring Session所要试图解决的。这些问题在下面以样例的形式进行了阐述。redis
在原生的云应用架构中,会假设应用可以进行扩展,这是经过在Linux容器中运行更多的应用程序实例实现的,这些容器会位于一个 大型的虚拟机池中。例如,咱们能够很容易地将一个“.war”文件部署到位于Cloud Foundry或Heroku的Tomcat中,而后在几秒钟的时间内就能扩展到100个应用实例,每一个实例能够具备1GB RAM。咱们还能够配置云平台,基于用户的需求自动增长和减小应用实例的数量。spring
在不少的应用服务器中,都会将HTTP session状态保存在JVM中,这个JVM与运行应用程序代码的JVM是同一个,由于这样易于实现,而且速度很快。当新的应用服务器实例加入或离开集 群时,HTTP session会基于现有的应用服务器实例进行从新平衡。在弹性的云环境中,咱们会拥有上百个应用服务器实例,而且实例的数量可能在任意时刻增长或减小, 这样的话,咱们就会遇到一些问题:数据库
所以,更为高效的办法是将HTTP session状态保存在独立的数据存储中,这个存储位于运行应用程序代码的JVM以外。例如,咱们能够将100个Tomcat实例配置为使用Redis 来存储session状态,当Tomcat实例增长或减小的时候,Redis中所存储的session并不会受到影响。同时,由于Redis是使用C语言 编写的,因此它可使用上百GB甚至TB级别的RAM,它不会涉及到垃圾收集的问题。apache
对于像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的方式是手动完成上述的每个配置步骤。
借助像Maven或Gradle这样的依赖管理器,将Spring Session添加应用中是很容易的。若是你使用Maven和Spring Boot的话,那么能够在pom.xml中使用以下的依赖:
<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 |
0 |
若是存在session与别名0关联的话,就返回该session,不然的话建立一个新的session并将其与别名0关联。 |
example.com/resource?_s=1 |
1 |
若是存在session与别名1关联的话,就返回该session,不然的话建立一个新的session并将其与别名1关联。 |
example.com/resource?_s=0 |
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。
Spring Session使用指南
Websocket / HttpSession超时的交互
Webinar播放地址:Introducing Spring Session
Adib Saikali是 Pivotal的高级现场工程师(Senior Field Engineer),对技术和创业充满热情,所工做的内容包括组装JavaScript代码,给风险资本家拨打不通过预定的电话等等不一而足。在过去的十 多年中,Adib一直使用Spring和Java构建解决方案,目前致力于帮助客户借助大数据、PaaS以及敏捷方法论的做用,构建优秀的产品和服务。你 能够经过twitter联系到Adib,他的帐号是@asaikali。
查看英文原文Next Generation Session Management with Spring Session