本文基于以下组合的应用,通过源代码分析一下一个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;
}
}