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