title: Spring Session 源码解读
date: 2021/02/21 11:00
概述
本文基于以下组合的应用:
Spring Boot 2.1.3.RELEASE
- SessionAutoConfiguration(@Conditional(Spring Session Core))
- RedisSessionConfiguration(@Conditional(Spring Session Data Redis))
Spring Session Core 2.1.4.RELEASE
Spring Session Data Redis 2.1.3.RELEASE
其中 Spring Session Core 提供了部分接口交由子类实现,一般我们用的实现就是 Spring Session Data Redis
SessionAutoConfiguration
package org.springframework.boot.autoconfigure.session;
// 省略 imports 行
/**
* EnableAutoConfiguration Auto-configuration for Spring Session.
*
* @since 1.4.0
*/
// 声明这是一个配置类
@Configuration
// 仅在类 Session 存在于 classpath 时候才生效,
// Session 类由包 Spring Session Core 提供
@ConditionalOnClass(Session.class)
// 仅在当前应用是 Web 应用时才生效 : Servlet Web 应用, Reactive Web 应用都可以
@ConditionalOnWebApplication
// 确保如下前缀的配置属性的加载到如下 bean :
// server ==> ServerProperties
// spring.session ==> SessionProperties
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })
// 当前配置必须在指定的自动配置结束之后进行,这里虽然列出了很多,但同一应用中它们未必
// 都存在,这里指的是当前应用中如果它们中间某些存在的话,SessionAutoConfiguration
// 自动配置的执行必须要在这些自动配置结束之后完成,本文的分析使用 Redis 支持 Spring Session,
// 并且是 Servlet Web 应用,所以 RedisAutoConfiguration 会被启用
// 为什么要在 RedisAutoConfiguration 之后执行?
// 因为当前配置类会引入 RedisSessionConfiguration 其需要 RedisAutoConfiguration 自动装配的 RedisConnectionFactory。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class, MongoDataAutoConfiguration.class,
MongoReactiveDataAutoConfiguration.class, RedisAutoConfiguration.class,
RedisReactiveAutoConfiguration.class })
// 在自动配置 HttpHandlerAutoConfiguration 执行前执行
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
public class SessionAutoConfiguration {
// 内嵌配置子类,针对 Servlet Web 的情况
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
// 1. 导入 ServletSessionRepositoryValidator,确保存储库类型被指定以及相应的存储库类的存在(校验用户填写的 spring.session.store-type 属性,校验容器中有 SessionRepository);
// 2. 导入 SessionRepositoryFilterConfiguration,配置 “注册 SessionRepositoryFilter 到 Servlet 容器”的 FilterRegistrationBean
@Import({ ServletSessionRepositoryValidator.class,
SessionRepositoryFilterConfiguration.class })
static class ServletSessionConfiguration {
// 定义一个 bean cookieSerializer
@Bean
// 仅在条件 DefaultCookieSerializerCondition 被满足时才生效
// 1. Bean HttpSessionIdResolver 和 CookieSerializer 都不存在
// 或者
// 2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
@Conditional(DefaultCookieSerializerCondition.class)
public DefaultCookieSerializer cookieSerializer(
ServerProperties serverProperties) {
Cookie cookie = serverProperties.getServlet().getSession().getCookie();
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(cookie::getName).to(cookieSerializer::setCookieName);
map.from(cookie::getDomain).to(cookieSerializer::setDomainName);
map.from(cookie::getPath).to(cookieSerializer::setCookiePath);
map.from(cookie::getHttpOnly).to(cookieSerializer::setUseHttpOnlyCookie);
map.from(cookie::getSecure).to(cookieSerializer::setUseSecureCookie);
map.from(cookie::getMaxAge).to((maxAge) -> cookieSerializer
.setCookieMaxAge((int) maxAge.getSeconds()));
return cookieSerializer;
}
// 内嵌配置类
// 该类自身没有提供任何实现,其效果主要通过注解来实现 :
// 仅在 bean SessionRepository 不存在时导入 ServletSessionRepositoryImplementationValidator 和 ServletSessionConfigurationImportSelector
// ServletSessionRepositoryImplementationValidator:检查类路径下是否有多个 SessionRepository 实现类,如果有多个则检查是否指定了 StoreType,如果没指定则报错
// ServletSessionConfigurationImportSelector:引入所有支持的类型的自动配置类,例如 Redis 的是 RedisSessionConfiguration
@Configuration
@ConditionalOnMissingBean(SessionRepository.class)
@Import({ ServletSessionRepositoryImplementationValidator.class,
ServletSessionConfigurationImportSelector.class })
static class ServletSessionRepositoryConfiguration {
}
}
// 内嵌配置子类,针对 Reactive Web 的情况
@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@Import(ReactiveSessionRepositoryValidator.class)
static class ReactiveSessionConfiguration {
// 内嵌配置类
// 该类自身没有提供任何实现,其效果主要通过注解来实现 :
// 仅在 bean ReactiveSessionRepository 不存在时导入 ReactiveSessionRepositoryImplementationValidator
// 和 ReactiveSessionConfigurationImportSelector
@Configuration
// 仅在 bean ReactiveSessionRepository 不存在时生效
@ConditionalOnMissingBean(ReactiveSessionRepository.class)
// 导入 ReactiveSessionRepositoryImplementationValidator
// 和 ReactiveSessionConfigurationImportSelector
@Import({ ReactiveSessionRepositoryImplementationValidator.class,
ReactiveSessionConfigurationImportSelector.class })
static class ReactiveSessionRepositoryConfiguration {
}
}
/**
* Condition to trigger the creation of a DefaultCookieSerializer. This kicks
* in if either no HttpSessionIdResolver and CookieSerializer beans
* are registered, or if CookieHttpSessionIdResolver is registered but
* CookieSerializer is not.
* 触发创建 DefaultCookieSerializer 的条件 :
* 1. Bean HttpSessionIdResolver 和 CookieSerializer 都不存在
* 或者
* 2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
*
* DefaultCookieSerializerCondition 是一个 AnyNestedCondition,
* 这种条件被满足的条件是:某个内嵌子条件被满足
*/
static class DefaultCookieSerializerCondition extends AnyNestedCondition {
DefaultCookieSerializerCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnMissingBean({ HttpSessionIdResolver.class, CookieSerializer.class })
static class NoComponentsAvailable {
}
@ConditionalOnBean(CookieHttpSessionIdResolver.class)
@ConditionalOnMissingBean(CookieSerializer.class)
static class CookieHttpSessionIdResolverAvailable {
}
}
/**
* ImportSelector base class to add StoreType configuration classes.
* 抽象基类,提供工具方法用于不同 Web 环境下决定导入哪些 Session Store 配置类
*/
abstract static class SessionConfigurationImportSelector implements ImportSelector {
protected final String[] selectImports(WebApplicationType webApplicationType) {
List imports = new ArrayList<>();
StoreType[] types = StoreType.values();
for (int i = 0; i < types.length; i++) {
imports.add(SessionStoreMappings.getConfigurationClass(webApplicationType,
types[i]));
}
return StringUtils.toStringArray(imports);
}
}
/**
* ImportSelector to add StoreType configuration classes for reactive
* web applications.
* 在 Reactive Web 情况下使用,用于导入相应的 Session Store 配置类
*/
static class ReactiveSessionConfigurationImportSelector
extends SessionConfigurationImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return super.selectImports(WebApplicationType.REACTIVE);
}
}
/**
* ImportSelector to add StoreType configuration classes for Servlet
* web applications.
* 在 Servlet Web 情况下使用,用于导入相应的 Session Store 配置类
*/
static class ServletSessionConfigurationImportSelector
extends SessionConfigurationImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return super.selectImports(WebApplicationType.SERVLET);
}
}
/**
* Base class for beans used to validate that only one supported implementation is
* available in the classpath when the store-type property is not set.
* 抽象基类,用于检查 store type 未设置的情况下仅有一个session repository 实现类存在于 classpath
*/
abstract static class AbstractSessionRepositoryImplementationValidator {
private final List candidates;
private final ClassLoader classLoader;
private final SessionProperties sessionProperties;
AbstractSessionRepositoryImplementationValidator(
ApplicationContext applicationContext,
SessionProperties sessionProperties, List candidates) {
this.classLoader = applicationContext.getClassLoader();
this.sessionProperties = sessionProperties;
this.candidates = candidates;
}
@PostConstruct
public void checkAvailableImplementations() {
List> availableCandidates = new ArrayList<>();
for (String candidate : this.candidates) {
addCandidateIfAvailable(availableCandidates, candidate);
}
StoreType storeType = this.sessionProperties.getStoreType();
if (availableCandidates.size() > 1 && storeType == null) {
// 这里通过异常方式确保storeType 属性未设置时必须只有一个session存储库实现类存在
throw new NonUniqueSessionRepositoryException(availableCandidates);
}
}
// 对类型 type 进行检查,如果该类型对应的类能够被 classLoader 加载成功,则将其作为候选类,
// 也就是添加到列表 candidates 中,否则该类型 type 不作为候选。
private void addCandidateIfAvailable(List> candidates, String type) {
try {
Class> candidate = this.classLoader.loadClass(type);
if (candidate != null) {
candidates.add(candidate);
}
}
catch (Throwable ex) {
// Ignore
}
}
}
/**
* Bean used to validate that only one supported implementation is available in the
* classpath when the store-type property is not set.
*/
static class ServletSessionRepositoryImplementationValidator
extends AbstractSessionRepositoryImplementationValidator {
ServletSessionRepositoryImplementationValidator(
ApplicationContext applicationContext,
SessionProperties sessionProperties) {
super(applicationContext, sessionProperties, Arrays.asList(
"org.springframework.session.hazelcast.HazelcastSessionRepository",
"org.springframework.session.jdbc.JdbcOperationsSessionRepository",
"org.springframework.session.data.mongo.MongoOperationsSessionRepository",
"org.springframework.session.data.redis.RedisOperationsSessionRepository"));
}
}
/**
* Bean used to validate that only one supported implementation is available in the
* classpath when the store-type property is not set.
*/
static class ReactiveSessionRepositoryImplementationValidator
extends AbstractSessionRepositoryImplementationValidator {
ReactiveSessionRepositoryImplementationValidator(
ApplicationContext applicationContext,
SessionProperties sessionProperties) {
super(applicationContext, sessionProperties, Arrays.asList(
"org.springframework.session.data.redis.ReactiveRedisOperationsSessionRepository",
"org.springframework.session.data.mongo.ReactiveMongoOperationsSessionRepository"));
}
}
/**
* Base class for validating that a (reactive) session repository bean exists.
* 抽象基类,用于确保只有一个 session repository bean 实例存在,如果有多个,则抛出异常
*/
abstract static class AbstractSessionRepositoryValidator {
private final SessionProperties sessionProperties;
private final ObjectProvider> sessionRepositoryProvider;
protected AbstractSessionRepositoryValidator(SessionProperties sessionProperties,
ObjectProvider> sessionRepositoryProvider) {
this.sessionProperties = sessionProperties;
this.sessionRepositoryProvider = sessionRepositoryProvider;
}
@PostConstruct
public void checkSessionRepository() {
StoreType storeType = this.sessionProperties.getStoreType();
if (storeType != StoreType.NONE
&& this.sessionRepositoryProvider.getIfAvailable() == null
&& storeType != null) {
throw new SessionRepositoryUnavailableException(
"No session repository could be auto-configured, check your "
+ "configuration (session store type is '"
+ storeType.name().toLowerCase(Locale.ENGLISH) + "')",
storeType);
}
}
}
/**
* Bean used to validate that a SessionRepository exists and provide a
* meaningful message if that's not the case.
*/
static class ServletSessionRepositoryValidator
extends AbstractSessionRepositoryValidator {
ServletSessionRepositoryValidator(SessionProperties sessionProperties,
ObjectProvider> sessionRepositoryProvider) {
super(sessionProperties, sessionRepositoryProvider);
}
}
/**
* Bean used to validate that a ReactiveSessionRepository exists and provide a
* meaningful message if that's not the case.
*/
static class ReactiveSessionRepositoryValidator
extends AbstractSessionRepositoryValidator {
ReactiveSessionRepositoryValidator(SessionProperties sessionProperties,
ObjectProvider> sessionRepositoryProvider) {
super(sessionProperties, sessionRepositoryProvider);
}
}
}
从以上源代码可以看出,SessionAutoConfiguration
自身没有提供任何配置方法或者进行任何bean
定义,其配置效果主要通过自身所使用的注解和它的嵌套配置类来完成。
SessionAutoConfiguration
自身的注解约定了如下配置效果 :
-
SessionAutoConfiguration
生效的条件
- 类
Session
必须存在于classpath
上,换句话讲,也就是要求必须依赖包Spring Session Core
; - 当前应用必须是一个
Web
应用,Servlet Web
应用,Reactive Web
应用均可
- 类
导入了如下配置到相应的
bean
– 前缀为server
的配置项到类型为ServerProperties
的bean
– 前缀为spring.session
的配置项到类型为SessionProperties
的bean
-
SessionAutoConfiguration
自动配置(以及嵌套配置)的执行时机
-
在以下自动配置执行之后
这些自动配置主要是配置
Spring Session
存储库机制所使用底层基础设施,所以要在SessionAutoConfiguration
之前完成DataSourceAutoConfiguration
HazelcastAutoConfiguration
JdbcTemplateAutoConfiguration
MongoDataAutoConfiguration
MongoReactiveDataAutoConfiguration
RedisAutoConfiguration
-
在以下自动配置执行之前
HttpHandlerAutoConfiguration
-
ServletSessionConfiguration
ServletSessionConfiguration
配置类定义了一个bean
:
-
DefaultCookieSerializer cookieSerializer
仅在条件
DefaultCookieSerializerCondition
被满足时定义 :-
Bean HttpSessionIdResolver
和CookieSerializer
都不存在 或者 -
Bean CookieHttpSessionIdResolver
存在 但bean CookieSerializer
不存在
-
-
导入配置类 SessionRepositoryFilterConfiguration 用于配置注册 SessionRepositoryFilter 到 Servlet 容器的 FilterRegistrationBean
SessionRepositoryFilter
是Spring Session
机制在运行时工作的核心组件,用于服务用户请求处理过程中所有HttpSession
操作请求 导入验证器组件
ServletSessionRepositoryValidator
确保只存在一个SessionRepository bean
或者指定的SessionRepository bean
存在-
定义嵌套配置类 ServletSessionRepositoryConfiguration
仅在
bean SessionRepository
不存在时生效-
导入 ServletSessionConfigurationImportSelector 以选择合适的存储库配置类
针对本文所使用的应用的情形,最终会选择
RedisSessionConfiguration
。
RedisSessionConfiguration
配置类会应用sping.session
/spring.session.redis
为前缀的配置项,并定义如下bean
:-
RedisOperationsSessionRepository sessionRepository
, (重要) -
RedisMessageListenerContainer redisMessageListenerContainer
, -
InitializingBean enableRedisKeyspaceNotificationsInitializer
, -
SessionRepositoryFilter springSessionRepositoryFilter
, (重要) -
SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter
,
-
导入验证器组件
ServletSessionRepositoryImplementationValidator
以确保相应的存储库配置类存在于classpath
总结
- 检查用户配置的 StoreType 的正确性
- 配置 SessionRepositoryFilter 到 Servlet 容器中
- 配置默认的 DefaultCookieSerializer
- 检查当前类路径下至少存在一个 SessionRepository 的实现
- 导入配置类 RedisSessionConfiguration
RedisSessionConfiguration
上面的 ServletSessionConfigurationImportSelector 会导入一系列配置类,但是配置类上有条件(@Conditional),所以在本例中只有 RedisSessionConfiguration 会生效。
package org.springframework.boot.autoconfigure.session;
// 省略 import 行
@Configuration
// 仅在指定类存在于 classpath 上时才生效
@ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class })
// 仅在 bean SessionRepository 不存在时才生效
@ConditionalOnMissingBean(SessionRepository.class)
// 仅在 bean RedisConnectionFactory 存在时才生效
@ConditionalOnBean(RedisConnectionFactory.class)
// 仅在条件 ServletSessionCondition 被满足时才生效
@Conditional(ServletSessionCondition.class)
// 确保前缀为 spring.session.redis 的配置参数被加载到 bean RedisSessionProperties
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {
// 配置 redis 的键空间通知功能,notify-keyspace-events Exg,这里先买个关子。
@Bean
@ConditionalOnMissingBean
ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
switch (redisSessionProperties.getConfigureAction()) {
case NOTIFY_KEYSPACE_EVENTS:
return new ConfigureNotifyKeyspaceEventsAction();
case NONE:
return ConfigureRedisAction.NO_OP;
}
throw new IllegalStateException(
"Unsupported redis configure action '" + redisSessionProperties.getConfigureAction() + "'.");
}
// 内置配置类
// 1. 应用配置参数
// 2. 继承自 RedisHttpSessionConfiguration 以定义 sessionRepository,springSessionRepositoryFilter 等运行时工作组件 bean
@Configuration
public static class SpringBootRedisHttpSessionConfiguration
extends RedisHttpSessionConfiguration {
// 应用用户配置的 spring.session.redis 属性
@Autowired
public void customize(SessionProperties sessionProperties,
RedisSessionProperties redisSessionProperties) {
Duration timeout = sessionProperties.getTimeout();
if (timeout != null) {
setMaxInactiveIntervalInSeconds((int) timeout.getSeconds());
}
setRedisNamespace(redisSessionProperties.getNamespace());
setRedisFlushMode(redisSessionProperties.getFlushMode());
setCleanupCron(redisSessionProperties.getCleanupCron());
}
}
}
父类 RedisHttpSessionConfiguration
package org.springframework.session.data.redis.config.annotation.web.http;
// 省略 import 行
/**
* Exposes the SessionRepositoryFilter as a bean named
* springSessionRepositoryFilter. In order to use this a single
* RedisConnectionFactory must be exposed as a Bean.
*
* @since 1.0
*/
@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
SchedulingConfigurer {
// 清除过期 session 的定时任务的 cron 表达式,
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
// 会话被允许处于不活跃状态的最长时间, 超过该事件,会话会被认为是过期无效
// 使用缺省值 30 分钟
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
// 所创建的 session 在 redis 中的命名空间, 使用缺省值 : spring:session
private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
// 清除过期 session 的定时任务的 cron 表达式,
// 使用缺省值 : "0 * * * * *", 表示每个分钟的0秒执行一次
private String cleanupCron = DEFAULT_CLEANUP_CRON;
// 对 redis 的配置动作,缺省是 : notify-keyspace-events
// 该缺省值确保 redis keyspace 事件通知机制启用
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
// 创建连接到目标 redis 数据库的工厂类,由外部提供
private RedisConnectionFactory redisConnectionFactory;
private RedisSerializer
父类 SpringHttpSessionConfiguration
package org.springframework.session.config.annotation.web.http;
// 省略 import 行
@Configuration
public class SpringHttpSessionConfiguration implements ApplicationContextAware {
private final Log logger = LogFactory.getLog(getClass());
private CookieHttpSessionIdResolver defaultHttpSessionIdResolver =
new CookieHttpSessionIdResolver();
private boolean usesSpringSessionRememberMeServices;
private ServletContext servletContext;
private CookieSerializer cookieSerializer;
private HttpSessionIdResolver httpSessionIdResolver = this.defaultHttpSessionIdResolver;
private List httpSessionListeners = new ArrayList<>();
@PostConstruct
public void init() {
// 如果用户没有配置 CookieSerializer 则自己创建一个默认的
CookieSerializer cookieSerializer = (this.cookieSerializer != null)
? this.cookieSerializer
: createDefaultCookieSerializer();
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
// 定义bean SessionEventHttpSessionListenerAdapter,一个ApplicationListener,
// 它会监听 Spring Session 的事件 SessionDestroyedEvent,SessionCreatedEvent
// 并将其转换为HttpSessionEvent,然后转发给所注册的各个 HttpSessionListener
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
// 定义从 Servlet 容器层面可见的 Filter SessionRepositoryFilter, 它会对 Servlet 容器原生
// request/response 进行包装,从而拦截 HttpSession 的获取,创建和删除等操作,这些
// 操作最终会由底层的 Spring Session 机制支持,在本文所使用的项目例子中,其实就是
// 使用 redis 以及相关工作组件来支持 session
@Bean
public SessionRepositoryFilter extends Session>
springSessionRepositoryFilter(
SessionRepository sessionRepository) {
SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
if (ClassUtils.isPresent(
"org.springframework.security.web.authentication.RememberMeServices",
null)) {
this.usesSpringSessionRememberMeServices = !ObjectUtils
.isEmpty(applicationContext
.getBeanNamesForType(SpringSessionRememberMeServices.class));
}
}
@Autowired(required = false)
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Autowired(required = false)
public void setCookieSerializer(CookieSerializer cookieSerializer) {
this.cookieSerializer = cookieSerializer;
}
@Autowired(required = false)
public void setHttpSessionIdResolver(HttpSessionIdResolver httpSessionIdResolver) {
this.httpSessionIdResolver = httpSessionIdResolver;
}
@Autowired(required = false)
public void setHttpSessionListeners(List listeners) {
this.httpSessionListeners = listeners;
}
private CookieSerializer createDefaultCookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
if (this.servletContext != null) {
SessionCookieConfig sessionCookieConfig = null;
try {
sessionCookieConfig = this.servletContext.getSessionCookieConfig();
}
catch (UnsupportedOperationException ex) {
this.logger
.warn("Unable to obtain SessionCookieConfig: " + ex.getMessage());
}
if (sessionCookieConfig != null) {
if (sessionCookieConfig.getName() != null) {
cookieSerializer.setCookieName(sessionCookieConfig.getName());
}
if (sessionCookieConfig.getDomain() != null) {
cookieSerializer.setDomainName(sessionCookieConfig.getDomain());
}
if (sessionCookieConfig.getPath() != null) {
cookieSerializer.setCookiePath(sessionCookieConfig.getPath());
}
if (sessionCookieConfig.getMaxAge() != -1) {
cookieSerializer.setCookieMaxAge(sessionCookieConfig.getMaxAge());
}
}
}
if (this.usesSpringSessionRememberMeServices) {
cookieSerializer.setRememberMeRequestAttribute(
SpringSessionRememberMeServices.REMEMBER_ME_LOGIN_ATTR);
}
return cookieSerializer;
}
}
总结
- 注入 ConfigureRedisAction 以 redis 启用键空间通知功能
- 应用用户配置的
spring.session
和spring.session.redis
属性 - 配置 RedisIndexedSessionRepository
- 配置 RedisMessageListenerContainer 来处理 redis 的消息
- 执行 ConfigureRedisAction 启用键空间通知功能
- 注册后台执行任务,以清除过期的 session
- 如果 CookieSerializer 为空则创建默认的,并设置到 CookieHttpSessionIdResolver 中
- 创建 SessionEventHttpSessionListenerAdapter 以监听 Spring Session 相关的事件
- 创建 SessionRepositoryFilter
SessionRepositoryFilterConfiguration
SessionAutoConfiguration 引入的用来配置 SessionRepositoryFilter 的配置类。
package org.springframework.boot.autoconfigure.session;
// 省略 import 行
@Configuration
// 在 bean SessionRepositoryFilter 存在的的情况下才生效
@ConditionalOnBean(SessionRepositoryFilter.class)
// 确保配置属性项 server.session.* 提取到 bean SessionProperties
@EnableConfigurationProperties(SessionProperties.class)
class SessionRepositoryFilterConfiguration {
// 定义bean FilterRegistrationBean, 这是一个过滤器注册bean,它的任务是将一个过滤器注册到
// Servlet 容器,这里的过滤器指的就是 bean SessionRepositoryFilter
@Bean
public FilterRegistrationBean> sessionRepositoryFilterRegistration(
SessionProperties sessionProperties, SessionRepositoryFilter> filter) {
FilterRegistrationBean> registration = new FilterRegistrationBean<>(
filter);
registration.setDispatcherTypes(getDispatcherTypes(sessionProperties));
registration.setOrder(sessionProperties.getServlet().getFilterOrder());
return registration;
}
// 从配置属性项中获取所指定的 DispatcherType 集合,如果配置属性中没有指定该信息,则使用
// 缺省值 : DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST
private EnumSet getDispatcherTypes(
SessionProperties sessionProperties) {
SessionProperties.Servlet servletProperties = sessionProperties.getServlet();
if (servletProperties.getFilterDispatcherTypes() == null) {
return null;
}
return servletProperties.getFilterDispatcherTypes().stream()
.map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors
.collectingAndThen(Collectors.toSet(), EnumSet::copyOf));
}
}
SessionRepositoryFilter
先不点进去了,先看下 request.getSession() 的流程
request#getSession()
看一下 RedisSession 吧
final class RedisSession implements Session {
// 缓存对象、委托对象,这个类中的所有方法几乎都是委托这个对象来做的
private final MapSession cached;
private Instant originalLastAccessTime;
// 存储 Session 域中的数据
private Map delta = new HashMap<>();
// 标识当前 Session 是否是新创建的,当修改了 SessionId的时候用来决定是否使用 rename 命令
private boolean isNew;
private String originalPrincipalName;
// 当前会话的 id
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
// 如果是新创建的,则添加几个属性
if (this.isNew) {
this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
(int) cached.getMaxInactiveInterval().getSeconds());
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
}
if (this.isNew || (RedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
cached.getAttribute(attributeName)));
}
}
@Override
public void setLastAccessedTime(Instant lastAccessedTime) {
this.cached.setLastAccessedTime(lastAccessedTime);
this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
flushImmediateIfNecessary();
}
@Override
public boolean isExpired() {
return this.cached.isExpired();
}
@Override
public Instant getCreationTime() {
return this.cached.getCreationTime();
}
@Override
public String getId() {
return this.cached.getId();
}
@Override
public String changeSessionId() {
return this.cached.changeSessionId();
}
@Override
public Instant getLastAccessedTime() {
return this.cached.getLastAccessedTime();
}
@Override
public void setMaxInactiveInterval(Duration interval) {
this.cached.setMaxInactiveInterval(interval);
this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
flushImmediateIfNecessary();
}
@Override
public Duration getMaxInactiveInterval() {
return this.cached.getMaxInactiveInterval();
}
@Override
public T getAttribute(String attributeName) {
T attributeValue = this.cached.getAttribute(attributeName);
if (attributeValue != null
&& RedisIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
}
return attributeValue;
}
@Override
public Set getAttributeNames() {
return this.cached.getAttributeNames();
}
@Override
public void setAttribute(String attributeName, Object attributeValue) {
this.cached.setAttribute(attributeName, attributeValue);
this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
flushImmediateIfNecessary();
}
@Override
public void removeAttribute(String attributeName) {
this.cached.removeAttribute(attributeName);
this.delta.put(getSessionAttrNameKey(attributeName), null);
flushImmediateIfNecessary();
}
private void flushImmediateIfNecessary() {
if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
save();
}
}
// wrappedRequest.commitSession() 会调用这个方法
private void save() {
saveChangeSessionId();
saveDelta();
}
/**
* Saves any attributes that have been changed and updates the expiration of this
* session.
*/
private void saveDelta() {
if (this.delta.isEmpty()) {
return;
}
String sessionId = getId();
// 将数据保存到 redis 中
getSessionBoundHashOperations(sessionId).putAll(this.delta);
// 下面这部分好像和 redis 索引和安全相关,等学完 redis 再看吧
String principalSessionKey = getSessionAttrNameKey(
FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
if (this.originalPrincipalName != null) {
String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
.remove(sessionId);
}
Map indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
this.originalPrincipalName = principal;
if (principal != null) {
String principalRedisKey = getPrincipalKey(principal);
RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
.add(sessionId);
}
}
// 将当前 Session 中的 delta 清空
this.delta = new HashMap<>(this.delta.size());
// 计算过期时间
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
// 设置过期时间
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
private void saveChangeSessionId() {
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) {
return;
}
// 如果不是新创建的 Session 对象,则使用 rename 重命名 key
if (!this.isNew) {
String originalSessionIdKey = getSessionKey(this.originalSessionId);
String sessionIdKey = getSessionKey(sessionId);
try {
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
sessionIdKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
// 获取到和过期时间相关的两个 key,对他们重命名
// "spring:session:expirations:1523934840000"
// "spring:session:sessions:expires:39feb101-87d4-42c7-ab53-ac6fe0d91925"
String originalExpiredKey = getExpiredKey(this.originalSessionId);
String expiredKey = getExpiredKey(sessionId);
try {
RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
}
catch (NonTransientDataAccessException ex) {
handleErrNoSuchKeyError(ex);
}
}
// 将最新的 sessionId 赋值到 originalSessionId
this.originalSessionId = sessionId;
}
private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
throw ex;
}
}
}
问题:delta 中没有存 redis 中已有的数据,加入操作 session 的时候数据库中的信息过期了,那么就会有数据丢失了。看代码好像吧 SaveModel 改为 ALWAYS 可以解决这个问题。
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
看接下来的内容之前强烈建议看下这篇文章:https://www.iocoder.cn/Spring-Session/laoxu/spring-session-4/?self
太乱了,总结一下:
- 对过期时间四舍五入到下一分钟
- 将 B 类型键移动到新的过期时间桶中,设置过期时间为 35min
- 将 C 类型键(它相当于 A 类型键的引用),设置过期时间为 30min
- 设置 A 类型键的过期时间为 35min
后台执行删除操作的线程(在 RedisHttpSessionConfiguration 中配置的)
回头看下 request#getSession() 中的
很简单,就是从数据库里找到 A 类型键,获取他的数据,封装成 MapSession,判断是否过期,再次封装成 RedisSession。
过期咋接收的 notify-keyspace-events Exg
先了解下键空间通知功能:http://doc.redisfans.com/topic/notification.html
这里有一个使用键空间通知功能的一个 demo,可以看下:
- https://blog.csdn.net/liuchuanhong1/article/details/70147149
- https://zhuanlan.zhihu.com/p/59065399
我们再回头看 RedisHttpSessionConfiguration 中配置的 RedisMessageListenerContainer:
RedisHttpSessionConfiguration.java
@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
RedisIndexedSessionRepository sessionRepository) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(this.redisConnectionFactory);
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
// 其中 sessionRepository 是 MessageListener 的实现
// 配置了两个监听的 topic 分别是:
// __keyevent@0__:del
// __keyevent@0__:expired
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
// 这里配置了一个基于模式匹配的 topic:spring:session:event:0:created:*
container.addMessageListener(sessionRepository,
Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
return container;
}
RedisIndexedSessionRepository.java
@Override
public void onMessage(Message message, byte[] pattern) {
byte[] messageChannel = message.getChannel();
byte[] messageBody = message.getBody();
String channel = new String(messageChannel);
// 如果 topic 是以 spring:session:event:3:created: 开头的,则发布 SessionCreatedEvent
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
// TODO: is this thread safe?
@SuppressWarnings("unchecked")
Map loaded = (Map) this.defaultSerializer.deserialize(message.getBody());
handleCreated(loaded, channel);
return;
}
String body = new String(messageBody);
if (!body.startsWith(getExpiredKeyPrefix())) {
return;
}
// 判断是否是 __keyevent@0__:del 或 __keyevent@0__:expired Channel 的。
boolean isDeleted = channel.equals(this.sessionDeletedChannel);
if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
int beginIndex = body.lastIndexOf(":") + 1;
int endIndex = body.length();
String sessionId = body.substring(beginIndex, endIndex);
RedisSession session = getSession(sessionId, true);
if (session == null) {
logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
cleanupPrincipalIndex(session);
// 如果是删除则触发 SessionDeletedEvent
if (isDeleted) {
handleDeleted(session);
}
// 如果是过期则触发 SessionExpiredEvent
else {
handleExpired(session);
}
}
}
但是 Spring 并没有写这些事件的监听器,是留给我们的一个钩子。他把 A 类型键设置多 5min 也是为了让我们在这段时间内做一些我们想做的事,比如日志记录等。
其实,如果说 Spring 使用 A 类型、B 类型、C 类型键来保证到指定时间会释放内存是错误的,因为A 类型键实际上还是由 redis 来清除的,而且增加了 B、C 类型键会增加 redis 的消耗,所以他这样做的目的就是为了让我们在这段时间内做一些我们想做的事。