概述
本文基於以下組合的應用,通過源代碼分析一下一個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
,這是在各種條件就緒後,基於配置屬性對基於Redis
的Spring Session
的最終工作組件執行真正配置任務的配置類。
首先,RedisSessionConfiguration
通過註解聲明瞭自己生效的條件如下 :
- 僅在類
RedisTemplate
,RedisOperationsSessionRepository
存在於classpath
上時才生效; - 僅在
bean SessionRepository
不存在時才生效; - 僅在
bean RedisConnectionFactory
存在時才生效; - 僅在條件
ServletSessionCondition
被滿足時才生效;
在以上條件都滿足的情況下,RedisSessionConfiguration
的效果如下 :
- 確保前綴爲
spring.session.redis
的配置參數被加載到bean RedisSessionProperties
- 使用繼承自
RedisHttpSessionConfiguration
的內部配置類SpringBootRedisHttpSessionConfiguration
完成以下配置任務:- 定義
bean RedisOperationsSessionRepository sessionRepository
- 定義
bean RedisMessageListenerContainer redisMessageListenerContainer
- 定義
bean InitializingBean enableRedisKeyspaceNotificationsInitializer
- 定義
bean SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter
- 定義
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;
}
}