Spring Session 源码解读


title: Spring Session 源码解读
date: 2021/02/21 11:00


概述

本文基于以下组合的应用:

Spring Boot 2.1.3.RELEASE

  • SessionAutoConfiguration(@Conditional(Spring Session Core))
  • RedisSessionConfiguration(@Conditional(Spring Session Data Redis))

Spring Session Core 2.1.4.RELEASE

Spring Session Data Redis 2.1.3.RELEASE

其中 Spring Session Core 提供了部分接口交由子类实现,一般我们用的实现就是 Spring Session Data Redis

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 会被启用
// 为什么要在 RedisAutoConfiguration 之后执行?
// 因为当前配置类会引入 RedisSessionConfiguration 其需要 RedisAutoConfiguration 自动装配的 RedisConnectionFactory。
@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,确保存储库类型被指定以及相应的存储库类的存在(校验用户填写的 spring.session.store-type 属性,校验容器中有 SessionRepository);
    // 2. 导入 SessionRepositoryFilterConfiguration,配置 “注册 SessionRepositoryFilter 到 Servlet 容器”的 FilterRegistrationBean
    @Import({ ServletSessionRepositoryValidator.class,
            SessionRepositoryFilterConfiguration.class })
    static class ServletSessionConfiguration {

        // 定义一个 bean  cookieSerializer
        @Bean
        // 仅在条件 DefaultCookieSerializerCondition 被满足时才生效
        // 1. Bean HttpSessionIdResolver 和 CookieSerializer 都不存在 
        // 或者
        // 2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
        @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
        // ServletSessionRepositoryImplementationValidator:检查类路径下是否有多个 SessionRepository 实现类,如果有多个则检查是否指定了 StoreType,如果没指定则报错
        // ServletSessionConfigurationImportSelector:引入所有支持的类型的自动配置类,例如 Redis 的是 RedisSessionConfiguration
        @Configuration
        @ConditionalOnMissingBean(SessionRepository.class)
        @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 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 candidates;

        private final ClassLoader classLoader;

        private final SessionProperties sessionProperties;

        AbstractSessionRepositoryImplementationValidator(
                ApplicationContext applicationContext,
                SessionProperties sessionProperties, List candidates) {
            this.classLoader = applicationContext.getClassLoader();
            this.sessionProperties = sessionProperties;
            this.candidates = candidates;
        }

        @PostConstruct
        public void checkAvailableImplementations() {
            List> 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> 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> 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> 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
  4. ServletSessionConfiguration

ServletSessionConfiguration配置类定义了一个bean :

  • DefaultCookieSerializer cookieSerializer

    仅在条件DefaultCookieSerializerCondition被满足时定义 :

    1. Bean HttpSessionIdResolverCookieSerializer 都不存在 或者
    2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
  • 导入配置类 SessionRepositoryFilterConfiguration 用于配置注册 SessionRepositoryFilter 到 Servlet 容器的 FilterRegistrationBean

    SessionRepositoryFilterSpring Session机制在运行时工作的核心组件,用于服务用户请求处理过程中所有HttpSession操作请求

  • 导入验证器组件ServletSessionRepositoryValidator确保只存在一个SessionRepository bean或者指定的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

总结

  1. 检查用户配置的 StoreType 的正确性
  2. 配置 SessionRepositoryFilter 到 Servlet 容器中
  3. 配置默认的 DefaultCookieSerializer
  4. 检查当前类路径下至少存在一个 SessionRepository 的实现
  5. 导入配置类 RedisSessionConfiguration

RedisSessionConfiguration

上面的 ServletSessionConfigurationImportSelector 会导入一系列配置类,但是配置类上有条件(@Conditional),所以在本例中只有 RedisSessionConfiguration 会生效。

package org.springframework.boot.autoconfigure.session;

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

    // 配置 redis 的键空间通知功能,notify-keyspace-events Exg,这里先买个关子。
    @Bean
    @ConditionalOnMissingBean
    ConfigureRedisAction configureRedisAction(RedisSessionProperties redisSessionProperties) {
        switch (redisSessionProperties.getConfigureAction()) {
        case NOTIFY_KEYSPACE_EVENTS:
            return new ConfigureNotifyKeyspaceEventsAction();
        case NONE:
            return ConfigureRedisAction.NO_OP;
        }
        throw new IllegalStateException(
                "Unsupported redis configure action '" + redisSessionProperties.getConfigureAction() + "'.");
    }

    // 内置配置类
    // 1. 应用配置参数
    // 2. 继承自 RedisHttpSessionConfiguration 以定义 sessionRepository,springSessionRepositoryFilter 等运行时工作组件 bean
    @Configuration
    public static class SpringBootRedisHttpSessionConfiguration
            extends RedisHttpSessionConfiguration {

        // 应用用户配置的 spring.session.redis 属性
        @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 {

    // 清除过期 session 的定时任务的 cron 表达式,
    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 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 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 事件
    // 通知机制启动,用于确保 key 超时和删除逻辑。
    @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 
            springSessionRedisConnectionFactory,
            ObjectProvider redisConnectionFactory) {
        RedisConnectionFactory redisConnectionFactoryToUse = springSessionRedisConnectionFactory
                .getIfAvailable();
        if (redisConnectionFactoryToUse == null) {
            redisConnectionFactoryToUse = redisConnectionFactory.getObject();
        }
        this.redisConnectionFactory = redisConnectionFactoryToUse;
    }

    @Autowired(required = false)
    @Qualifier("springSessionDefaultRedisSerializer")
    public void setDefaultRedisSerializer(
            RedisSerializer 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 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;
        }
    }

    // 注册后台执行任务,用来清除过期的 session
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addCronTask(() -> sessionRepository().cleanupExpiredSessions(),
                this.cleanupCron);
    }

    private RedisTemplate createRedisTemplate() {
        RedisTemplate 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 httpSessionListeners = new ArrayList<>();

    @PostConstruct
    public void init() {
        // 如果用户没有配置 CookieSerializer 则自己创建一个默认的
        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  SessionRepositoryFilter 
            springSessionRepositoryFilter(
            SessionRepository sessionRepository) {
        SessionRepositoryFilter 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 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;
    }

}

总结

  1. 注入 ConfigureRedisAction 以 redis 启用键空间通知功能
  2. 应用用户配置的 spring.sessionspring.session.redis 属性
  3. 配置 RedisIndexedSessionRepository
  4. 配置 RedisMessageListenerContainer 来处理 redis 的消息
  5. 执行 ConfigureRedisAction 启用键空间通知功能
  6. 注册后台执行任务,以清除过期的 session
  7. 如果 CookieSerializer 为空则创建默认的,并设置到 CookieHttpSessionIdResolver 中
  8. 创建 SessionEventHttpSessionListenerAdapter 以监听 Spring Session 相关的事件
  9. 创建 SessionRepositoryFilter

SessionRepositoryFilterConfiguration

SessionAutoConfiguration 引入的用来配置 SessionRepositoryFilter 的配置类。

package org.springframework.boot.autoconfigure.session;

// 省略 import 行

@Configuration
// 在 bean SessionRepositoryFilter 存在的的情况下才生效
@ConditionalOnBean(SessionRepositoryFilter.class)
// 确保配置属性项 server.session.* 提取到 bean SessionProperties
@EnableConfigurationProperties(SessionProperties.class)
class SessionRepositoryFilterConfiguration {

    // 定义bean FilterRegistrationBean, 这是一个过滤器注册bean,它的任务是将一个过滤器注册到
    // Servlet 容器,这里的过滤器指的就是 bean SessionRepositoryFilter
    @Bean
    public FilterRegistrationBean> sessionRepositoryFilterRegistration(
            SessionProperties sessionProperties, SessionRepositoryFilter filter) {
        FilterRegistrationBean> registration = new FilterRegistrationBean<>(
                filter);
        registration.setDispatcherTypes(getDispatcherTypes(sessionProperties));
        registration.setOrder(sessionProperties.getServlet().getFilterOrder());
        return registration;
    }

    // 从配置属性项中获取所指定的 DispatcherType 集合,如果配置属性中没有指定该信息,则使用
    // 缺省值 : DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST
    private EnumSet getDispatcherTypes(
            SessionProperties sessionProperties) {
        SessionProperties.Servlet servletProperties = sessionProperties.getServlet();
        if (servletProperties.getFilterDispatcherTypes() == null) {
            return null;
        }
        return servletProperties.getFilterDispatcherTypes().stream()
                .map((type) -> DispatcherType.valueOf(type.name())).collect(Collectors
                        .collectingAndThen(Collectors.toSet(), EnumSet::copyOf));
    }

}

SessionRepositoryFilter

过滤器的 doFilter 方法

先不点进去了,先看下 request.getSession() 的流程

request#getSession()

request#getSession()
createSession()

看一下 RedisSession 吧

final class RedisSession implements Session {

    // 缓存对象、委托对象,这个类中的所有方法几乎都是委托这个对象来做的
    private final MapSession cached;

    private Instant originalLastAccessTime;

    // 存储 Session 域中的数据
    private Map delta = new HashMap<>();

    // 标识当前 Session 是否是新创建的,当修改了 SessionId的时候用来决定是否使用 rename 命令
    private boolean isNew;

    private String originalPrincipalName;

    // 当前会话的 id
    private String originalSessionId;

    RedisSession(MapSession cached, boolean isNew) {
        this.cached = cached;
        this.isNew = isNew;
        this.originalSessionId = cached.getId();
        Map indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
        this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
        // 如果是新创建的,则添加几个属性
        if (this.isNew) {
            this.delta.put(RedisSessionMapper.CREATION_TIME_KEY, cached.getCreationTime().toEpochMilli());
            this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY,
                    (int) cached.getMaxInactiveInterval().getSeconds());
            this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, cached.getLastAccessedTime().toEpochMilli());
        }
        if (this.isNew || (RedisIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
            getAttributeNames().forEach((attributeName) -> this.delta.put(getSessionAttrNameKey(attributeName),
                    cached.getAttribute(attributeName)));
        }
    }

    @Override
    public void setLastAccessedTime(Instant lastAccessedTime) {
        this.cached.setLastAccessedTime(lastAccessedTime);
        this.delta.put(RedisSessionMapper.LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
        flushImmediateIfNecessary();
    }

    @Override
    public boolean isExpired() {
        return this.cached.isExpired();
    }

    @Override
    public Instant getCreationTime() {
        return this.cached.getCreationTime();
    }

    @Override
    public String getId() {
        return this.cached.getId();
    }

    @Override
    public String changeSessionId() {
        return this.cached.changeSessionId();
    }

    @Override
    public Instant getLastAccessedTime() {
        return this.cached.getLastAccessedTime();
    }

    @Override
    public void setMaxInactiveInterval(Duration interval) {
        this.cached.setMaxInactiveInterval(interval);
        this.delta.put(RedisSessionMapper.MAX_INACTIVE_INTERVAL_KEY, (int) getMaxInactiveInterval().getSeconds());
        flushImmediateIfNecessary();
    }

    @Override
    public Duration getMaxInactiveInterval() {
        return this.cached.getMaxInactiveInterval();
    }

    @Override
    public  T getAttribute(String attributeName) {
        T attributeValue = this.cached.getAttribute(attributeName);
        if (attributeValue != null
                && RedisIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
            this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
        }
        return attributeValue;
    }

    @Override
    public Set getAttributeNames() {
        return this.cached.getAttributeNames();
    }

    @Override
    public void setAttribute(String attributeName, Object attributeValue) {
        this.cached.setAttribute(attributeName, attributeValue);
        this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
        flushImmediateIfNecessary();
    }

    @Override
    public void removeAttribute(String attributeName) {
        this.cached.removeAttribute(attributeName);
        this.delta.put(getSessionAttrNameKey(attributeName), null);
        flushImmediateIfNecessary();
    }

    private void flushImmediateIfNecessary() {
        if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
            save();
        }
    }

    // wrappedRequest.commitSession() 会调用这个方法
    private void save() {
        saveChangeSessionId();
        saveDelta();
    }

    /**
        * Saves any attributes that have been changed and updates the expiration of this
        * session.
        */
    private void saveDelta() {
        if (this.delta.isEmpty()) {
            return;
        }
        String sessionId = getId();
        // 将数据保存到 redis 中
        getSessionBoundHashOperations(sessionId).putAll(this.delta);

        // 下面这部分好像和 redis 索引和安全相关,等学完 redis 再看吧
        String principalSessionKey = getSessionAttrNameKey(
                FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
        String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
        if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
            if (this.originalPrincipalName != null) {
                String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
                RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
                        .remove(sessionId);
            }
            Map indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
            String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
            this.originalPrincipalName = principal;
            if (principal != null) {
                String principalRedisKey = getPrincipalKey(principal);
                RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
                        .add(sessionId);
            }
        }

        // 将当前 Session 中的 delta 清空
        this.delta = new HashMap<>(this.delta.size());

        // 计算过期时间
        Long originalExpiration = (this.originalLastAccessTime != null)
                ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
        // 设置过期时间
        RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
    }

    private void saveChangeSessionId() {
        String sessionId = getId();
            
        if (sessionId.equals(this.originalSessionId)) {
            return;
        }        
        // 如果不是新创建的 Session 对象,则使用 rename 重命名 key
        if (!this.isNew) {
            String originalSessionIdKey = getSessionKey(this.originalSessionId);
            String sessionIdKey = getSessionKey(sessionId);
            try {
                RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalSessionIdKey,
                        sessionIdKey);
            }
            catch (NonTransientDataAccessException ex) {
                handleErrNoSuchKeyError(ex);
            }
            // 获取到和过期时间相关的两个 key,对他们重命名
            //  "spring:session:expirations:1523934840000"
            //  "spring:session:sessions:expires:39feb101-87d4-42c7-ab53-ac6fe0d91925"
            String originalExpiredKey = getExpiredKey(this.originalSessionId);
            String expiredKey = getExpiredKey(sessionId);
            try {
                RedisIndexedSessionRepository.this.sessionRedisOperations.rename(originalExpiredKey, expiredKey);
            }
            catch (NonTransientDataAccessException ex) {
                handleErrNoSuchKeyError(ex);
            }
        }
        // 将最新的 sessionId 赋值到 originalSessionId
        this.originalSessionId = sessionId;
    }

    private void handleErrNoSuchKeyError(NonTransientDataAccessException ex) {
        if (!"ERR no such key".equals(NestedExceptionUtils.getMostSpecificCause(ex).getMessage())) {
            throw ex;
        }
    }

}

问题:delta 中没有存 redis 中已有的数据,加入操作 session 的时候数据库中的信息过期了,那么就会有数据丢失了。看代码好像吧 SaveModel 改为 ALWAYS 可以解决这个问题。

RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);

看接下来的内容之前强烈建议看下这篇文章:https://www.iocoder.cn/Spring-Session/laoxu/spring-session-4/?self

image

太乱了,总结一下:

  1. 对过期时间四舍五入到下一分钟
  2. 将 B 类型键移动到新的过期时间桶中,设置过期时间为 35min
  3. 将 C 类型键(它相当于 A 类型键的引用),设置过期时间为 30min
  4. 设置 A 类型键的过期时间为 35min

后台执行删除操作的线程(在 RedisHttpSessionConfiguration 中配置的)

image

回头看下 request#getSession() 中的

image
image
image

很简单,就是从数据库里找到 A 类型键,获取他的数据,封装成 MapSession,判断是否过期,再次封装成 RedisSession。

过期咋接收的 notify-keyspace-events Exg

先了解下键空间通知功能:http://doc.redisfans.com/topic/notification.html

Spring 使用的三个参数

这里有一个使用键空间通知功能的一个 demo,可以看下:

  1. https://blog.csdn.net/liuchuanhong1/article/details/70147149
  2. https://zhuanlan.zhihu.com/p/59065399

我们再回头看 RedisHttpSessionConfiguration 中配置的 RedisMessageListenerContainer:

RedisHttpSessionConfiguration.java

@Bean
public RedisMessageListenerContainer springSessionRedisMessageListenerContainer(
        RedisIndexedSessionRepository sessionRepository) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(this.redisConnectionFactory);
    if (this.redisTaskExecutor != null) {
        container.setTaskExecutor(this.redisTaskExecutor);
    }
    if (this.redisSubscriptionExecutor != null) {
        container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
    }

    // 其中 sessionRepository 是 MessageListener 的实现
    // 配置了两个监听的 topic 分别是:
    // __keyevent@0__:del
    // __keyevent@0__:expired
    container.addMessageListener(sessionRepository,
            Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
                    new ChannelTopic(sessionRepository.getSessionExpiredChannel())));

    // 这里配置了一个基于模式匹配的 topic:spring:session:event:0:created:*
    container.addMessageListener(sessionRepository,
            Collections.singletonList(new PatternTopic(sessionRepository.getSessionCreatedChannelPrefix() + "*")));
    return container;
}
RedisIndexedSessionRepository.java

@Override
public void onMessage(Message message, byte[] pattern) {
    byte[] messageChannel = message.getChannel();
    byte[] messageBody = message.getBody();

    String channel = new String(messageChannel);
    
    // 如果 topic 是以 spring:session:event:3:created: 开头的,则发布 SessionCreatedEvent 
    if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
        // TODO: is this thread safe?
        @SuppressWarnings("unchecked")
        Map loaded = (Map) this.defaultSerializer.deserialize(message.getBody());
        handleCreated(loaded, channel);
        return;
    }

    String body = new String(messageBody);
    if (!body.startsWith(getExpiredKeyPrefix())) {
        return;
    }

    // 判断是否是 __keyevent@0__:del 或 __keyevent@0__:expired Channel 的。
    boolean isDeleted = channel.equals(this.sessionDeletedChannel);
    if (isDeleted || channel.equals(this.sessionExpiredChannel)) {
        int beginIndex = body.lastIndexOf(":") + 1;
        int endIndex = body.length();
        String sessionId = body.substring(beginIndex, endIndex);

        RedisSession session = getSession(sessionId, true);

        if (session == null) {
            logger.warn("Unable to publish SessionDestroyedEvent for session " + sessionId);
            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
        }

        cleanupPrincipalIndex(session);

        // 如果是删除则触发 SessionDeletedEvent
        if (isDeleted) {
            handleDeleted(session);
        }
        // 如果是过期则触发 SessionExpiredEvent
        else {
            handleExpired(session);
        }
    }
}

但是 Spring 并没有写这些事件的监听器,是留给我们的一个钩子。他把 A 类型键设置多 5min 也是为了让我们在这段时间内做一些我们想做的事,比如日志记录等。

其实,如果说 Spring 使用 A 类型、B 类型、C 类型键来保证到指定时间会释放内存是错误的,因为A 类型键实际上还是由 redis 来清除的,而且增加了 B、C 类型键会增加 redis 的消耗,所以他这样做的目的就是为了让我们在这段时间内做一些我们想做的事。

你可能感兴趣的:(Spring Session 源码解读)