title: Spring Session 源碼解讀
date: 2021/02/21 11:00
概述
本文基于以下組合的應(yīng)用:
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 提供了部分接口交由子類實(shí)現(xiàn)线定,一般我們用的實(shí)現(xiàn)就是 Spring Session Data Redis
SessionAutoConfiguration
package org.springframework.boot.autoconfigure.session;
// 省略 imports 行
/**
* EnableAutoConfiguration Auto-configuration for Spring Session.
*
* @since 1.4.0
*/
// 聲明這是一個(gè)配置類
@Configuration
// 僅在類 Session 存在于 classpath 時(shí)候才生效,
// Session 類由包 Spring Session Core 提供
@ConditionalOnClass(Session.class)
// 僅在當(dāng)前應(yīng)用是 Web 應(yīng)用時(shí)才生效 : Servlet Web 應(yīng)用, Reactive Web 應(yīng)用都可以
@ConditionalOnWebApplication
// 確保如下前綴的配置屬性的加載到如下 bean :
// server ==> ServerProperties
// spring.session ==> SessionProperties
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })
// 當(dāng)前配置必須在指定的自動(dòng)配置結(jié)束之后進(jìn)行,這里雖然列出了很多,但同一應(yīng)用中它們未必
// 都存在辩尊,這里指的是當(dāng)前應(yīng)用中如果它們中間某些存在的話骑脱,SessionAutoConfiguration
// 自動(dòng)配置的執(zhí)行必須要在這些自動(dòng)配置結(jié)束之后完成,本文的分析使用 Redis 支持 Spring Session,
// 并且是 Servlet Web 應(yīng)用,所以 RedisAutoConfiguration 會(huì)被啟用
// 為什么要在 RedisAutoConfiguration 之后執(zhí)行沦偎?
// 因?yàn)楫?dāng)前配置類會(huì)引入 RedisSessionConfiguration 其需要 RedisAutoConfiguration 自動(dòng)裝配的 RedisConnectionFactory咐吼。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class,
JdbcTemplateAutoConfiguration.class, MongoDataAutoConfiguration.class,
MongoReactiveDataAutoConfiguration.class, RedisAutoConfiguration.class,
RedisReactiveAutoConfiguration.class })
// 在自動(dòng)配置 HttpHandlerAutoConfiguration 執(zhí)行前執(zhí)行
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
public class SessionAutoConfiguration {
// 內(nèi)嵌配置子類吹缔,針對(duì) Servlet Web 的情況
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
// 1. 導(dǎo)入 ServletSessionRepositoryValidator,確保存儲(chǔ)庫類型被指定以及相應(yīng)的存儲(chǔ)庫類的存在(校驗(yàn)用戶填寫的 spring.session.store-type 屬性锯茄,校驗(yàn)容器中有 SessionRepository)厢塘;
// 2. 導(dǎo)入 SessionRepositoryFilterConfiguration,配置 “注冊(cè) SessionRepositoryFilter 到 Servlet 容器”的 FilterRegistrationBean
@Import({ ServletSessionRepositoryValidator.class,
SessionRepositoryFilterConfiguration.class })
static class ServletSessionConfiguration {
// 定義一個(gè) bean cookieSerializer
@Bean
// 僅在條件 DefaultCookieSerializerCondition 被滿足時(shí)才生效
// 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;
}
// 內(nèi)嵌配置類
// 該類自身沒有提供任何實(shí)現(xiàn)肌幽,其效果主要通過注解來實(shí)現(xiàn) :
// 僅在 bean SessionRepository 不存在時(shí)導(dǎo)入 ServletSessionRepositoryImplementationValidator 和 ServletSessionConfigurationImportSelector
// ServletSessionRepositoryImplementationValidator:檢查類路徑下是否有多個(gè) SessionRepository 實(shí)現(xiàn)類晚碾,如果有多個(gè)則檢查是否指定了 StoreType,如果沒指定則報(bào)錯(cuò)
// ServletSessionConfigurationImportSelector:引入所有支持的類型的自動(dòng)配置類喂急,例如 Redis 的是 RedisSessionConfiguration
@Configuration
@ConditionalOnMissingBean(SessionRepository.class)
@Import({ ServletSessionRepositoryImplementationValidator.class,
ServletSessionConfigurationImportSelector.class })
static class ServletSessionRepositoryConfiguration {
}
}
// 內(nèi)嵌配置子類格嘁,針對(duì) Reactive Web 的情況
@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@Import(ReactiveSessionRepositoryValidator.class)
static class ReactiveSessionConfiguration {
// 內(nèi)嵌配置類
// 該類自身沒有提供任何實(shí)現(xiàn),其效果主要通過注解來實(shí)現(xiàn) :
// 僅在 bean ReactiveSessionRepository 不存在時(shí)導(dǎo)入 ReactiveSessionRepositoryImplementationValidator
// 和 ReactiveSessionConfigurationImportSelector
@Configuration
// 僅在 bean ReactiveSessionRepository 不存在時(shí)生效
@ConditionalOnMissingBean(ReactiveSessionRepository.class)
// 導(dǎo)入 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.
* 觸發(fā)創(chuàng)建 DefaultCookieSerializer 的條件 :
* 1. Bean HttpSessionIdResolver 和 CookieSerializer 都不存在
* 或者
* 2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
*
* DefaultCookieSerializerCondition 是一個(gè) AnyNestedCondition,
* 這種條件被滿足的條件是:某個(gè)內(nèi)嵌子條件被滿足
*/
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 環(huán)境下決定導(dǎo)入哪些 Session Store 配置類
*/
abstract static class SessionConfigurationImportSelector implements ImportSelector {
protected final String[] selectImports(WebApplicationType webApplicationType) {
List<String> 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 情況下使用糕簿,用于導(dǎo)入相應(yīng)的 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 情況下使用涣易,用于導(dǎo)入相應(yīng)的 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 未設(shè)置的情況下僅有一個(gè)session repository 實(shí)現(xiàn)類存在于 classpath
*/
abstract static class AbstractSessionRepositoryImplementationValidator {
private final List<String> candidates;
private final ClassLoader classLoader;
private final SessionProperties sessionProperties;
AbstractSessionRepositoryImplementationValidator(
ApplicationContext applicationContext,
SessionProperties sessionProperties, List<String> candidates) {
this.classLoader = applicationContext.getClassLoader();
this.sessionProperties = sessionProperties;
this.candidates = candidates;
}
@PostConstruct
public void checkAvailableImplementations() {
List<Class<?>> availableCandidates = new ArrayList<>();
for (String candidate : this.candidates) {
addCandidateIfAvailable(availableCandidates, candidate);
}
StoreType storeType = this.sessionProperties.getStoreType();
if (availableCandidates.size() > 1 && storeType == null) {
// 這里通過異常方式確保storeType 屬性未設(shè)置時(shí)必須只有一個(gè)session存儲(chǔ)庫實(shí)現(xiàn)類存在
throw new NonUniqueSessionRepositoryException(availableCandidates);
}
}
// 對(duì)類型 type 進(jìn)行檢查冶伞,如果該類型對(duì)應(yīng)的類能夠被 classLoader 加載成功,則將其作為候選類步氏,
// 也就是添加到列表 candidates 中响禽,否則該類型 type 不作為候選。
private void addCandidateIfAvailable(List<Class<?>> 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.
* 抽象基類荚醒,用于確保只有一個(gè) session repository bean 實(shí)例存在芋类,如果有多個(gè),則拋出異常
*/
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<SessionRepository<?>> 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<ReactiveSessionRepository<?>> sessionRepositoryProvider) {
super(sessionProperties, sessionRepositoryProvider);
}
}
}
從以上源代碼可以看出,SessionAutoConfiguration
自身沒有提供任何配置方法或者進(jìn)行任何bean
定義界阁,其配置效果主要通過自身所使用的注解和它的嵌套配置類來完成侯繁。
SessionAutoConfiguration
自身的注解約定了如下配置效果 :
-
SessionAutoConfiguration
生效的條件
- 類
Session
必須存在于classpath
上,換句話講泡躯,也就是要求必須依賴包Spring Session Core
; - 當(dāng)前應(yīng)用必須是一個(gè)
Web
應(yīng)用贮竟,Servlet Web
應(yīng)用,Reactive Web
應(yīng)用均可
- 類
導(dǎo)入了如下配置到相應(yīng)的
bean
– 前綴為server
的配置項(xiàng)到類型為ServerProperties
的bean
– 前綴為spring.session
的配置項(xiàng)到類型為SessionProperties
的bean
-
SessionAutoConfiguration
自動(dòng)配置(以及嵌套配置)的執(zhí)行時(shí)機(jī)
-
在以下自動(dòng)配置執(zhí)行之后
這些自動(dòng)配置主要是配置
Spring Session
存儲(chǔ)庫機(jī)制所使用底層基礎(chǔ)設(shè)施较剃,所以要在SessionAutoConfiguration
之前完成DataSourceAutoConfiguration
HazelcastAutoConfiguration
JdbcTemplateAutoConfiguration
MongoDataAutoConfiguration
MongoReactiveDataAutoConfiguration
RedisAutoConfiguration
-
在以下自動(dòng)配置執(zhí)行之前
HttpHandlerAutoConfiguration
-
ServletSessionConfiguration
ServletSessionConfiguration
配置類定義了一個(gè)bean
:
-
DefaultCookieSerializer cookieSerializer
僅在條件
DefaultCookieSerializerCondition
被滿足時(shí)定義 :-
Bean HttpSessionIdResolver
和CookieSerializer
都不存在 或者 -
Bean CookieHttpSessionIdResolver
存在 但bean CookieSerializer
不存在
-
-
導(dǎo)入配置類 SessionRepositoryFilterConfiguration 用于配置注冊(cè) SessionRepositoryFilter 到 Servlet 容器的 FilterRegistrationBean
SessionRepositoryFilter
是Spring Session
機(jī)制在運(yùn)行時(shí)工作的核心組件,用于服務(wù)用戶請(qǐng)求處理過程中所有HttpSession
操作請(qǐng)求 導(dǎo)入驗(yàn)證器組件
ServletSessionRepositoryValidator
確保只存在一個(gè)SessionRepository bean
或者指定的SessionRepository bean
存在-
定義嵌套配置類 ServletSessionRepositoryConfiguration
僅在
bean SessionRepository
不存在時(shí)生效-
導(dǎo)入 ServletSessionConfigurationImportSelector 以選擇合適的存儲(chǔ)庫配置類
針對(duì)本文所使用的應(yīng)用的情形咕别,最終會(huì)選擇
RedisSessionConfiguration
。
RedisSessionConfiguration
配置類會(huì)應(yīng)用sping.session
/spring.session.redis
為前綴的配置項(xiàng),并定義如下bean
:-
RedisOperationsSessionRepository sessionRepository
, (重要) -
RedisMessageListenerContainer redisMessageListenerContainer
, -
InitializingBean enableRedisKeyspaceNotificationsInitializer
, -
SessionRepositoryFilter springSessionRepositoryFilter
, (重要) -
SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter
,
-
導(dǎo)入驗(yàn)證器組件
ServletSessionRepositoryImplementationValidator
以確保相應(yīng)的存儲(chǔ)庫配置類存在于classpath
總結(jié)
- 檢查用戶配置的 StoreType 的正確性
- 配置 SessionRepositoryFilter 到 Servlet 容器中
- 配置默認(rèn)的 DefaultCookieSerializer
- 檢查當(dāng)前類路徑下至少存在一個(gè) SessionRepository 的實(shí)現(xiàn)
- 導(dǎo)入配置類 RedisSessionConfiguration
RedisSessionConfiguration
上面的 ServletSessionConfigurationImportSelector 會(huì)導(dǎo)入一系列配置類写穴,但是配置類上有條件(@Conditional)惰拱,所以在本例中只有 RedisSessionConfiguration 會(huì)生效。
package org.springframework.boot.autoconfigure.session;
// 省略 import 行
@Configuration
// 僅在指定類存在于 classpath 上時(shí)才生效
@ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class })
// 僅在 bean SessionRepository 不存在時(shí)才生效
@ConditionalOnMissingBean(SessionRepository.class)
// 僅在 bean RedisConnectionFactory 存在時(shí)才生效
@ConditionalOnBean(RedisConnectionFactory.class)
// 僅在條件 ServletSessionCondition 被滿足時(shí)才生效
@Conditional(ServletSessionCondition.class)
// 確保前綴為 spring.session.redis 的配置參數(shù)被加載到 bean RedisSessionProperties
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {
// 配置 redis 的鍵空間通知功能啊送,notify-keyspace-events Exg偿短,這里先買個(gè)關(guān)子。
@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() + "'.");
}
// 內(nèi)置配置類
// 1. 應(yīng)用配置參數(shù)
// 2. 繼承自 RedisHttpSessionConfiguration 以定義 sessionRepository馋没,springSessionRepositoryFilter 等運(yùn)行時(shí)工作組件 bean
@Configuration
public static class SpringBootRedisHttpSessionConfiguration
extends RedisHttpSessionConfiguration {
// 應(yīng)用用戶配置的 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 的定時(shí)任務(wù)的 cron 表達(dá)式昔逗,
static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";
// 會(huì)話被允許處于不活躍狀態(tài)的最長時(shí)間, 超過該事件,會(huì)話會(huì)被認(rèn)為是過期無效
// 使用缺省值 30 分鐘
private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
// 所創(chuàng)建的 session 在 redis 中的命名空間披泪, 使用缺省值 : spring:session
private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;
// 清除過期 session 的定時(shí)任務(wù)的 cron 表達(dá)式纤子,
// 使用缺省值 : "0 * * * * *", 表示每個(gè)分鐘的0秒執(zhí)行一次
private String cleanupCron = DEFAULT_CLEANUP_CRON;
// 對(duì) redis 的配置動(dòng)作,缺省是 : notify-keyspace-events
// 該缺省值確保 redis keyspace 事件通知機(jī)制啟用
private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();
// 創(chuàng)建連接到目標(biāo) redis 數(shù)據(jù)庫的工廠類款票,由外部提供
private RedisConnectionFactory redisConnectionFactory;
private RedisSerializer<Object> defaultRedisSerializer;
private ApplicationEventPublisher applicationEventPublisher;
// redis 消息監(jiān)聽器容器使用的異步執(zhí)行器控硼,用于監(jiān)聽到消息時(shí)執(zhí)行監(jiān)聽器邏輯
private Executor redisTaskExecutor;
private Executor redisSubscriptionExecutor;
private ClassLoader classLoader;
private StringValueResolver embeddedValueResolver;
// 定義 bean RedisOperationsSessionRepository,這是創(chuàng)建其他spring session 工作組件
// 所必要的底層存儲(chǔ)庫組件對(duì)象
@Bean
public RedisOperationsSessionRepository sessionRepository() {
// 注意艾少,這里使用了自己創(chuàng)建的 RedisTemplate 對(duì)象卡乾,而不是某個(gè) RedisTemplate bean
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;
}
// 定義 bean RedisMessageListenerContainer, 它使用一個(gè) redis 連接多路,異步處理 redis 消息
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 設(shè)置 redis 連接工廠對(duì)象
container.setConnectionFactory(this.redisConnectionFactory);
// 設(shè)置異步消息監(jiān)聽器邏輯執(zhí)行器
if (this.redisTaskExecutor != null) {
container.setTaskExecutor(this.redisTaskExecutor);
}
if (this.redisSubscriptionExecutor != null) {
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
}
// 添加消息監(jiān)聽器
// 監(jiān)聽 session 的 創(chuàng)建缚够,刪除 和 過期 等消息
container.addMessageListener(sessionRepository(), Arrays.asList(
new ChannelTopic(sessionRepository().getSessionDeletedChannel()),
new ChannelTopic(sessionRepository().getSessionExpiredChannel())));
container.addMessageListener(sessionRepository(),
Collections.singletonList(new PatternTopic(
sessionRepository().getSessionCreatedChannelPrefix() + "*")));
return container;
}
// 定義一個(gè) bean EnableRedisKeyspaceNotificationsInitializer 幔妨,這是一個(gè) InitializingBean,
// 他在自己的初始化階段對(duì) redis 配置 notify-keyspace-events鹦赎, 確保 redis keyspace 事件
// 通知機(jī)制啟動(dòng),用于確保 key 超時(shí)和刪除邏輯误堡。
@Bean
public InitializingBean enableRedisKeyspaceNotificationsInitializer() {
return new EnableRedisKeyspaceNotificationsInitializer(
this.redisConnectionFactory, this.configureRedisAction);
}
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setRedisNamespace(String namespace) {
this.redisNamespace = namespace;
}
public void setRedisFlushMode(RedisFlushMode redisFlushMode) {
Assert.notNull(redisFlushMode, "redisFlushMode cannot be null");
this.redisFlushMode = redisFlushMode;
}
public void setCleanupCron(String cleanupCron) {
this.cleanupCron = cleanupCron;
}
/**
* Sets the action to perform for configuring Redis.
*
* @param configureRedisAction the configureRedis to set. The default is
* ConfigureNotifyKeyspaceEventsAction.
*/
@Autowired(required = false)
public void setConfigureRedisAction(ConfigureRedisAction configureRedisAction) {
this.configureRedisAction = configureRedisAction;
}
// 連接到 redis 的連接的工廠組件 RedisConnectionFactory 由外部提供古话,
// 關(guān)于 RedisConnectionFactory 工廠組件的創(chuàng)建,可以參考 LettuceConnectionConfiguration,
// JedisConnectionConfiguration
@Autowired
public void setRedisConnectionFactory(
@SpringSessionRedisConnectionFactory ObjectProvider<RedisConnectionFactory>
springSessionRedisConnectionFactory,
ObjectProvider<RedisConnectionFactory> redisConnectionFactory) {
RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory
.getIfAvailable();
if (redisConnectionFactoryToUse == null) {
redisConnectionFactoryToUse = redisConnectionFactory.getObject();
}
this.redisConnectionFactory = redisConnectionFactoryToUse;
}
@Autowired(required = false)
@Qualifier("springSessionDefaultRedisSerializer")
public void setDefaultRedisSerializer(
RedisSerializer<Object> defaultRedisSerializer) {
this.defaultRedisSerializer = defaultRedisSerializer;
}
@Autowired
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Autowired(required = false)
@Qualifier("springSessionRedisTaskExecutor")
public void setRedisTaskExecutor(Executor redisTaskExecutor) {
this.redisTaskExecutor = redisTaskExecutor;
}
@Autowired(required = false)
@Qualifier("springSessionRedisSubscriptionExecutor")
public void setRedisSubscriptionExecutor(Executor redisSubscriptionExecutor) {
this.redisSubscriptionExecutor = redisSubscriptionExecutor;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> attributeMap = importMetadata
.getAnnotationAttributes(EnableRedisHttpSession.class.getName());
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
this.maxInactiveIntervalInSeconds = attributes
.getNumber("maxInactiveIntervalInSeconds");
String redisNamespaceValue = attributes.getString("redisNamespace");
if (StringUtils.hasText(redisNamespaceValue)) {
this.redisNamespace = this.embeddedValueResolver
.resolveStringValue(redisNamespaceValue);
}
this.redisFlushMode = attributes.getEnum("redisFlushMode");
String cleanupCron = attributes.getString("cleanupCron");
if (StringUtils.hasText(cleanupCron)) {
this.cleanupCron = cleanupCron;
}
}
// 注冊(cè)后臺(tái)執(zhí)行任務(wù)锁施,用來清除過期的 session
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(),
this.cleanupCron);
}
private RedisTemplate<Object, Object> createRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
if (this.defaultRedisSerializer != null) {
redisTemplate.setDefaultSerializer(this.defaultRedisSerializer);
}
redisTemplate.setConnectionFactory(this.redisConnectionFactory);
redisTemplate.setBeanClassLoader(this.classLoader);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
private int resolveDatabase() {
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
}
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
}
return RedisOperationsSessionRepository.DEFAULT_DATABASE;
}
/**
* Ensures that Redis is configured to send keyspace notifications. This is important
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.
* Without the SessionDestroyedEvent resources may not get cleaned up properly. For
* example, the mapping of the Session to WebSocket connections may not get cleaned
* up.
*/
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
private final RedisConnectionFactory connectionFactory;
private ConfigureRedisAction configure;
EnableRedisKeyspaceNotificationsInitializer(
RedisConnectionFactory connectionFactory,
ConfigureRedisAction configure) {
this.connectionFactory = connectionFactory;
this.configure = configure;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.configure == ConfigureRedisAction.NO_OP) {
return;
}
RedisConnection connection = this.connectionFactory.getConnection();
try {
this.configure.configure(connection);
}
finally {
try {
connection.close();
}
catch (Exception ex) {
LogFactory.getLog(getClass()).error("Error closing RedisConnection",
ex);
}
}
}
}
}
父類 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<HttpSessionListener> httpSessionListeners = new ArrayList<>();
@PostConstruct
public void init() {
// 如果用戶沒有配置 CookieSerializer 則自己創(chuàng)建一個(gè)默認(rèn)的
CookieSerializer cookieSerializer = (this.cookieSerializer != null)
? this.cookieSerializer
: createDefaultCookieSerializer();
this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
}
// 定義bean SessionEventHttpSessionListenerAdapter陪踩,一個(gè)ApplicationListener,
// 它會(huì)監(jiān)聽 Spring Session 的事件 SessionDestroyedEvent,SessionCreatedEvent
// 并將其轉(zhuǎn)換為HttpSessionEvent,然后轉(zhuǎn)發(fā)給所注冊(cè)的各個(gè) HttpSessionListener
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
}
// 定義從 Servlet 容器層面可見的 Filter SessionRepositoryFilter, 它會(huì)對(duì) Servlet 容器原生
// request/response 進(jìn)行包裝悉抵,從而攔截 HttpSession 的獲取肩狂,創(chuàng)建和刪除等操作,這些
// 操作最終會(huì)由底層的 Spring Session 機(jī)制支持姥饰,在本文所使用的項(xiàng)目例子中傻谁,其實(shí)就是
// 使用 redis 以及相關(guān)工作組件來支持 session
@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;
}
@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<HttpSessionListener> 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;
}
}
總結(jié)
- 注入 ConfigureRedisAction 以 redis 啟用鍵空間通知功能
- 應(yīng)用用戶配置的
spring.session
和spring.session.redis
屬性 - 配置 RedisIndexedSessionRepository
- 配置 RedisMessageListenerContainer 來處理 redis 的消息
- 執(zhí)行 ConfigureRedisAction 啟用鍵空間通知功能
- 注冊(cè)后臺(tái)執(zhí)行任務(wù),以清除過期的 session
- 如果 CookieSerializer 為空則創(chuàng)建默認(rèn)的列粪,并設(shè)置到 CookieHttpSessionIdResolver 中
- 創(chuàng)建 SessionEventHttpSessionListenerAdapter 以監(jiān)聽 Spring Session 相關(guān)的事件
- 創(chuàng)建 SessionRepositoryFilter
SessionRepositoryFilterConfiguration
SessionAutoConfiguration 引入的用來配置 SessionRepositoryFilter 的配置類审磁。
package org.springframework.boot.autoconfigure.session;
// 省略 import 行
@Configuration
// 在 bean SessionRepositoryFilter 存在的的情況下才生效
@ConditionalOnBean(SessionRepositoryFilter.class)
// 確保配置屬性項(xiàng) server.session.* 提取到 bean SessionProperties
@EnableConfigurationProperties(SessionProperties.class)
class SessionRepositoryFilterConfiguration {
// 定義bean FilterRegistrationBean, 這是一個(gè)過濾器注冊(cè)bean,它的任務(wù)是將一個(gè)過濾器注冊(cè)到
// Servlet 容器,這里的過濾器指的就是 bean SessionRepositoryFilter
@Bean
public FilterRegistrationBean<SessionRepositoryFilter<?>> sessionRepositoryFilterRegistration(
SessionProperties sessionProperties, SessionRepositoryFilter<?> filter) {
FilterRegistrationBean<SessionRepositoryFilter<?>> registration = new FilterRegistrationBean<>(
filter);
registration.setDispatcherTypes(getDispatcherTypes(sessionProperties));
registration.setOrder(sessionProperties.getServlet().getFilterOrder());
return registration;
}
// 從配置屬性項(xiàng)中獲取所指定的 DispatcherType 集合岂座,如果配置屬性中沒有指定該信息力图,則使用
// 缺省值 : DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST
private EnumSet<DispatcherType> 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
先不點(diǎn)進(jìn)去了,先看下 request.getSession() 的流程
request#getSession()
看一下 RedisSession 吧
final class RedisSession implements Session {
// 緩存對(duì)象掺逼、委托對(duì)象吃媒,這個(gè)類中的所有方法幾乎都是委托這個(gè)對(duì)象來做的
private final MapSession cached;
private Instant originalLastAccessTime;
// 存儲(chǔ) Session 域中的數(shù)據(jù)
private Map<String, Object> delta = new HashMap<>();
// 標(biāo)識(shí)當(dāng)前 Session 是否是新創(chuàng)建的,當(dāng)修改了 SessionId的時(shí)候用來決定是否使用 rename 命令
private boolean isNew;
private String originalPrincipalName;
// 當(dāng)前會(huì)話的 id
private String originalSessionId;
RedisSession(MapSession cached, boolean isNew) {
this.cached = cached;
this.isNew = isNew;
this.originalSessionId = cached.getId();
Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
// 如果是新創(chuàng)建的吕喘,則添加幾個(gè)屬性
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> 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<String> 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() 會(huì)調(diào)用這個(gè)方法
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();
// 將數(shù)據(jù)保存到 redis 中
getSessionBoundHashOperations(sessionId).putAll(this.delta);
// 下面這部分好像和 redis 索引和安全相關(guān)赘那,等學(xué)完 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<String, String> 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);
}
}
// 將當(dāng)前 Session 中的 delta 清空
this.delta = new HashMap<>(this.delta.size());
// 計(jì)算過期時(shí)間
Long originalExpiration = (this.originalLastAccessTime != null)
? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
// 設(shè)置過期時(shí)間
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
private void saveChangeSessionId() {
String sessionId = getId();
if (sessionId.equals(this.originalSessionId)) {
return;
}
// 如果不是新創(chuàng)建的 Session 對(duì)象,則使用 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);
}
// 獲取到和過期時(shí)間相關(guān)的兩個(gè) key氯质,對(duì)他們重命名
// "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 中已有的數(shù)據(jù)募舟,加入操作 session 的時(shí)候數(shù)據(jù)庫中的信息過期了,那么就會(huì)有數(shù)據(jù)丟失了闻察」敖福看代碼好像吧 SaveModel 改為 ALWAYS 可以解決這個(gè)問題。
RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
看接下來的內(nèi)容之前強(qiáng)烈建議看下這篇文章:https://www.iocoder.cn/Spring-Session/laoxu/spring-session-4/?self
太亂了辕漂,總結(jié)一下:
- 對(duì)過期時(shí)間四舍五入到下一分鐘
- 將 B 類型鍵移動(dòng)到新的過期時(shí)間桶中呢灶,設(shè)置過期時(shí)間為 35min
- 將 C 類型鍵(它相當(dāng)于 A 類型鍵的引用),設(shè)置過期時(shí)間為 30min
- 設(shè)置 A 類型鍵的過期時(shí)間為 35min
后臺(tái)執(zhí)行刪除操作的線程(在 RedisHttpSessionConfiguration 中配置的)
回頭看下 request#getSession() 中的
很簡單钉嘹,就是從數(shù)據(jù)庫里找到 A 類型鍵鸯乃,獲取他的數(shù)據(jù),封裝成 MapSession跋涣,判斷是否過期缨睡,再次封裝成 RedisSession鸟悴。
過期咋接收的 notify-keyspace-events Exg
先了解下鍵空間通知功能:http://doc.redisfans.com/topic/notification.html
這里有一個(gè)使用鍵空間通知功能的一個(gè) demo,可以看下:
我們?cè)倩仡^看 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 的實(shí)現(xiàn)
// 配置了兩個(gè)監(jiān)聽的 topic 分別是:
// __keyevent@0__:del
// __keyevent@0__:expired
container.addMessageListener(sessionRepository,
Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
new ChannelTopic(sessionRepository.getSessionExpiredChannel())));
// 這里配置了一個(gè)基于模式匹配的 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: 開頭的奖年,則發(fā)布 SessionCreatedEvent
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
// TODO: is this thread safe?
@SuppressWarnings("unchecked")
Map<Object, Object> loaded = (Map<Object, Object>) 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);
// 如果是刪除則觸發(fā) SessionDeletedEvent
if (isDeleted) {
handleDeleted(session);
}
// 如果是過期則觸發(fā) SessionExpiredEvent
else {
handleExpired(session);
}
}
}
但是 Spring 并沒有寫這些事件的監(jiān)聽器,是留給我們的一個(gè)鉤子陋守。他把 A 類型鍵設(shè)置多 5min 也是為了讓我們?cè)谶@段時(shí)間內(nèi)做一些我們想做的事揍堰,比如日志記錄等。
其實(shí)嗅义,如果說 Spring 使用 A 類型、B 類型隐砸、C 類型鍵來保證到指定時(shí)間會(huì)釋放內(nèi)存是錯(cuò)誤的之碗,因?yàn)锳 類型鍵實(shí)際上還是由 redis 來清除的,而且增加了 B季希、C 類型鍵會(huì)增加 redis 的消耗褪那,所以他這樣做的目的就是為了讓我們?cè)谶@段時(shí)間內(nèi)做一些我們想做的事。