简介
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 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英文资料

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐