博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
spring-session学习
阅读量:6217 次
发布时间:2019-06-21

本文共 30418 字,大约阅读时间需要 101 分钟。

hot3.png

spring-session学习 博客分类: spring java

简介 

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, 
    HttpServletResponse response)
    把session相关信息放入response中,返回给客户端;(3)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配置

  • 36
  • 37
  • 38
  • 39
  • 40

再是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; }}
  • 9
  • 20
  • 21

如上代码中,配置提供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中。 *
* */ @Bean public ConfigureRedisAction configureRedisAction() { return ConfigureRedisAction.NO_OP; }}
  • 32
  • 33
  • 34
  • 35

因为运行的redis是2.8以前的,不支持“键空间通知”功能,而使用redis的config注解时,又是打开这个通知功能的,所以我们需要关闭这个服务,通过提供不需要打开的实例bean。【这个也是可以在配置文件中配置的:

注意,配置中需要添加xmlns:util="http://www.springframework.org/schema/util" 

xsi:schemaLocation="引用。 

(2)context配置文件方式: 

通用方式:

redis自己的配置:

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。我们现在换种名称实现的:

以上配置好session后,就会实例化出SessionRepositoryFilter。不过注意的是,它的名称是:springSessionRepositoryFilter。 

最后是SessionRepositoryFilter过滤器的配置: 
这里有几种方式: 
代理方式(web.xml): 
通过DelegatingFilterProxy代理实例化的过滤器。过滤器的名称就是实例bean的名称:springSessionRepositoryFilter

springSessionRepositoryFilter
org.springframework.web.filter.DelegatingFilterProxy
springSessionRepositoryFilter
*.htm

初始化方式: 

这种方式时通过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
dispatcherTypes = getSessionDispatcherTypes(); registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*"); } ......
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

所以我们如下添加下前后处理逻辑即可:

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); }}
  • 19
  • 20

以上配置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");    }}
  • 36
  • 37
  • 38
  • 39
  • 40

这个时候就要打开“键值管理”功能。 

config方式时: 
在RedisHttpSessionConfig中添加

@Bean    public HttpSessionListener httpSessionListener() {        return new HttpSessionMonitorListener();    }

同时去掉ConfigureRedisAction的实例化。

//@Bean    public ConfigureRedisAction configureRedisAction() {        return ConfigureRedisAction.NO_OP;    }

xml配置方式时: 

实例化监听器。

同时注入RedisHttpSessionConfiguration实例中

  • 9

去掉

运行之后,可以看到: 

———————————-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();        }    }
  • 21
  • 22

其中:

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
attrs = new HashMap
(); Enumeration
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
attr : attrs.entrySet()) { String attrName = attr.getKey(); Object attrValue = attr.getValue(); newSession.setAttribute(attrName, attrValue); } return newSession.getId();}
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102

上面可以看到在session相关操作时,并不是直接针对Session的,二是通过HttpSessionWrapper的封装间接操作Session的。

private final class HttpSessionWrapper extends ExpiringSessionHttpSession {
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()); }}
  • 14

进行深入,查看ExpiringSessionHttpSession:

class ExpiringSessionHttpSession 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 implements HttpSession {
private S session; ......
  • 13

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();        }    }
  • 14
  • 15

在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
  • 10

首先保存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后台清理过期的任务的优先级比较低,很可能不会触发的。(详细参考:;)。

为了规避这个问题。我们可以确保:当每个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-cli config set notify-keyspace-events Egx

在RedisOperationsSessionRepository中定义了发布session时间的方法:

//session创建时间操作处理public void handleCreated(Map
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); }}
  • 30
  • 31
  • 32
  • 33

首先分析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);        }    }
  • 8
  • 继续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() {            public Object doInRedis(RedisConnection connection) {//保存回调时,发布事件通知。    connection.publish(rawChannel, rawMessage);                return null;            }        }, true);    }
  • 14
  • 15

上面就是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
entries) { MapSession loaded = new MapSession(id); for (Map.Entry
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; }
  • 19
  • 20

所以才会造成上面我们的问题。

再分析下session过期时间流程:

  • 在最开始项目启动的时候,RedisHttpSessionConfiguration中会实例化出RedisMessageListenerContainer容器。
  • 当session过期时,redis触发事件通知。
  • 进入DispatchMessageListener的onMessage(…)方法。
  • 进入RedisHttpSessionConfiguration的dispatchMessage(…)方法。
  • 后面进入RedisOperationsSessionRepository的onMessage(Message message, byte[] pattern)方法。在这个方法中,会根据sessionId从redis中获取session的信息(这个也就是为什么session的真实过期时间点要延后几分钟。),然后调用handleDeleted(…)/handleExpired(…)方法。

sping-session还提供了其他的对接方式,后续慢慢补充。也可以查看原文档学习

 

 

http://blog.csdn.net/zcl111/article/details/51700925

转载于:https://my.oschina.net/xiaominmin/blog/1598743

你可能感兴趣的文章
Kali-linux Arpspoof工具
查看>>
PDF文档页面如何重新排版?
查看>>
基于http协议使用protobuf进行前后端交互
查看>>
bash腳本編程之三 条件判断及算数运算
查看>>
php cookie
查看>>
linux下redis安装
查看>>
弃 Java 而使用 Kotlin 的你后悔了吗?| kotlin将会是最好的开发语言
查看>>
JavaScript 数据类型
查看>>
量子通信和大数据最有市场突破前景
查看>>
StringBuilder用法小结
查看>>
对‘初学者应该选择哪种编程语言’的回答——计算机达人成长之路(38)
查看>>
如何申请开通微信多客服功能
查看>>
Sr_C++_Engineer_(LBS_Engine@Global Map Dept.)
查看>>
非监督学习算法:异常检测
查看>>
App开发中甲乙方冲突会闹出啥后果?H5 APP 开发可以改变现状吗
查看>>
jquery的checkbox,radio,select等方法总结
查看>>
Linux coredump
查看>>
Ubuntu 10.04安装水晶(Mercury)无线网卡驱动
查看>>
Myeclipes快捷键
查看>>
我的友情链接
查看>>