Spring Boot 應用中 Spring Session 的配置(1) : 自動配置 SessionAutoConfiguration

概述

本文基於以下組合的應用,通過源代碼分析一下一個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自身的註解約定了如下配置效果 :

  1. SessionAutoConfiguration生效的條件
    • Session必須存在於classpath上,換句話講,也就是要求必須依賴包Spring Session Core;
    • 當前應用必須是一個Web應用:Servlet Web應用,Reactive Web應用均可
  2. 導入瞭如下配置到相應的bean
    – 前綴爲server的配置項到類型爲ServerPropertiesbean
    – 前綴爲spring.session的配置項到類型爲SessionPropertiesbean
  3. SessionAutoConfiguration自動配置(以及嵌套配置)的執行時機
    • 在以下自動配置執行之後

      這些自動配置主要是配置Spring Session存儲庫機制所使用底層基礎設施,所以要在SessionAutoConfiguration之前完成

      • DataSourceAutoConfiguration
      • HazelcastAutoConfiguration
      • JdbcTemplateAutoConfiguration
      • MongoDataAutoConfiguration
      • MongoReactiveDataAutoConfiguration
      • RedisAutoConfiguration
    • 在以下自動配置執行之前
      • HttpHandlerAutoConfiguration

通過所使用的註解,SessionAutoConfiguration聲明瞭自身生效的條件和時機,然後在相應的條件生效,相應的時機到達時,SessionAutoConfiguration又將具體的配置任務委託給自己所包含的嵌套配置類來完成 :

  • ServletSessionConfiguration – 針對Servlet Web環境
  • ReactiveSessionConfiguration – 針對Reactive Web 環境

ServletSessionConfigurationReactiveSessionConfiguration這兩個配置類的工作模式很類似。這裏僅僅分析一下對應於比較常用的Servlet Web環境的ServletSessionConfiguration

ServletSessionConfiguration配置類定義了一個bean :

  • DefaultCookieSerializer cookieSerializer

    僅在條件DefaultCookieSerializerCondition被滿足時定義 :

    1. Bean HttpSessionIdResolverCookieSerializer 都不存在 或者
    2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
  • 導入配置類SessionRepositoryFilterConfiguration用於配置註冊SessionRepositoryFilterServlet容器的FilterRegistrationBean

    SessionRepositoryFilterSpring Session機制在運行時工作的核心組件,用於服務用戶請求處理過程中所有HttpSession操作請求

  • 導入驗證器組件ServletSessionRepositoryValidator確保只存在一個SessionRepository bean
  • 定義嵌套配置類ServletSessionRepositoryConfiguration
    • 僅在bean SessionRepository不存在時生效
    • 導入ServletSessionConfigurationImportSelector以選擇合適的存儲庫配置類

      針對本文所使用的應用的情形,最終會選擇RedisSessionConfiguration
      RedisSessionConfiguration配置類會應用sping.session/spring.session.redis爲前綴的配置項,並定義如下bean :

      1. RedisOperationsSessionRepository sessionRepository, (重要)
      2. RedisMessageListenerContainer redisMessageListenerContainer,
      3. InitializingBean enableRedisKeyspaceNotificationsInitializer,
      4. SessionRepositoryFilter springSessionRepositoryFilter, (重要)
      5. SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter,
    • 導入驗證器組件ServletSessionRepositoryImplementationValidator以確保相應的存儲庫配置類存在於classpath

總結

通過上面分析可見,SessionAutoConfiguration自動配置會檢測相應的條件然後在相應的時機執行自己的配置任務,它自身以及它委託的配置類通過逐層條件判斷,最終在一個基於RedisServletSpring Boot Web應用中,會最終使用RedisSessionConfiguration進行相應的Spring Session工作組件的配置,其最重要的目的是生成一個SessionRepositoryFilter,而SessionAutoConfiguration所導入的配置類SessionRepositoryFilterConfiguration會將該SessionRepositoryFilter註冊到Servlet容器。在這些工作都完成後,用戶請求處理過程中對HttpSession的各種操作纔會由Spring Session機制來服務。

接下來,我會在其他篇幅中繼續講解RedisSessionConfigurationSessionRepositoryFilterConfiguration

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章