spring-session学习
来源:互联网 发布:java性能测试环境搭建 编辑:程序博客网 时间:2024/06/04 17:48
简介
spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。
- httpSession:提供了在应用容器(列如:Tomcat)中对httpsession的扩展,同时提供了很多额外的特性:
1.Clustered Sessions集群session。
2.Multiple Browser Sessions多浏览器session。即单个浏览器多个session的管理。
3.RESTful APIs - WebSocket:提供了在接受websocket消息时,维持session有效的支持。
关键点
在spring-session的架构中,有个关键的节点,起着重要的或支持或转换或拓展的作用。
- Session/ExpiringSession:spring封装的session接口,提供了对于session的方法,有获取sessionId、保存获取参数、是否过期,最近访问时间等等。具体的实现有:
1、GemFireSession:对于GemFire方式的存取session封装,用于AbstractGemFireOperationsSessionRepository/GemFireOperationsSessionRepository中。
2、JdbcSession:对于jdbc方式的存取session封装,用于JdbcOperationsSessionRepository中。
3、MapSession:
4:、MongoExpiringSession:对于mongo方法的存取session封装,用于MongoOperationsSessionRepository中。
5、RedisSession:对于redis方式的存放session封装,通过MapSession存放变动变量,当调用redis保持后,数据持久化。用于RedisOperationsSessionRepository中。 - SessionRepository: 用于管理session的创建、检索、持久化的接口。实际上就是对session存放。因为sping-session已经提供了httpSession和session的关联适配,所以在开发中,不推荐开发者直接操作SessionRepository和session,而是应该通过httpSession/WebSocket来简介操作。
1、MapSessionRepository:使用mapSession,默认线程安全的ConcurrentHashMap。但是在NoSQL的存取方式,列如Redis、Hazelcast时,可以使用自定义的Map实现。注意的是,MapSessionRepository不支持SessionDeletedEvent/SessionExpiredEvent事件。
2、AbstractGemFireOperationsSessionRepository/GemFireOperationsSessionRepository:使用GemFireSession,用于GemFire中支持和存储session。通过SessionMessageListener监听,可以支持SessionDestroyedEvent /SessionCreatedEvent事件。
3、JdbcOperationsSessionRepository:使用JdbcSession。通过spring的JdbcOperations对session在关系型数据库中操作。注意,它不支持session事件。
4、MongoOperationsSessionRepository:使用MongoExpiringSession,在mongo中存放session。通过AbstractMongoSessionConverter对session对象和mongo代表进行转换。支持对过期session的清理,默认每分钟。
5、RedisOperationsSessionRepository:使用RedisSession。通过spring的RedisOperations对session在redis中操作。通过SessionMessageListener可以监听SessionDestroyedEvent /SessionCreatedEvent 事件。这个是实际应用中最常用的的。后续会具体说明。 - HttpSessionStrategy:用于管理request/response中如何获取、传递session标识的接口。只要提供了(1)
String getRequestedSessionId(HttpServletRequest request)
从request中获取sessionid;(2)void onNewSession(Session session, HttpServletRequest request,
把session相关信息放入response中,返回给客户端;(3)
HttpServletResponse response)void onInvalidateSession(HttpServletRequest request, HttpServletResponse response)
作废session时,需要对客户端的相关操作。
1、HeaderHttpSessionStrategy:使用header来获取保持session标识。默认名称“x-auth-token”,当session创建后,就会在response中保存个头部,值是session的标识。客户端请求时带上这样的header信息。当session过期时,response就会把此header的值设为空。
2、CookieHttpSessionStrategy:使用cookie来获取保持session标识。默认cookie名称“SESSION”,当session创建后,response就会产生这个名称的cookie来保存session标识,项目路径作为cookie的路径,同时标识为HttpOnly。如果HttpServletRequest#isSecure()返回true的话,就会设置成安全cookie。
3、MultiHttpSessionStrategy:提供对于request/response的拓展接口。 - SessionRepositoryFilter:过滤器,在spring-session中起着重要作用,提供了对request/response的转换,使httpSession和session建立关联。这样用户直接使用httpSession就间接达成了对session的操作。注意的是,这个filter必须是放在任何用在session之前的。
- SessionRepositoryRequestWrapper:对request的一些session相关方法的覆盖重写,原本队httpSession的操作转换成对Session的操作;同时封装了对session的持久化和对request/response客户端操作的入口。
private void commitSession()
。 - SessionRepositoryResponseWrapper:对response进行相应的封装,确保response在commit时,session会被保存。
- ExpiringSessionHttpSession/HttpSessionWrapper:对httpSession的封装和覆盖,使对httpSession的操作都转换成对session的相关操作。
- SpringHttpSessionConfiguration:web环境中,spring-session的java基本配置文件。在这个配置文件中,查看源码可以看出,它提供了对SessionRepository/HttpSessionStrategy/HttpSessionListener的配置方式。其中HttpSessionStrategy已经提供了默认方式:CookieHttpSessionStrategy, 当然,也可以更换实现策略;所以在使用时,SessionRepository的实现就需要我们来提供了;因为提供了对httpSession的相关监听配置入口,所以我们可以很方便的配置定义自己的监听实例,来对session创建/销毁时处理其他逻辑功能。不过注意的是,配置的SessionRepository必须要能支持SessionCreatedEvent/SessionDestroyedEvent事件。【还可以通过注解@EnableSpringHttpSession来实现】
1、GemFireHttpSessionConfiguration。需要提供GemfireOperations的实例bean。【还可以通过注解@EnableGemFireHttpSession来实现】
2、HazelcastHttpSessionConfiguration:必须提供HazelcastInstance实例bean。【还可以通过注解@EnableHazelcastHttpSession来实现】
3、JdbcHttpSessionConfiguration:必须提供DataSource实例bean。【还可以通过注解@EnableJdbcHttpSession来实现】
4、RedisHttpSessionConfiguration:必须提供RedisConnectionFactory实例bean。【还可以通过注解@EnableRedisHttpSession来实现】
5、MongoHttpSessionConfiguration:需要提供MongoOperations的实例bean。【还可以通过注解@EnableMongoHttpSession来实现】
配置说明
用redis为例说明:
首先是redis的相关配置:
1、redis.properties配置文件
# redis configredis.host=localhostredis.port=6379redis.password=redis.timeout=120000redis.database=6
2、spring-redis.xml配置
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd"> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <!-- 池中可借的最大数 --> <property name="maxTotal" value="50" /> <!-- 允许池中空闲的最大连接数 --> <property name="maxIdle" value="10" /> <!-- 允许池中空闲的最小连接数 --> <property name="minIdle" value="2" /> <!-- 获取连接最大等待时间(毫秒) --> <property name="maxWaitMillis" value="12000" /> <!-- 当maxActive到达最大数,获取连接时的操作 是否阻塞等待 --> <property name="blockWhenExhausted" value="true" /> <!-- 在获取连接时,是否验证有效性 --> <property name="testOnBorrow" value="true" /> <!-- 在归还连接时,是否验证有效性 --> <property name="testOnReturn" value="true" /> <!-- 当连接空闲时,是否验证有效性 --> <property name="testWhileIdle" value="true" /> <!-- 设定间隔没过多少毫秒进行一次后台连接清理的行动 --> <property name="timeBetweenEvictionRunsMillis" value="1800000" /> <!-- 每次检查的连接数 --> <property name="numTestsPerEvictionRun" value="5" /> </bean> <bean id="jedisFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="poolConfig" ref="jedisPoolConfig"></property> <property name="hostName" value="${redis.host}"></property> <property name="port" value="${redis.port}"></property> <property name="timeout" value="${redis.timeout}"></property> </bean></beans>
再是session的相关配置:
1、首先SessionRepositoryFilter实例化bean。spring提供了两种方式:
(1)config注解方式:
注解方式时,必须详细看下提供的注解文档,里面有说明必须提供的何种bean实例。
通配方式:@EnableSpringHttpSession(这种注解必须要提供SessionRepository实例)。如下列子:
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.session.SessionRepository;import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;import org.springframework.session.data.redis.RedisOperationsSessionRepository;@Configuration@EnableSpringHttpSessionpublic class SpringHttpSessionConfig { //@Bean(name={"a", "b}) 如果配置了name,使用name的,否则使用方法的名称 @Bean public SessionRepository<?> sessionRepository(RedisConnectionFactory redisConnectionFactory) { RedisOperationsSessionRepository redisSessionRepository = new RedisOperationsSessionRepository(redisConnectionFactory); redisSessionRepository.setDefaultMaxInactiveInterval(600); redisSessionRepository.setRedisKeyNamespace("web_01"); return redisSessionRepository; }}
如上代码中,配置提供sessionRepository的实例化。因为用的redis相关是通过xml方式配置的,所以这里就不直接new出来了,而是用原有的,所以通过注入RedisConnectionFactory。
redis方式:上面是可以对所有的都如此配置。但spring也提供了各自的配置,redis的就是@EnableRedisHttpSession(这个是需要提供暴露redisFactory实例的):
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;import org.springframework.session.data.redis.config.ConfigureRedisAction;import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;@Configuration@EnableRedisHttpSessionpublic class RedisHttpSessionConfig { /** 因为redisFactory已经在配置文件中有了,这里就不需要另外创建了 @Bean public JedisConnectionFactory connectionFactory() throws Exception { return new JedisConnectionFactory(); } **/ /** * * 因为我使用的是redis2.8+以下的版本,使用RedisHttpSessionConfiguration配置时。 * 会报错误:ERR Unsupported CONFIG parameter: notify-keyspace-events * 是因为旧版本的redis不支持“键空间通知”功能,而RedisHttpSessionConfiguration默认是启动通知功能的 * 解决方法有: * (1)install Redis 2.8+ on localhost and run it with the default port (6379)。 * (2)If you don't care about receiving events you can disable the keyspace notifications setup。 * 如本文件,配置ConfigureRedisAction的bean为不需要打开的模式。 * 另外一种方式是在xml中。 * <util:constant static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/> * */ @Bean public ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; }}
因为运行的redis是2.8以前的,不支持“键空间通知”功能,而使用redis的config注解时,又是打开这个通知功能的,所以我们需要关闭这个服务,通过提供不需要打开的实例bean。【这个也是可以在配置文件中配置的:
<util:constant static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
注意,配置中需要添加xmlns:util="http://www.springframework.org/schema/util"
引用。
xsi:schemaLocation="
】
(2)context配置文件方式:
通用方式:
<bean class="org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration"> </bean> <bean id="redisOperationsSessionRepository" class="org.springframework.session.data.redis.RedisOperationsSessionRepository"> <constructor-arg ref="jedisFactory" /> <property name="defaultMaxInactiveInterval" value="600" /> </bean>
redis自己的配置:
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="redisNamespace" value="web_01" /> <property name="maxInactiveIntervalInSeconds" value="600" /> </bean>
redisNamespace:redis键空间名称;
maxInactiveIntervalInSeconds:redis最大生存时间(秒)。
以上都是关于注入SessionRepository。
我们也可以注入自己的HttpSessionStrategy。不过spring已经提供了默认的方式,在SpringHttpSessionConfiguration中可以看到:
@Configurationpublic class SpringHttpSessionConfiguration { private CookieHttpSessionStrategy defaultHttpSessionStrategy = new CookieHttpSessionStrategy(); private HttpSessionStrategy httpSessionStrategy = this.defaultHttpSessionStrategy; .......
默认的是CookieHttpSessionStrategy。它的默认cookie系列化是:DefaultCookieSerializer,其中定义了:cookie的名字是:SESSION。我们现在换种名称实现的:
<bean class="org.springframework.session.web.http.DefaultCookieSerializer"> <property name="cookieName" value="SYSTEM_SESSION_ID" /> </bean>
以上配置好session后,就会实例化出SessionRepositoryFilter。不过注意的是,它的名称是:springSessionRepositoryFilter。
最后是SessionRepositoryFilter过滤器的配置:
这里有几种方式:
代理方式(web.xml):
通过DelegatingFilterProxy代理实例化的过滤器。过滤器的名称就是实例bean的名称:springSessionRepositoryFilter
<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>*.htm</url-pattern> </filter-mapping>
初始化方式:
这种方式时通过org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer实现的。在spring加载时,会注入sessionRepositoryFilter,查看源码如下:
```@Order(100)public abstract class AbstractHttpSessionApplicationInitializer implements WebApplicationInitializer { ...... public void onStartup(ServletContext servletContext) throws ServletException { beforeSessionRepositoryFilter(servletContext); if (this.configurationClasses != null) { AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext(); rootAppContext.register(this.configurationClasses); servletContext.addListener(new ContextLoaderListener(rootAppContext)); } insertSessionRepositoryFilter(servletContext); afterSessionRepositoryFilter(servletContext); } /** * Registers the springSessionRepositoryFilter. * @param servletContext the {@link ServletContext} */ private void insertSessionRepositoryFilter(ServletContext servletContext) { String filterName = DEFAULT_FILTER_NAME; DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy( filterName); String contextAttribute = getWebApplicationContextAttribute(); if (contextAttribute != null) { springSessionRepositoryFilter.setContextAttribute(contextAttribute); } registerFilter(servletContext, true, filterName, springSessionRepositoryFilter); } ...... /** * Registers the provided filter using the {@link #isAsyncSessionSupported()} and * {@link #getSessionDispatcherTypes()}. * * @param servletContext the servlet context * @param insertBeforeOtherFilters should this Filter be inserted before or after * other {@link Filter} * @param filterName the filter name * @param filter the filter */ private void registerFilter(ServletContext servletContext, boolean insertBeforeOtherFilters, String filterName, Filter filter) { Dynamic registration = servletContext.addFilter(filterName, filter); if (registration == null) { throw new IllegalStateException( "Duplicate Filter registration for '" + filterName + "'. Check to ensure the Filter is only configured once."); } registration.setAsyncSupported(isAsyncSessionSupported()); EnumSet<DispatcherType> dispatcherTypes = getSessionDispatcherTypes(); registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*"); } ......
所以我们如下添加下前后处理逻辑即可:
import javax.servlet.ServletContext;import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;public class SpringHttpSessionApplicationInitializer extends AbstractHttpSessionApplicationInitializer { //注入sessionRepositoryFilter前业务处理 @Override protected void beforeSessionRepositoryFilter(ServletContext servletContext) { System.out.println("----beforeSessionRepositoryFilter"); super.beforeSessionRepositoryFilter(servletContext); } //注入sessionRepositoryFilter后业务处理 @Override protected void afterSessionRepositoryFilter(ServletContext servletContext) { System.out.println("----afterSessionRepositoryFilter"); super.afterSessionRepositoryFilter(servletContext); }}
以上配置ok之后,运行之后我们就可以看到,session被创建了:
在浏览器上可以看到在response中看到我们定义的cookie名称,里面放入的就是sessionId。
同时,我们可以看到redis中已经存放了session信息。
在开发过程中,我们可能会在session的创建或销毁时要处理额外的业务,这个时候我们就应该添加相应的监听器,用于监听处理session创建、销毁事件。不过首先要确保配置的SessionRepository是支持session事件触发的。
还是使用redis为列:
在SpringHttpSessionConfiguration中提供了HttpSessionListener监听器的注入方式。
首先,继承HttpSessionListener,创建session事件的触发器。
import javax.servlet.http.HttpSession;import javax.servlet.http.HttpSessionEvent;import javax.servlet.http.HttpSessionListener;public class HttpSessionMonitorListener implements HttpSessionListener { @Override public void sessionCreated(HttpSessionEvent se) { HttpSession session = se.getSession(); System.out.println("----------------------------------sessionCreated"); System.out.println("sessionId: " + session.getId()); System.out.println("sessionCreationTime: " + session.getCreationTime()); System.out.println("sessionLastAccessedTime: "+session.getLastAccessedTime()); int maxInterval = session.getMaxInactiveInterval(); System.out.println("sessionMaxInactiveInterval(s): " + session.getMaxInactiveInterval()); System.out.println("sessionExpirtion: " + (session.getCreationTime() + maxInterval*1000)) ; System.out.println("----------------------------------sessionCreated"); } @Override public void sessionDestroyed(HttpSessionEvent se) { HttpSession session = se.getSession(); System.out.println("----------------------------------sessionDestroyed"); System.out.println("sessionId: " + session.getId()); System.out.println("sessionCreationTime: " + session.getCreationTime()); System.out.println("sessionLastAccessedTime:"+session.getLastAccessedTime()); System.out.println("hsName: "+session.getAttribute("hsName")); System.out.println("-----------------------------------sessionDestroyed"); }}
这个时候就要打开“键值管理”功能。
config方式时:
在RedisHttpSessionConfig中添加
@Bean public HttpSessionListener httpSessionListener() { return new HttpSessionMonitorListener(); }
同时去掉ConfigureRedisAction的实例化。
//@Bean public ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; }
xml配置方式时:
实例化监听器。
<bean id="httpSessionMonitorListener" class="com.zcl.listener.HttpSessionMonitorListener" />
同时注入RedisHttpSessionConfiguration实例中
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"> <property name="redisNamespace" value="web_01" /> <property name="maxInactiveIntervalInSeconds" value="600" /> <property name="httpSessionListeners"> <list> <ref bean="httpSessionMonitorListener"/> </list> </property> </bean>
去掉
<!-- <util:constant static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/> -->
运行之后,可以看到:
———————————-sessionCreated
sessionId: 11a5e8c3-652f-421d-b9c1-b78170d4af61
sessionCreationTime: 1466389797442
sessionLastAccessedTime: 1466389797442
sessionMaxInactiveInterval(s): 1800
sessionExpirtion: 1466391597442
———————————-sessionCreated
//等session到期之后。
———————————-sessionDestroyed
sessionId: 11a5e8c3-652f-421d-b9c1-b78170d4af61
sessionCreationTime: 1466389797293
sessionLastAccessedTime:1466389797293
hsName: hsSession
———————————–sessionDestroyed
不过注意的是,就是我设置maxInactiveIntervalInSeconds=60。但是在session创建时间的时候却是显示的是1800(默认的值)。为什么了?后续“注意点 2》关于监听redis的session事件”部分有分析。
流程:
当请求进来后,进入SessionRepositoryFilter过滤器doFilterInternal(**):
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(); } }
其中:
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext);
就是对httpServletRequest进行包装,重载了httpServletRequest中关于session操作的方法。我们可以看到SessionRepositoryRequestWrapper中一些重载方法:
@Overridepublic HttpSessionWrapper getSession() { return getSession(true);}......@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 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)")); } S session = SessionRepositoryFilter.this.sessionRepository.createSession(); session.setLastAccessedTime(System.currentTimeMillis()); currentSession = new HttpSessionWrapper(session, getServletContext()); setCurrentSession(currentSession); return currentSession;}......@Overridepublic boolean isRequestedSessionIdValid() { if (this.requestedSessionIdValid == null) { String sessionId = getRequestedSessionId(); S session = sessionId == null ? null : getSession(sessionId); return isRequestedSessionIdValid(session); } return this.requestedSessionIdValid;}private boolean isRequestedSessionIdValid(S session) { if (this.requestedSessionIdValid == null) { this.requestedSessionIdValid = session != null; } return this.requestedSessionIdValid;} ......@SuppressWarnings("unused")public String changeSessionId() { HttpSession session = getSession(false); if (session == null) { throw new IllegalStateException( "Cannot change session ID. There is no session associated with this request."); } // eagerly get session attributes in case implementation lazily loads them Map<String, Object> attrs = new HashMap<String, Object>(); Enumeration<String> iAttrNames = session.getAttributeNames(); while (iAttrNames.hasMoreElements()) { String attrName = iAttrNames.nextElement(); Object value = session.getAttribute(attrName); attrs.put(attrName, value); } SessionRepositoryFilter.this.sessionRepository.delete(session.getId()); HttpSessionWrapper original = getCurrentSession(); setCurrentSession(null); HttpSessionWrapper newSession = getSession(); original.setSession(newSession.getSession()); newSession.setMaxInactiveInterval(session.getMaxInactiveInterval()); for (Map.Entry<String, Object> attr : attrs.entrySet()) { String attrName = attr.getKey(); Object attrValue = attr.getValue(); newSession.setAttribute(attrName, attrValue); } return newSession.getId();}
上面可以看到在session相关操作时,并不是直接针对Session的,二是通过HttpSessionWrapper的封装间接操作Session的。
private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> { HttpSessionWrapper(S session, ServletContext servletContext) { super(session, servletContext); } @Override public void invalidate() { super.invalidate(); SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true; setCurrentSession(null); SessionRepositoryFilter.this.sessionRepository.delete(getId()); }}
进行深入,查看ExpiringSessionHttpSession:
class ExpiringSessionHttpSession<S extends ExpiringSession> implements HttpSession { private S session; private final ServletContext servletContext; private boolean invalidated; private boolean old; ExpiringSessionHttpSession(S session, ServletContext servletContext) { this.session = session; this.servletContext = servletContext; } public void setSession(S session) { this.session = session; } public S getSession() { return this.session; } ......
这样我们就可以理清一条线路:
1、HttpServletRequest的getSession()方法。
2、演变成SessionRepositoryRequestWrapper的getSession()/getSession(boolean create)方法。
3、在getSession方法中,通过SessionRepository的createSession()创建出对应session。放入HttpSessionWrapper封装,session作为HttpSessionWrapper的一个参数属性:
SessionRepositoryRequestWrapper类中:S session = SessionRepositoryFilter.this.sessionRepository.createSession();session.setLastAccessedTime(System.currentTimeMillis());currentSession = new HttpSessionWrapper(session, getServletContext());setCurrentSession(currentSession);----------------------------------------class ExpiringSessionHttpSession<S extends ExpiringSession> implements HttpSession { private S session; ......
4、所以对httpSession的操作都变成HttpSessionWrapper对Session属性的操作。
session是何时持久化的:
当调用HttpSession的setAttribute(String name, Object value);方法时。调用追踪的时候就会看到:
public void setAttribute(String attributeName, Object attributeValue) { this.cached.setAttribute(attributeName, attributeValue); this.delta.put(getSessionAttrNameKey(attributeName), attributeValue); flushImmediateIfNecessary(); }
在flushImmediateIfNecessary()方式中:
private void flushImmediateIfNecessary() { if (RedisOperationsSessionRepository.this.redisFlushMode == RedisFlushMode.IMMEDIATE) { saveDelta(); } }
根据实例话时redisFlushMode的属性,判断是否立即持久化。默认的是RedisFlushMode.ON_SAVE。即在web环境时,response在commit时。
response何时commit呢?在SessionRepositoryFilter过滤结束时执行:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ......SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper( request, response, this.servletContext); ...... try { filterChain.doFilter(strategyRequest, strategyResponse); } finally { wrappedRequest.commitSession(); } }
在SessionRepositoryRequestWrapper的commitSession()时,持久化,同时会把session唯一标识放入web中(cookie策略是放入cookie中,header策略是放入header中等)。
注意点:
1》
session在创建保持到redis中,有如下动作:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \ maxInactiveInterval 1800 \ lastAccessedTime 1404360000000 \ sessionAttr:attrName someAttrValue \ sessionAttr2:attrName someAttrValue2EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32feEXPIRE spring:session:expirations1439245080000 2100
首先保存session,作为Hash保持到redis中的,使用HMSET 命令:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \ maxInactiveInterval 1800 \ lastAccessedTime 1404360000000 \ sessionAttr:attrName someAttrValue \ sessionAttr2:attrName2 someAttrValue2
可以看出这些信息点:
- session标识为:33fdd1b6-b496-4b33-9f7d-df96679d32fe。
- session的创建时间点是1404360000000(距离1/1/1970的毫秒时间)。
- session的存在期限是1800秒(30分钟)。
- session的最近访问时间点是1404360000000(距离1/1/1970的毫秒时间)。
- session有两个键值对属性:attrName-someAttrValue和attrName2-someAttrValue2。
当属性变更时:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
session的过期通过EXPIRE 命令控制:
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
注意到这里是2100而不是1800,说明设置的真实过期是在过期5分钟后。为什么这样设置了,这就涉及到了redis对于session过期处理方式:
为了保证在session销毁时,把session关联的资源都清理掉,需要redis在session过期时,通过“keyspace notifications ”触发SessionDeletedEvent/SessionExpiredEvent 事件。因为session事件中涉及session信息,所以要保证这个时候,session的相关信息还是要存在。所以要把session的真实过期事件设置比需要的还有长5分钟,这样才能保证逻辑过期后,依然能获取到sssion信息。
因为要触发事件,所以session得过期时间设置的比逻辑上的晚5分钟,但是这样会造成不符合我们的逻辑设定,为此,过期的设置添加一些额外处理:
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
这样,当spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe在我们逻辑设定的时间点过期时,就会触发session销毁事件,但是因为session的信息延后5分钟过期,又保证了在session事件中能正常获取到session信息。
但是有个问题出现,这是redis独有的问题:redis的过期处理,实质上就是先看下这个key的过期时间,如果过期了,才删除。所以,当key没有被访问到时,我们就不能保证这个实际上已经过期的session在何时会触发session过期事件。尤其redis后台清理过期的任务的优先级比较低,很可能不会触发的。(详细参考:redis过期事件时间;redis删除过期机制)。
为了规避这个问题。我们可以确保:当每个key预期到期时,key可以被访问到。这个就意味着:当key的生命周期到期时,我们试图通过先访问到它,然后来让redis删除key,同时触发过期事件。
为此,要让每个session在过期时间点附近就可以被访问追踪到,这样就可以让后台任务访问到可能过期的session,以更确定性的方式确保redis可以触发过期事件:
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32feEXPIRE spring:session:expirations1439245080000 2100
后台任务通过映射(eg:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe)关联到准确的请求key。通过访问key,而不是删除它,我们可以确保只有当生命周期过期时,redis才会删除这个key。
我们并不能很确切地删除这些key,因为在一些情况下,可能会错误地认定key过期了,除了使用分布式锁外,没有任何一种方法能保证过期映射的一致性。但是通过简单的访问方式,我们却可以保证只有过期能才会被删除。
2》关于监听redis的session事件。为了能够正常作用,需要打开redis的设置,redis默认是关闭的(redis键控制通知)。
redis-cli config set notify-keyspace-events Egx
在RedisOperationsSessionRepository中定义了发布session时间的方法:
//session创建时间操作处理public void handleCreated(Map<Object, Object> loaded, String channel) { String id = channel.substring(channel.lastIndexOf(":") + 1); ExpiringSession session = loadSession(id, loaded); publishEvent(new SessionCreatedEvent(this, session)); }//session删除事件处理private void handleDeleted(String sessionId, RedisSession session) { if (session == null) { publishEvent(new SessionDeletedEvent(this, sessionId)); } else { publishEvent(new SessionDeletedEvent(this, session)); }}//session过期事件处理private void handleExpired(String sessionId, RedisSession session) { if (session == null) { publishEvent(new SessionExpiredEvent(this, sessionId)); } else { publishEvent(new SessionExpiredEvent(this, session)); }}//发布事件通知private void publishEvent(ApplicationEvent event) { try { this.eventPublisher.publishEvent(event); } catch (Throwable ex) { logger.error("Error publishing " + event + ".", ex); }}
首先分析session创建事件,流程是这样的:
- SessionRepositoryFilter中doFilterInternal(…)方法中的
wrappedRequest.commitSession();
- SessionRepositoryRequestWrapper的commitSession()方法中的
SessionRepositoryFilter.this.sessionRepository.save(session);
- RedisOperationsSessionRepository的save(RedisSession session)方法:
public void save(RedisSession session) { session.saveDelta(); if (session.isNew()) { String sessionCreatedKey = getSessionCreatedChannel(session.getId()); this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta); session.setNew(false); } }
- 继续RedisOperationsSessionRepository的convertAndSend(String destination, Object message)方法。
- 进入RedisTemplate的convertAndSend(String channel, Object message)方法。
public void convertAndSend(String channel, Object message) { Assert.hasText(channel, "a non-empty channel is required"); final byte[] rawChannel = rawString(channel); final byte[] rawMessage = rawValue(message); execute(new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) {//保存回调时,发布事件通知。 connection.publish(rawChannel, rawMessage); return null; } }, true); }
上面就是session创建的到发布事件的流程。在前面我们还留了个问题,为什么在session创建事件中session的过期失效时间不对?
在RedisOperationsSessionRepository的save(RedisSession session)方法中session.saveDelta();
在这个方法中,会把session的delta数据保存到redis后重置了this.delta = new HashMap<String, Object>(this.delta.size());
。这样delta 就会没有保留信息。
但是在convertAndSend(String destination, Object message)
时,把delta传入作为message了。所以最后发布事件通知时,通知中没有有效信息。
在接受处理的时候,根据id重新创建了MapSession ,只能默认属性值。
private MapSession loadSession(String id, Map<Object, Object> entries) { MapSession loaded = new MapSession(id); for (Map.Entry<Object, Object> entry : entries.entrySet()) { String key = (String) entry.getKey(); if (CREATION_TIME_ATTR.equals(key)) { loaded.setCreationTime((Long) entry.getValue()); } else if (MAX_INACTIVE_ATTR.equals(key)) { loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue()); } else if (LAST_ACCESSED_ATTR.equals(key)) { loaded.setLastAccessedTime((Long) entry.getValue()); } else if (key.startsWith(SESSION_ATTR_PREFIX)) { loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue()); } } return loaded; }
所以才会造成上面我们的问题。
再分析下session过期时间流程:
- 在最开始项目启动的时候,RedisHttpSessionConfiguration中会实例化出RedisMessageListenerContainer容器。
- 当session过期时,redis触发事件通知。
- 进入DispatchMessageListener的onMessage(…)方法。
- 进入RedisHttpSessionConfiguration的dispatchMessage(…)方法。
- 后面进入RedisOperationsSessionRepository的onMessage(Message message, byte[] pattern)方法。在这个方法中,会根据sessionId从redis中获取session的信息(这个也就是为什么session的真实过期时间点要延后几分钟。),然后调用handleDeleted(…)/handleExpired(…)方法。
sping-session还提供了其他的对接方式,后续慢慢补充。也可以查看原文档学习spring sesison英文资料
- spring-session学习
- 学习Spring-Session+Redis实现session共享
- 学习Spring-Session+Redis实现session共享
- 学习Spring-Session+Redis实现session共享
- 学习Spring-Session+Redis实现session共享
- Spring Session学习(一)
- Spring学习之(Hibernate)Session接口
- 学习Spring-Session+Redis实现sess…
- Spring session
- Spring-Session
- Spring Session
- Spring session
- Spring Session
- Spring Session
- Spring Session
- spring-session
- Spring session
- Spring Session - Spring Boot
- 深拷贝和浅拷贝
- 第十六周阅读程序3(1)
- IOS 与 JS 交互
- Python学习笔记(条件语句)
- android字符串规定字符变色
- spring-session学习
- Android-蓝牙详解【占坑中】
- 8天学通MongoDB——第七天 运维技术
- java泛型详解
- 第四十讲 项目1 小明借书
- android自定义View
- 余弦相似度 —— Cosine Similarity
- UITabBarController+UINavigationController多层嵌套
- PHP多图片上传并按照比例修改像素