Spring Boot 應用中 Spring Session 的配置(2) : 基於Redis的配置 RedisSessionConfiguration

概述

本文基於以下組合的應用,通過源代碼分析一下一個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

在上一篇文章中,我們分析了自動配置類SessionAutoConfiguration,這篇文章我們來看RedisSessionConfiguration,這是在各種條件就緒後,基於配置屬性對基於RedisSpring Session的最終工作組件執行真正配置任務的配置類。

首先,RedisSessionConfiguration通過註解聲明瞭自己生效的條件如下 :

  1. 僅在類RedisTemplate,RedisOperationsSessionRepository存在於classpath上時才生效;
  2. 僅在bean SessionRepository不存在時才生效;
  3. 僅在bean RedisConnectionFactory存在時才生效;
  4. 僅在條件ServletSessionCondition被滿足時才生效;

在以上條件都滿足的情況下,RedisSessionConfiguration的效果如下 :

  1. 確保前綴爲 spring.session.redis 的配置參數被加載到 bean RedisSessionProperties
  2. 使用繼承自RedisHttpSessionConfiguration的內部配置類SpringBootRedisHttpSessionConfiguration完成以下配置任務:
    1. 定義 bean RedisOperationsSessionRepository sessionRepository
    2. 定義 bean RedisMessageListenerContainer redisMessageListenerContainer
    3. 定義 bean InitializingBean enableRedisKeyspaceNotificationsInitializer
    4. 定義 bean SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter
    5. 定義 bean SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter (主要任務)

源代碼分析

RedisSessionConfiguration

package org.springframework.boot.autoconfigure.session;

import java.time.Duration;

// 省略 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 {

    // 內置配置類
    // 1. 應用配置參數
    // 2. 繼承自 RedisHttpSessionConfiguration 以定義 sessionRepository,
    //  springSessionRepositoryFilter 等運行時工作組件 bean
	@Configuration
	public static class SpringBootRedisHttpSessionConfiguration
			extends RedisHttpSessionConfiguration {

        // 應用配置參數
		@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 {

	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<Object> defaultRedisSerializer;

	private ApplicationEventPublisher applicationEventPublisher;

    // redis 消息監聽器容器使用的異步執行器,用於監聽到消息時執行監聽器邏輯
	private Executor redisTaskExecutor;

	private Executor redisSubscriptionExecutor;

	private ClassLoader classLoader;

	private StringValueResolver embeddedValueResolver;

    // 定義 bean RedisOperationsSessionRepository, 這是創建其他spring session 工作組件
    // 所必要的底層存儲庫組件對象
	@Bean
	public RedisOperationsSessionRepository sessionRepository() {
       // 注意,這裏使用了自己創建的  RedisTemplate 對象,而不是某個 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, 它使用一個 redis 連接多路,異步處理 redis 消息
	@Bean
	public RedisMessageListenerContainer redisMessageListenerContainer() {
		RedisMessageListenerContainer container = new RedisMessageListenerContainer();
       // 設置 redis 連接工廠對象 
		container.setConnectionFactory(this.redisConnectionFactory);
        
       // 設置異步消息監聽器邏輯執行器 
		if (this.redisTaskExecutor != null) {
			container.setTaskExecutor(this.redisTaskExecutor);
		}
		if (this.redisSubscriptionExecutor != null) {
			container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
		}
        
       // 添加消息監聽器 
       // 監聽 session 的 創建,刪除 和 過期 等消息
		container.addMessageListener(sessionRepository(), Arrays.asList(
				new ChannelTopic(sessionRepository().getSessionDeletedChannel()),
				new ChannelTopic(sessionRepository().getSessionExpiredChannel())));
		container.addMessageListener(sessionRepository(),
				Collections.singletonList(new PatternTopic(
						sessionRepository().getSessionCreatedChannelPrefix() + "*")));
		return container;
	}

    // 定義一個 bean EnableRedisKeyspaceNotificationsInitializer ,這是一個 InitializingBean,
    // 他在自己的初始化階段對 redis 配置 notify-keyspace-events, 確保 redis keyspace 事件
    // 通知機制啓動,用於確保會話超時和刪除邏輯。
	@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 由外部提供,
    // 關於 RedisConnectionFactory 工廠組件的創建,可以參考 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;
		}
	}

	@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 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 <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;
	}

}

相關文章

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