引言
在Web项目由于Http的无状态性,如何存储用户的会话信息是一个普遍的问题,在传统的单体项目中用户会话信息Session存储在系统内存中,也就是堆内存的Map中。随着前后端分离和微服务的架构遍地开花,传统的Session-Cookie方案已不能满足项目的要求。因为项目集群化以后,不能在每台服务器上单独管理会话,需要一个集中管理的方式。通俗的说就是如何在集群环境中实现Session共享
Spring总是为开发者着想,所以为了解决上面的问题,开发了一个名为Spring Session的框架结合其对Redis的实现可以完美的解决问题。
[!Spring Session官方文档]docs.spring.io/spring-sess…
使用
Spring Boot版本 2.5.6
pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.4.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.5.6</version>
</dependency>
application配置文件
spring:
redis:
port: 16379
database: 1 #Redis相关配置
启动类
@SpringBootApplication
@EnableRedisHttpSession //开启以Redis作为基础存储设施的HttpSession功能
public class SessionApplication {
public static void main(String[] args) {
SpringApplication.run(SessionApplication.class,args);
}
}
测试
@RestController
@RequestMapping("/session")
public class SessionController {
@GetMapping("/set")
public void session(HttpServletRequest request,String name) {
request.getSession().setAttribute("name",name);
}
@GetMapping("/current")
public String getName(HttpServletRequest request){
return (String) request.getSession().getAttribute("name");
}
}
然后在IDEA中启动两个JVM进程端口号分别是8080和8090,模拟集群部署的方案。
先在8089的服务上设置,设置当前用户的姓名为张三,调用如下
http://localhost:8089/session/set?name=张三
然后在8080和8090两个服务上分别调用/session/current
接口获取当前用户信息
可以看到都是能获取到用户信息的?那么不免就有一个疑问了,为什么在8089上设置用户信息,能在8080上也获取到呢?原因是因为设置的用户信息,被统一存储到了Redis中。
那么其中的执行过程又是如何的呢?接下来分析Spring Session的流程
源码分析
@EnableRedisHttpSession
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({RedisHttpSessionConfiguration.class}) //开启基于Redis的Http Session的配置
@Configuration(
proxyBeanMethods = false
)
public @interface EnableRedisHttpSession {
int maxInactiveIntervalInSeconds() default 1800; //Session的最大有效时间 30分钟
String redisNamespace() default "spring:session";
/** @deprecated */
@Deprecated
RedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;
FlushMode flushMode() default FlushMode.ON_SAVE;
String cleanupCron() default "0 * * * * *"; //默认执行清除
SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}
这个注解的目的,就是告诉Spring容器,让它帮我们加载RedisHttpSessionConfiguration
类,所以让我们看看这个配置类
RedisHttpSessionConfiguration
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration ...{
//注册一个RedisIndexedSessionRepository的Bean,目的是实现对Redis的读取和写入操作,底层用的还是RedisTeamplate而已
@Bean
public RedisIndexedSessionRepository sessionRepository() {
RedisTemplate<Object, Object> redisTemplate = this.createRedisTemplate();
RedisIndexedSessionRepository sessionRepository = new RedisIndexedSessionRepository(redisTemplate);
sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
if (this.indexResolver != null) {
sessionRepository.setIndexResolver(this.indexResolver);
}
if (this.defaultRedisSerializer != null) {
sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
}
sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
if (StringUtils.hasText(this.redisNamespace)) {
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
}
sessionRepository.setFlushMode(this.flushMode);
sessionRepository.setSaveMode(this.saveMode);
int database = this.resolveDatabase();
sessionRepository.setDatabase(database);
this.sessionRepositoryCustomizers.forEach((sessionRepositoryCustomizer) -> {
sessionRepositoryCustomizer.customize(sessionRepository);
});
return sessionRepository;
}
...
}
这时候还有一个疑问:当我们在使用request.getSession()
的时候,是如何调用了redisTemplate
的查询呢?
注意到一个类SpringHttpSessionConfiguration
玄机就这它里面
SpringHttpSessionConfiguration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
//注册了一个过滤器,这个过滤器中使用到了SessionRepository
@Bean
public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter(sessionRepository);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
}
SessionRepository
是RedisIndexedSessionRepository
的抽象而已,所以可以断定一切对Session的管理都在这个过滤器中实现的
SessionRepositoryFilter
public class SessionRepositoryFilter<S extends Session> extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//在请求中设置SessionReporitory方便后续使用
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//将原始的request和response包装为Session相关
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryFilter.SessionRepositoryRequestWrapper(request, response);
SessionRepositoryFilter.SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryFilter.SessionRepositoryResponseWrapper(wrappedRequest, response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally {
//方法执行完后对Session操作的回调
wrappedRequest.commitSession();
}
}
}
也就是说当我们执行request.getSession
的时候,其实并不是调用了HttpServletRequest的getSession
方法,而是调用了
SessionRepositoryRequestWrapper
中的getSession
方法
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
//这里就是真正被执行getSession的方法
public SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper getSession(boolean create) {
//获取当前的会话信息
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper currentSession = this.getCurrentSession();
if (currentSession != null) {
return currentSession;
} else {
S requestedSession = this.getRequestedSession();
if (requestedSession != null) {
if (this.getAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR) == null) {
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(requestedSession, this.getServletContext());
currentSession.markNotNew();
this.setCurrentSession(currentSession);
return currentSession;
}
} else {
this.setAttribute(SessionRepositoryFilter.INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
} else if (SessionRepositoryFilter.this.httpSessionIdResolver instanceof CookieHttpSessionIdResolver && this.response.isCommitted()) {
throw new IllegalStateException("Cannot create a session after the response has been committed");
} else {
//创建新的Session会话
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
session.setLastAccessedTime(Instant.now());
currentSession = new SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper(session, this.getServletContext());
//设置当前会话
this.setCurrentSession(currentSession);
return currentSession;
}
}
}
private void commitSession() {
SessionRepositoryFilter<S>.SessionRepositoryRequestWrapper.HttpSessionWrapper wrappedSession = this.getCurrentSession();
if (wrappedSession == null) {
//不合法的Session
if (this.isInvalidateClientSession()) {
//清除Session
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
} else {
S session = wrappedSession.getSession();
this.clearRequestedSessionCache();
//保存Sesssion到Redis中
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
//Session有效,并且sessionId变化了,将SessionId重新写入Cookie中,后回写到客户端
if (!this.isRequestedSessionIdValid() || !sessionId.equals(this.getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
}
总结
一句话描述一Spring Session的流程:当我们开启了@EnableRedisHttpSession
,Spring Boot会注册一个名为SessionRepositoryFilter
的过滤器,这个过滤器的作用将原始的HttpServletRequest
和HttpServletReponse
包装为SessionRepositoryRequestWrapper
和SessionRepositoryResponseWrapper
,其中SessionRepositoryRequestWrapper
重写了getSession
的方法,实现了从Redis从中读取缓存、设置过期时间等操作。
我们应该学到的东西:
- 可以在Filter中包装HttpServletRequest,实现对request的增强,其实这种做法在SpringCloud GateWay是很常见的。
- 回顾一下装饰者设计模式
- Spring Session的基本使用
- 回顾代理和委派设计模式