概述
本文基於以下組合的應用,通過源代碼分析一下一個Spring Boot
應用中Spring Session
的配置過程:
Spring Boot 2.1.3.RELEASE
Spring Session Core 2.1.4.RELEASE
Spring Session Data Redis 2.1.3.RELEASE
Spring Web MVC 5.1.5.RELEASE
在一個Spring Boot
應用中,關於Spring Session
的配置,首先要提到的就是自動配置類SessionAutoConfiguration
了。
源代碼分析
源代碼 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 會被啓用
@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,確保存儲庫類型被指定以及相應的存儲庫類的存在;
// 2. 導入 SessionRepositoryFilterConfiguration, 配置註冊SessionRepositoryFilter到Servlet容器的
// FilterRegistrationBean
@Import({ ServletSessionRepositoryValidator.class,
SessionRepositoryFilterConfiguration.class })
static class ServletSessionConfiguration {
// 定義一個 bean cookieSerializer
@Bean
// 僅在條件 DefaultCookieSerializerCondition 被滿足時才生效
@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
@Configuration
// 僅在 bean SessionRepository 不存在時生效
@ConditionalOnMissingBean(SessionRepository.class)
// 導入 ServletSessionRepositoryImplementationValidator
// 和 ServletSessionConfigurationImportSelector
@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<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 情況下使用,用於導入相應的 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<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 屬性未設置時必須只有一個session存儲庫實現類存在
throw new NonUniqueSessionRepositoryException(availableCandidates);
}
}
// 對類型 type 進行檢查,如果該類型對應的類能夠被 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.
* 抽象基類,用於確保只有一個 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<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
自身沒有提供任何配置方法或者進行任何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
- 在以下自動配置執行之後
通過所使用的註解,SessionAutoConfiguration
聲明瞭自身生效的條件和時機,然後在相應的條件生效,相應的時機到達時,SessionAutoConfiguration
又將具體的配置任務委託給自己所包含的嵌套配置類來完成 :
ServletSessionConfiguration
– 針對Servlet Web
環境ReactiveSessionConfiguration
– 針對Reactive Web
環境
ServletSessionConfiguration
和ReactiveSessionConfiguration
這兩個配置類的工作模式很類似。這裏僅僅分析一下對應於比較常用的Servlet Web
環境的ServletSessionConfiguration
。
ServletSessionConfiguration
配置類定義了一個bean
:
DefaultCookieSerializer cookieSerializer
僅在條件
DefaultCookieSerializerCondition
被滿足時定義 :Bean HttpSessionIdResolver
和CookieSerializer
都不存在 或者Bean CookieHttpSessionIdResolver
存在 但bean CookieSerializer
不存在
- 導入配置類
SessionRepositoryFilterConfiguration
用於配置註冊SessionRepositoryFilter
到Servlet
容器的FilterRegistrationBean
SessionRepositoryFilter
是Spring Session
機制在運行時工作的核心組件,用於服務用戶請求處理過程中所有HttpSession
操作請求 - 導入驗證器組件
ServletSessionRepositoryValidator
確保只存在一個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
- 僅在
總結
通過上面分析可見,SessionAutoConfiguration
自動配置會檢測相應的條件然後在相應的時機執行自己的配置任務,它自身以及它委託的配置類通過逐層條件判斷,最終在一個基於Redis
和Servlet
的Spring Boot Web
應用中,會最終使用RedisSessionConfiguration
進行相應的Spring Session
工作組件的配置,其最重要的目的是生成一個SessionRepositoryFilter
,而SessionAutoConfiguration
所導入的配置類SessionRepositoryFilterConfiguration
會將該SessionRepositoryFilter
註冊到Servlet
容器。在這些工作都完成後,用戶請求處理過程中對HttpSession
的各種操作纔會由Spring Session
機制來服務。
接下來,我會在其他篇幅中繼續講解RedisSessionConfiguration
和SessionRepositoryFilterConfiguration
。