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

}

相关文章

  • Spring Boot 应用中 Spring Session 的配置(1) : 自动配置 SessionAutoConfiguration
  • Spring Boot 应用中 Spring Session 的配置(3) : SessionRepositoryFilterConfiguration

你可能感兴趣的:(Spring,Session,Spring,Boot,自动配置)