spring-session学习
简介spring-session提供对用户session管理的一系列api和实现。提供了很多可扩展、透明的封装方式用于管理httpSession/WebSocket的处理。httpSession:提供了在应用容器(列如:Tomcat)中对httpsession的扩展,同时提供了很多额外的特性:1.Clustered Sessions集群session。2.Multiple Browse
简介
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 config
redis.host=localhost
redis.port=6379
redis.password=
redis.timeout=120000
redis.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
@EnableSpringHttpSession
public 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
@EnableRedisHttpSession
public 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中可以看到:
@Configuration
public 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中一些重载方法:
@Override
public HttpSessionWrapper getSession() {
return getSession(true);
}
......
@Override
public 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;
}
......
@Override
public 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 someAttrValue2
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE 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-df96679d32fe
EXPIRE 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英文资料
更多推荐
所有评论(0)