spring-session之一:简介、使用及实现原理

一、背景

http session(企业)一直都是我们做集群时需要解决的一个难题,我们知道HttpSession是通过Servlet容器创建和管理的,像Tomcat/Jetty都是保存在内存中的。而如果我们把web服务器搭建成分布式的集群,然后利用LVS或Nginx做负载均衡,那么来自同一用户的Http请求将有可能被分发到两个不同的web站点中去。那么问题就来了,如何保证不同的web站点能够共享同一份session数据呢?
最简单的想法就是把session数据保存到内存以外的一个统一的地方,例如Memcached/Redis等数据库中。那么问题又来了,如何替换掉Servlet容器创建和管理HttpSession的实现呢?
(1)设计一个Filter,利用HttpServletRequestWrapper,实现自己的 getSession()方法,接管创建和管理Session数据的工作。spring-session就是通过这样的思路实现的。
(2)利用Servlet容器提供的插件功能,自定义HttpSession的创建和管理策略,并通过配置的方式替换掉默认的策略。不过这种方式有个缺点,就是需要耦合Tomcat/Jetty等Servlet容器的代码。这方面其实早就有开源项目了,例如memcached-session-manager,以及tomcat-redis-session-manager。暂时都只支持Tomcat6/Tomcat7。
(3)或者通过nginx之类的负载均衡做ip_hash,路由到特定的服务器上。 此策略会出现单点故障问题。

二、spring-session简介 

Spring Session是Spring的项目之一,GitHub地址:https://github.com/spring-projects/spring-session。Spring Session把servlet容器实现的httpSession替换为spring-session,专注于解决session管理问题。Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。

spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。

三、支持功能 

1)轻易把session存储到第三方存储容器,框架提供了redis、jvm的map、mongo、gemfire、hazelcast、jdbc等多种存储session的容器的方式。这样可以独立于应用服务器的方式提供高质量的集群。
2)同一个浏览器同一个网站,支持多个session问题。 从而能够很容易地构建更加丰富的终端用户体验。
3)Restful API,不依赖于cookie。可通过header来传递jessionID 。控制session id如何在客户端和服务器之间进行交换,这样的话就能很容易地编写Restful API,因为它可以从HTTP 头信息中获取session id,而不必再依赖于cookie。
4)WebSocket和spring-session结合,同步生命周期管理。当用户使用WebSocket发送请求的时候,能够保持HttpSession处于活跃状态。
5)在非Web请求的处理代码中,能够访问session数据,比如在JMS消息的处理代码中。

  需要说明的很重要的一点就是,Spring Session的核心项目并不依赖于Spring框架,所以,我们甚至能够将其应用于不使用Spring框架的项目中。Spring Session提供了一种独立于应用服务器的方案,这种方案能够在Servlet规范之内配置可插拔的session数据存储,不依赖于任何应用服务器的特定API。这就意味着Spring Session能够用于实现了servlet规范的所有应用服务器之中(Tomcat、Jetty、 WebSphere、WebLogic、JBoss等),它能够非常便利地在所有应用服务器中以完全相同的方式进行配置。我们还可以选择任意最适应需求的外部session数据存储。这使得Spring Session成为一个很理想的迁移工具,帮助我们将传统的JavaEE应用转移到云中,使其成为满足如下:

3.1、每个用户有多个账号

假设我们在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之间切换的时候,就不需要退出和重新登录了。

3.2、多级别的安全预览

假设我们正在构建的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模式预览应用了。

3.3、当使用Web Socket的时候保持登录状态

假设用户登录了example.com上的Web应用,那么他们可以使用HTML5的chat客户端实现聊天的功能,这个客户端构建在websocket之上。按照servlet规范,通过websocket传入的请求并不能保持HTTP session处于活跃状态,所以当用户在聊天的过程中,HTTP session的倒数计时器会在不断地流逝。即便站在用户的立场上,他们一直在使用应用程序,HTTP session最终也可能会出现过期。当HTTP session过期时,websocket连接将会关闭。

借助Spring Session,对于系统中的用户,我们能够很容易地实现websocket请求和常规的HTTP请求都能保持HTTP session处于活跃状态。

3.4、非Web请求访问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了。

四、Spring Session是如何运行的

我们已经讨论了在传统的应用服务器中,HTTP session管理存在不足的各种场景,接下来看一下Spring Session是如何解决这些问题的。

4.1、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接口以及两个默认实现,即CookieHttpSessionStrategyHeaderHttpSessionStrategy,其中前者使用HTTP cookie将请求与session id关联,而后者使用HTTP header将请求与session关联。

 

4.2、Spring Session对HTTP的支持

Spring Session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它应该是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequestgetSession()方法时,都会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession。

如果要理解它的话,最简单的方式就是查看Spring Session实际所使用的源码。首先,我们了解一下标准servlet扩展点的一些背景知识,在实现Spring Session的时候会使用这些知识。

4.2.1、Spring Session对filer的request,response的装饰

在2001年,Servlet 2.3规范引入了ServletRequestWrapper。它的javadoc文档这样写道,ServletRequestWrapper“提供了ServletRequest接口的便利实现,开发人员如果希望将请求适配到Servlet的话,可以编写它的子类。这个类实现了包装(Wrapper)或者说是装饰(Decorator)模式。对方法的调用默认会通过包装的请求对象来执行”。如下的代码样例抽取自Tomcat,展现了ServletRequestWrapper是如何实现的。

javax.servlet-api-3.1.0.jar

package javax.servlet;
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;}//...
}

Servlet 2.3规范还定义了HttpServletRequestWrapper,它是ServletRequestWrapper的子类,能够快速提供HttpServletRequest的自定义实现,如下的代码是从Tomcat抽取出来的,展现了HttpServletRequesWrapper类是如何运行的。

javax.servlet-api-3.1.0.jar

package javax.servlet.http;
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {/** * Constructs a request object wrapping the given request.* @throws java.lang.IllegalArgumentException if the request is null*/public HttpServletRequestWrapper(HttpServletRequest request) {super(request);}private HttpServletRequest _getHttpServletRequest() {return (HttpServletRequest) super.getRequest();}//...
}

所以,借助这些包装类就能编写代码来扩展HttpServletRequest,重载返回HttpSession的方法,让它返回由外部存储所提供的实现。如下的代码是从Spring Session项目中提取出来的,但是我将原来的注释替换为我自己的注释,用来在本文中解释代码,所以在阅读下面的代码片段时,请留意注释。

spring-session-1.3.1.RELEASE.jar

package org.springframework.session.web.http;    private final class SessionRepositoryRequestWrapperextends HttpServletRequestWrapper {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;}/*** 使用HttpSessionStrategy写sessionid到返回对象,同时调用外部存储设备持久化session信息* sessionRepository相当于DAO,有关于session持久化的4个方法*/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);}}}/*** 在这里,Spring Session项目不再将调用委托给应用服务器,而是实现自己的逻辑,返回由外部数据存储作为支撑的HttpSession实例。* 基本的实现是,先检查是不是已经有session了。* 如果有的话,就将currentSession返回,* 否则的话,它会检查当前的请求中是否有session id。* 如果有的话,将会根据这个session id,从它的SessionRepository中加载session。* 如果session repository中没有session,或者在当前请求中,* 没有当前session id与请求关联的话,* 那么它会创建一个新的session,并将其持久化到session repository中。*/@Overridepublic 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 requestif (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;}@Overridepublic ServletContext getServletContext() {if (this.servletContext != null) {return this.servletContext;}// Servlet 3.0+return super.getServletContext();}@Overridepublic HttpSessionWrapper getSession() {return getSession(true);}@Overridepublic 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);}@Overridepublic void invalidate() {super.invalidate();SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;setCurrentSession(null);SessionRepositoryFilter.this.sessionRepository.delete(getId());}}}

response有对应SessionRepositoryResponseWrapper。

    /**这个就是Servlet response的重写类了*/private final class SessionRepositoryResponseWrapperextends OnCommittedResponseWrapper {private final SessionRepositoryRequestWrapper request;SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,HttpServletResponse response) {super(response);if (request == null) {throw new IllegalArgumentException("request cannot be null");}this.request = request;}/** 这步是持久化session到存储容器,我们可能会在一个控制层里多次调用session的操作方法如果我们每次对session的操作都持久化到存储容器,必定会带来性能的影响。比如redis所以我们可以在整个控制层执行完毕了,response返回信息到浏览器时,才持久化session**/@Overrideprotected void onResponseCommitted() {this.request.commitSession();}}

4.2.2、Spring Session中SessionRepositoryFilter的处理

Spring Session定义了SessionRepositoryFilter,它实现了Servlet Filter接口。我抽取了这个filter的关键部分,将其列在下面的代码片段中,我还添加了一些注释,用来在本文中阐述这些代码,所以,同样的,请阅读下面代码的注释部分。

package org.springframework.session.web.http;    
@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends ExpiringSession>extends OncePerRequestFilter {/**  session存储容器接口,redis、mongoDB、genfire等数据库都是实现该接口  **/private final SessionRepository<S> sessionRepository;private ServletContext servletContext;/** sessionID的传递方式接口。目前spring-session自带两个实现类1.cookie方式 :CookieHttpSessionStrategy2.http header 方式:HeaderHttpSessionStrategy当然,我们也可以自定义其他方式。**/private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {if (httpSessionStrategy == null) {throw new IllegalArgumentException("httpSessionStrategy cannot be null");}/** 通过前面的spring-session功能介绍,我们知道spring-session可以支持单浏览器多session, 就是通过MultiHttpSessionStrategyAdapter来实现的。每个浏览器拥有一个sessionID,但是这个sessionID拥有多个别名(根据浏览器的tab)。如:别名1 sessionID别名2 sessionID...而这个别名通过url来传递,这就是单浏览器多session原理了**/this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(httpSessionStrategy);}public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {if (httpSessionStrategy == null) {throw new IllegalArgumentException("httpSessionStrategy cannot be null");}this.httpSessionStrategy = httpSessionStrategy;}/** 这个方法是魔力真正发挥作用的地方。这个方法创建了* 我们上文所述的封装请求对象SessionRepositoryRequestWrapper和一个封装的响应对象SessionRepositoryResponseWrapper,然后调用其余的filter链。* 这里,关键在于当这个filter后面的应用代码执行时,* 如果要获得session的话,得到的将会是Spring Session的HttpServletSession实例,它是由后端的外部数据存储作为支撑的。*/@Overrideprotected 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();  //filter链处理完成后,写session信息到response及外围持久化设备,源码见上面的SessionRepositoryRequestWrapper
        }}

4.2.3、Spring Session中sessionRepository是session存储容器接口操作session信息的读取及存储

session存储容器接口,redis、mongoDB、genfire等数据库都是实现该接口

1、sessionRepository

先看SessionRepository接口的4个方法:

package org.springframework.session;public interface SessionRepository<S extends Session> {/*** 创建*/S createSession();/*** 保存*/void save(S session);/*** 读取*/S getSession(String id);/*** 删除*/void delete(String id);

 实现类FindByIndexNameSessionRepository.java:

package org.springframework.session;
public interface FindByIndexNameSessionRepository<S extends Session>extends SessionRepository<S> {String PRINCIPAL_NAME_INDEX_NAME = FindByIndexNameSessionRepository.class.getName().concat(".PRINCIPAL_NAME_INDEX_NAME");Map<String, S> findByIndexNameAndIndexValue(String indexName, String indexValue);
}

springsession项目启动后,redis会有:

 

Redis的实现类

package org.springframework.session.data.redis;public class RedisOperationsSessionRepository implementsFindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>,MessageListener {/*** RedisSession的构造函数新建一个session,往里看源码是通过UUID生成MapSession.this(UUID.randomUUID().toString());    */public RedisSession createSession() {RedisSession redisSession = new RedisSession();if (this.defaultMaxInactiveInterval != null) {redisSession.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval);}return redisSession;}/***  调用RedisTemplate.convertAndSend()保存到redis中*/public void save(RedisSession session) {session.saveDelta();if (session.isNew()) {String sessionCreatedKey = getSessionCreatedChannel(session.getId());this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);session.setNew(false);}}/***  先构造MapSession,再查找对应的session*/private RedisSession getSession(String id, boolean allowExpired) {Map<Object, Object> entries = getSessionBoundHashOperations(id).entries();if (entries.isEmpty()) {return null;}MapSession loaded = loadSession(id, entries);if (!allowExpired && loaded.isExpired()) {return null;}RedisSession result = new RedisSession(loaded);result.originalLastAccessTime = loaded.getLastAccessedTime();return result;}/***  如果没有找到对应的session直接返回,如果找到就删除*/public void delete(String sessionId) {RedisSession session = getSession(sessionId, true);if (session == null) {return;}cleanupPrincipalIndex(session);this.expirationPolicy.onDelete(session);String expireKey = getExpiredKey(session.getId());this.sessionRedisOperations.delete(expireKey);session.setMaxInactiveIntervalInSeconds(0);save(session);}
}

2、Session接口:(包路径package org.springframework.session;)

Redis的session实现类:其中MapSession中保存关联属性,创建完session会设置lastAccessTime。

package org.springframework.session.data.redis;final class RedisSession implements ExpiringSession {/*** Creates a new instance ensuring to mark all of the new attributes to be* persisted in the next save operation.*/RedisSession() {this(new MapSession());this.delta.put(CREATION_TIME_ATTR, getCreationTime());this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());this.isNew = true;this.flushImmediateIfNecessary();}}

4.2.4、Spring Session的ServletFilter配置 

  从4.2.1~4.2.3得到的关键信息是,Spring Session对HTTP的支持所依靠的是一个简单老式的ServletFilter,借助servlet规范中标准的特性来实现Spring Session的功能。最后一个问题是如何配置这个ServletFilter了,配置Spring Session Filter很容易,在Spring Boot中,只需要在Spring Boot的配置类上使用 @EnableRedisHttpSession注解就可以了,如下面的代码片段所示:

@EnableRedisHttpSession注解的源码:
package org.springframework.session.data.redis.config.annotation.web.http;
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({ java.lang.annotation.ElementType.TYPE })
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {int maxInactiveIntervalInSeconds() default 1800;String redisNamespace() default "";String redisNamespace() default "";
}

RedisHttpSessionConfiguration是SpringHttpSessionConfiguration的redis的实现类。先看SpringHttpSessionConfiguration.java的源码,在这里定义了bean名称为springSessionRepositoryFilter的Filter,对所有请求[/*]都处理。这一点在启动日志也可以说明。

package org.springframework.session.config.annotation.web.http;
@Configuration public class SpringHttpSessionConfiguration implements ApplicationContextAware { 

//...
@Bean public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {

return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
@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;
}

//...
}

 

RedisHttpSessionConfiguration.java的源码:

package org.springframework.session.data.redis.config.annotation.web.http;@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfigurationimplements EmbeddedValueResolverAware, ImportAware {private Integer maxInactiveIntervalInSeconds = 1800;private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();private String redisNamespace = "";private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;private RedisSerializer<Object> defaultRedisSerializer;private Executor redisTaskExecutor;private Executor redisSubscriptionExecutor;private StringValueResolver embeddedValueResolver;@Beanpublic RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory,RedisOperationsSessionRepository messageListener) {RedisMessageListenerContainer container = new RedisMessageListenerContainer();container.setConnectionFactory(connectionFactory);if (this.redisTaskExecutor != null) {container.setTaskExecutor(this.redisTaskExecutor);}if (this.redisSubscriptionExecutor != null) {container.setSubscriptionExecutor(this.redisSubscriptionExecutor);}container.addMessageListener(messageListener,Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));container.addMessageListener(messageListener, Arrays.asList(new PatternTopic(messageListener.getSessionCreatedChannelPrefix() + "*")));return container;}@Beanpublic RedisTemplate<Object, Object> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());if (this.defaultRedisSerializer != null) {template.setDefaultSerializer(this.defaultRedisSerializer);}template.setConnectionFactory(connectionFactory);return template;}@Beanpublic RedisOperationsSessionRepository sessionRepository(@Qualifier("sessionRedisTemplate") RedisOperations<Object, Object> sessionRedisTemplate,ApplicationEventPublisher applicationEventPublisher) {RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);sessionRepository.setApplicationEventPublisher(applicationEventPublisher);sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);if (this.defaultRedisSerializer != null) {sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);}String redisNamespace = getRedisNamespace();if (StringUtils.hasText(redisNamespace)) {sessionRepository.setRedisKeyNamespace(redisNamespace);}sessionRepository.setRedisFlushMode(this.redisFlushMode);return sessionRepository;}//...
}

 

4.2.5、sessionListener support

4.2.5.1、在RedisOperationSessionRepository实现了MessageListener接口,redis有消息通知的时候,onMessage方法被调用,然后接下来把消息封装成各种事件
然后通过ApplicationEventPublisher和listener协作(观察者模式),前者发送事件,后者监听处理。
4.2.5.2、SessionEventHttpSessionListenerAdapter实现了ApplicationListener,有新事件时,onApplicationEvent方法被调用。所以只需要在这个类里面添加我们所需要的处理逻辑的listener即可。
注:假如我们启动2个tomcat,那么当一个session过期时,那么我们的listener在这2个tomcat都会收到事件通知。会导致重复。如果需要基于session listener做一些事情,则需要注意这点。
配置自己的事件处理器,只需要实现HttpSessionListener接口,同时实现两个方法即可。如下:
@Component
public class MyListener implements HttpSessionListener {@Overridepublic void sessionCreated(HttpSessionEvent se) {System.out.println("sessionCreated()" + se);System.out.println("online + 1");}@Overridepublic void sessionDestroyed(HttpSessionEvent se) {System.out.println("sessionDestroyed()" + se);System.out.println("online - 1");}}

 

 

---如果想配置session的过期时间,那么需要在RedisHttpSessionConfiguration里面配置,有个参数maxInactiveIntervalInSeconds,默认1800秒。因为session的管理被放到了外部的存储,所以web.xml里面的关于session过期的配置不在生效。

 

4.2.6、MultiHttpSessionStrategyAdapter单浏览器多session支持

    /*** 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);}//...}

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别名的工具方法。

五、回顾

5.1、spring-session的包结构介绍

 

  • org.springframework.session包:

    定义一些接口:如:Session接口、SessionRepository接口(存储接口)、

  • org.springframework.session.web包:

    SessionRepositoryFilter重写Filter;
    集成Servlet,把上面的filter加入到filter chain、cookie和Http header方式存放到jsession,单浏览器多session支持等

  • org.springframework.session.data、org.springframework.session.jdbc、org.springframework.session.hazelcast:

主要是各类存储容器的实现,如:redis、jvm的map、mongo、gemfire、hazelcast、jdbc等

  • org.springframework.session.event包:

    定义session生命周期相关的事件

  • org.springframework.session.http包:

    配置spring-session

 

5.2、spring-session重写servlet request 及 redis实现存储相关问题

spring-session无缝替换应用服务器的request大概原理是: 
1.自定义个Filter,实现doFilter方法 
2.继承 HttpServletRequestWrapper 、HttpServletResponseWrapper 类,重写getSession等相关方法(在这些方法里调用相关的 session存储容器操作类)。 
3.在 第一步的doFilter中,new 第二步 自定义的request和response的类。并把它们分别传递 到 过滤器链 
4.把该filter配置到 过滤器链的第一个位置上

Redis存储容器实现。 
主要实现存储公共基础类->FindByIndexNameSessionRepository ,里面主要有根据indexName从redis中查找session、根据sessionID对redis中的session增删改查的方法。 
关于redis的session存储容器,实际上spring-session是有些缺陷的。比如无法做到session的过期以及销毁的实时发布事件,以及getCurrentSession中可能存在的一些并发问题(小问题)。但整体来说还是可用性很高的,毕竟我们自己写一套这类框架成本很高。 
以上只是针对redis session的存储容器,其他存储容器可能会比redis更好,比如gemfire,至少在事件发布上是完整了(根据它实现了事件猜的)

转载于:https://www.cnblogs.com/duanxz/p/3471448.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/376186.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

cad2017怎么改变选择方式_家用胎心仪怎么使用?建议孕妈妈选择数胎动的方式...

一般胎心仪都有说明书&#xff0c;孕妈妈可以根据说明书上的方法去做。 下面介绍比较通用的方法。时间&#xff1a;早中晚餐后的30-60分钟内 环境&#xff1a;周围没有电磁或辐射等干扰 辅助&#xff1a;耦合剂 步骤&#xff1a; 1、平躺&#xff0c;寻找适合胎心位置 在听胎心…

c#endread怎么打印出来_打印机打印出来是白板是怎么回事

引起针式打印纸空白的原因大多是由于色带油墨干涸、色带拉断、打印头损坏等&#xff0c;应及时更换色带或维修打印头。故障现象:针式打印机有打印声但打印空白。维修方法:具体解决方法如下:1) 检查打印机色带盒是否正确安装&#xff0c;如果安装不正确&#xff0c;重新安装色带…

使用dnspod遭遇的奇特问题以及背后的原因与临时解决方法

由于园子里有不少用户在使用dnspod&#xff0c;我们觉得有必要将这两天blogjava.net域名在dsnpod遇到的奇特问题分享一下&#xff0c;以免再有人踩着这个坑。 12月11日&#xff0c;我们登录到dnspod的后台时&#xff0c;大吃一惊&#xff0c;blogjava.net这个域名竟然消失了。 …

lgg6可以root的版本_Kali Linux 2020.1版本变更内容

kali2020.1于2020年1月28日发布&#xff0c;为2020年的第一个版本&#xff0c;由于此版本相较以前有较大变化&#xff0c;故专篇记录一下。根据官方说明&#xff0c;主要改变如下&#xff1a;默认用户改为非root用户针对不同需求出了单独的镜像文件nethunter改为非root用户改进…

icem密度盒怎么设置_怎么做好火灾自动报警系统施工安装?

关于火灾自动报警系统施工安装GB50166-2019 《火灾自动报警系统施工及验收标准》 中有明确规定&#xff1a;3.1 一般规定3.1.1 系统部件的设置应符合设计文件和现行国家标准《火灾自动报警系统设计规范》GB50116的规定。3.1.2 有爆炸危险性的场所&#xff0c;系统的布线和部件的…

python岗位 上海_上海黑马Python24期,平均薪资10150元,16个工作日就业率70.73%

黑马程序员上海中心月薪一万只是起点关注网红遍地起&#xff0c;颜值即正义&#xff0c;要说哪个网红靠实力&#xff0c;Python当属第一&#xff01;Python作为时下最流行的一门网红语言&#xff0c;用一句话来证明它的实力就是&#xff1a;Python在手&#xff0c;天下我有&…

在IIS中部署Asp.net Mvc

概述&#xff1a; 最近在做一个MVC 3的项目&#xff0c;在部署服务器时破费了一番功夫&#xff0c;特将过程整理下来&#xff0c;希望可以帮到大家&#xff01; 本文主要介绍在IIS5.1、IIS6.0、IIS7.5中安装配置MVC 3的具体办法&#xff01; 正文&#xff1a; IIS5.1 1. 安装Mi…

idea在分屏拖不回来_朋友圈赏花晒照新玩法,宫格分屏视频!

∆ 点击上方【有科唠】一起涨姿势~近期的天气好的不要不要的&#xff0c;出去赏花是件很惬意的事情&#xff0c;继《城墙下》推出的近期赏花攻略&#xff0c;唠科粉们可以跟着攻略赏花一番&#xff0c;赏花的同时&#xff0c;大家肯定会发个朋友圈纪念一下&#xff0c;见过九宫…

导出配置_Lua配置表导出优化

随着游戏的开发&#xff0c;项目的配置表数据越来越多&#xff0c;占用的内存越来越&#xff1b;配置表占用太大就会影响游戏加载速度&#xff0c;游戏流畅度的每一毫秒都是我们的必争之路。[1] {DungeonID10000, Dungeon "王进打高俅", NextDungeonID10100, Battle…

iOS数据存取---iOS-Apple苹果官方文档翻译

本系列所有文章,链接地址:iOS7开发-Apple苹果iPhone开发Xcode官方文档翻译PDF下载地址(2013年12月29日更新版) 本文对应pdf文档下载链接,猛戳—>:数据存取文档.key.zip3.1 MB数据存取文档.pdf1.1 MB 数据存取/*技术博客http://www.cnblogs.com/ChenYilong/ 新浪微博http://w…

为什么打不开_发票查重百科导出的发票台账为什么附件打不开?

1在扫描完发票之后&#xff0c;很多人都习惯性的定期将扫描的电子发票台账导出来&#xff0c;目前电子发票查重工具小程序版提供了两种导出方式&#xff1a;通过邮件的方式将指定日期内的发票台账直接发送到指定的邮箱&#xff1b;直接将指定日期内的发票台账下载到手机上进行查…

无法找到脚本文件 C:/Windows/explorer.exe:574323188.vbs

今天打开电脑后电脑有点反常&#xff0c;在启动时没有运行 “局域网” 保护的程序&#xff0c;而且还他是 “无法找到脚本文件”如下图&#xff1a; 发现这个东西后&#xff0c;第一反应&#xff0c;拔掉网线、因为很有可能是中病毒了&#xff0c;当时就出了一身冷汗&#xff0…

T-SQL:SQL Server-数据库查询语句基本查询

ylbtech-SQL Server-Basic:SQL Server-数据库查询语句基本查询SQL Server 数据库查询语句基本查询。 1&#xff0c;数据库查询语句基本查询数据库SQL ServerOracle基本语句 select select * from titles select title_id,title,price,pub_id from titles select * from title …

修改无效_修改劳动合同日期被认定无效,青岛一企业被判赔双倍工资

劳动合同法规定&#xff0c;用人单位超过一个月不满一年未与劳动者订立书面劳动合同的&#xff0c;应当向劳动者每月支付二倍的工资。在现实中&#xff0c;经常遇到的情况是续签合同时&#xff0c;已经到期的劳动合同与新合同之间的空档期&#xff0c;如果管理混乱或者遇到特殊…

HDUOJ-----2065红色病毒问题

"红色病毒"问题 Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 3339 Accepted Submission(s): 1422 Problem Description医学界发现的新病毒因其蔓延速度和Internet上传播的"红色病毒"不相…

下拉框_jQuery 美化界面的下拉框

插件简介之前我们介绍过一款基于jQuery多级联动美化版Select下拉框&#xff0c;很多朋友都非常喜欢&#xff0c;可见经过美化过的下拉框的确可以让你的网页更为精妙。今天我们要介绍的这款美化界面下拉框也是基于jQuery的&#xff0c;它的特点是可以通过上下箭头微调选择&#…

如何判断平台工具集去做条件编译(VC++目录、预处理器定义、$(PlatformToolsetVersion))...

作者&#xff1a;zyl910 从VS2010开始&#xff0c;提供了一个平台工作集&#xff08;Platform ToolSet&#xff09;选项用于配制vc编译版本。到了VS2012&#xff0c;更是因为默认平台工具集不支持WindowsXP&#xff0c;导致经常需要切换到xp版平台工具集&#xff0c;VS2013也是…

c#语言基础编程-转义符

C#转义字符: 引言 为了在程序中能够控制字符的输出以及区分开双引号和单引号&#xff08;双引号和单引号在程序中标示里面的内容为字符串和字符&#xff09;&#xff0c;所以用一种特殊的字符常量&#xff1b;是以反斜线”\”开头&#xff0c;后跟一个或几个字符。让其具有特定…

图片图层隐写_【软件】imageIN · 图影-隐藏文件到图片,简单轻快的图片隐写工具...

还记得以前的图种制作吗&#xff0c;今天给大家分形下一个相关的工具。度盘下载(imageIN Beta1.0 (2.18MB)) &#xff1a;pan.baidu.com/s/1hqve8YS官网下载&#xff1a;本地下载这是一个能把图片中嵌入文件、文本、隐藏水印的隐写工具这种嵌入是基于图像本身的而不是基于文件的…

mysql 密码长度约束_MySQL简单操作【1、在cmd下MySQL的运行及简单增删改查】

上篇文章介绍了在Windows10下安装MySQL&#xff0c;本篇文章介绍cmd下简单的操作。1、登录 MySQL当 MySQL 服务已经运行时, 我们可以通过 MySQL 自带的客户端工具登录到 MySQL 数据库中, 首先打开命令提示符, 输入以下格式的命名:mysql -h 主机名 -u 用户名 -p参数说明&#xf…