Spring-Session官网介绍

Spring Session提供了一套创建和管理Servlet HttpSession的方案。Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。spring-session特点包括

  • API和用于管理用户会话的实现;
  • 允许以应用程序容器(即Tomcat)中性的方式替换HttpSession;
  • Spring Session 让支持集群会话变得不那么繁琐,并且不和应用程序容器金习性绑定到。
  • Spring 会话支持在单个浏览器实例中管理多个用户的会话。
  • Spring Session 允许在headers 中提供会话ID以使用RESTful API

基本原理

Spring-Session的实现就是设计一个过滤器Filter,当Web服务器接收到http请求后,请求首先进入对应的Filter进行过滤,利用HttpServletRequestWrapper实现自己的 getSession()方法,接管创建和管理Session数据的工作。将原本需要由web服务器创建会话的过程转交给Spring-Session进行创建,本来创建的会话保存在Web服务器内存中,通过Spring-Session创建的会话信息可以保存第三方的服务中,如:redis,mysql等。Web服务器之间通过连接第三方服务来共享数据,实现Session共享!(扩展:我们也可以通过其他方法实现接管创建和管理Session数据的工作,可以利用Servlet容器提供的插件功能,自定义HttpSession的创建和管理策略,并通过配置的方式替换掉默认的策略。不过这种方式有个缺点,就是需要耦合Tomcat/Jetty等Servlet容器的代码。这方面其实早就有开源项目了,例如 memcached-session-manager,以及 tomcat-redis-session-manager。暂时都只支持Tomcat6/Tomcat7)

SpringBoot整合Redis并集成spring-session实战

<!--添加redis和spring-session依赖包 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
 
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
#application.yml文件配置redis连接信息
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
//用来启用RedisHttpSession功能,并向Spring容器中注册一个RedisConnectionFactory
//同时将RedisHttpSessionConfig加入到容器中
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class RedisHttpSessionConfig {
}

Spring Session原理

编写了一个配置类RedisHttpSessionConfig,它包含注解@EnableRedisHttpSession,@EnableRedisHttpSession注解通过Import引入了RedisHttpSessionConfiguration配置类。该配置类通过@Bean注解,向Spring容器中注册了一个SessionRepositoryFilter(SessionRepositoryFilter的依赖关系:SessionRepositoryFilter --> SessionRepository --> RedisTemplate --> RedisConnectionFactory)。

SessionRepositoryFilter这个过滤器的主要作用是拦所有的请求,接管创建和管理Session数据。具体怎样接管session数据我们后边讲,我们现在只需要了解SessionRepositoryFilter整个过滤器作用就行。

RedisHttpSessionConfiguration继承了SpringHttpSessionConfiguration,SpringHttpSessionConfiguration中进行了SessionRepositoryFilter的注册,代码如下

@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
 
    //.....
    
	@Bean
	public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(
			SessionRepository<S> sessionRepository) {
		SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>(sessionRepository);
		sessionRepositoryFilter.setServletContext(this.servletContext);
		sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
		return sessionRepositoryFilter;
	}
}

这里我们可以看到,注册这个filter时需要一个SessionRepository参数,那么,这个参数又是从哪里来的呢?

在SpringHttpSessionConfiguration的继承类RedisHttpSessionConfiguration中,我们找到了SessionRepository被注入的代码

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
		implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
		SchedulingConfigurer {
	@Bean
	public RedisOperationsSessionRepository sessionRepository() {
		RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
		RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
				redisTemplate);
		sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
		if (this.defaultRedisSerializer != null) {
			sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
		}
		sessionRepository
				.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
		if (StringUtils.hasText(this.redisNamespace)) {
			sessionRepository.setRedisKeyNamespace(this.redisNamespace);
		}
		sessionRepository.setRedisFlushMode(this.redisFlushMode);
		int database = resolveDatabase();
		sessionRepository.setDatabase(database);
		return sessionRepository;
	}
}

这样一来就不需要开发人员主动配置一个RedisOperationsSessionRepository,但是这个配置需要一个RedisOperations,而这个RedisOperations也是定义在这个类中的。而这个RedisTemplate依赖一个RedisConnectionFactory是需要开发人员配置的。如果我们使用spring-boot,只需要指定application.properties的spring.redis.cluster.nodes即可配置一个redis集群JedisConnectionFactory。具体请参考org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.RedisConnectionConfiguration

好了,下面我们可以来介绍一下SessionRepositoryFilter如何接管创建和管理Session数据了

SessionRepositoryFilter

SessionRepositoryFilter是一个优先级最高的 javax.servlet. Filter,它使用了一个SessionRepositoryRequestWrapper类接管了Http Session的创建和管理工作。

每当有请求进入时,过滤器会首先将ServletRequest 和ServletResponse 这两个对象转换成Spring内部的包装类SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper对象。

@Order(SessionRepositoryFilter.DEFAULT_ORDER)
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
    @Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
         //使用HttpServletRequest 、HttpServletResponse和servletContext创建一个SessionRepositoryRequestWrapper
		SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
				request, response, this.servletContext);
		SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
				wrappedRequest, response);
		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
            //保存session信息
			wrappedRequest.commitSession();
		}
	}
}

SessionRepositoryRequestWrapper类

重写了原生的getSession方法

@Override
		public HttpSessionWrapper getSession(boolean create) {
            //获取当前Request作用域中代表Session的属性,缓存作用避免每次都从sessionRepository获取
			HttpSessionWrapper currentSession = getCurrentSession();
			if (currentSession != null) {
				return currentSession;
			}
            //查找客户端中一个叫SESSION的cookie,拿到sessionId,通过sessionRepository对象根据sessionId去Redis中查找
			S requestedSession = getRequestedSession();
            //如果从redis中查询到了值
			if (requestedSession != null) {
                //客户端存在sessionId 并且未过期
				if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
					requestedSession.setLastAccessedTime(Instant.now());
					this.requestedSessionIdValid = true;
					currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
					currentSession.setNew(false);
                    //将Session设置到request属性中
					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");
			}
            //不创建Session就直接返回null
			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)"));
			}
            //执行到这了说明需要创建新的Session
            // 通过sessionRepository创建RedisSession这个对象
			S session = SessionRepositoryFilter.this.sessionRepository.createSession();
			session.setLastAccessedTime(Instant.now());
			currentSession = new HttpSessionWrapper(session, getServletContext());
			setCurrentSession(currentSession);
			return currentSession;
		}
	// 通过sessionRepository创建RedisSession这个对象
	@Override
	public RedisSession createSession() {
		Duration maxInactiveInterval = Duration
				.ofSeconds((this.defaultMaxInactiveInterval != null)
						? this.defaultMaxInactiveInterval
						: MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS);
		RedisSession session = new RedisSession(maxInactiveInterval);
		session.flushImmediateIfNecessary();
		return session;
}

上面有一点需要注意就是将Sesison对象包装成了HttpSessionWrapper,目的是当Session失效时可以从sessionRepository删除。这里重写了getSession方法,也就是为什么每当执行HttpServletRequest执行.getSession()方法后就会刷新session的过期时间。

private final class HttpSessionWrapper extends HttpSessionAdapter<S> {
 
			HttpSessionWrapper(S session, ServletContext servletContext) {
				super(session, servletContext);
			}
			@Override
			public void invalidate() {
				super.invalidate();
				SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
				setCurrentSession(null);
				clearRequestedSessionCache();
				SessionRepositoryFilter.this.sessionRepository.deleteById(getId());
			}
}

SessionRepository保存session数据

@Override
	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);
 
		try {
			filterChain.doFilter(wrappedRequest, wrappedResponse);
		}
		finally {
            //将Session同步到Redis,同时这个方法还会将当前的SESSIONID写到cookie中去,同时还会发布SESSION创建事件到队列里面去
			wrappedRequest.commitSession();
		}
	}
 
 
    //使用httpessionidresolver将会话id写入响应并持久化会话。
    private void commitSession() {
                HttpSessionWrapper wrappedSession = getCurrentSession();
                if (wrappedSession == null) {
                    //session已过期,更新过期时间
                    if (isInvalidateClientSession()) {
                        SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
                                this.response);
                    }
                }
                else {
                    S session = wrappedSession.getSession();
                    clearRequestedSessionCache();
                    //持久化session数据到redis
                    SessionRepositoryFilter.this.sessionRepository.save(session);
                    String sessionId = session.getId();
                    if (!isRequestedSessionIdValid()
                            || !sessionId.equals(getRequestedSessionId())) {
                        SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
                                this.response, sessionId);
                    }
                }
}

commitSession这个方法的作用就是当前Session存在则使用sessionRepository保存(可能是新Session)或更新(老Session则更新一下避免过期)Session。如果Session不存在并且isInvalidateClientSession()为true说明Session已过期调用httpSessionStrategy .onInvalidateSession(this, this.response);更新Cookie。commitSession()方法还会在过滤器结束后调用,用来更新Session

接下来为大家扩展了一个关于Spring Session 数据结构的知识点,请参考《分布式下session共享之spring-session数据结构(三)》该篇文章!