Spring Session 源碼解讀


title: Spring Session 源碼解讀
date: 2021/02/21 11:00


概述

本文基于以下組合的應(yīng)用:

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 提供了部分接口交由子類實(shí)現(xiàn)线定,一般我們用的實(shí)現(xiàn)就是 Spring Session Data Redis

SessionAutoConfiguration

package org.springframework.boot.autoconfigure.session;

// 省略 imports 行

/**
 * EnableAutoConfiguration Auto-configuration for Spring Session.
 *
 * @since 1.4.0
 */
 // 聲明這是一個(gè)配置類
@Configuration
// 僅在類 Session 存在于 classpath 時(shí)候才生效,
// Session 類由包 Spring Session Core 提供
@ConditionalOnClass(Session.class)
// 僅在當(dāng)前應(yīng)用是 Web 應(yīng)用時(shí)才生效 : Servlet Web 應(yīng)用, Reactive Web 應(yīng)用都可以
@ConditionalOnWebApplication
// 確保如下前綴的配置屬性的加載到如下 bean :
// server ==> ServerProperties
// spring.session ==> SessionProperties
@EnableConfigurationProperties({ ServerProperties.class, SessionProperties.class })
// 當(dāng)前配置必須在指定的自動(dòng)配置結(jié)束之后進(jìn)行,這里雖然列出了很多,但同一應(yīng)用中它們未必
// 都存在辩尊,這里指的是當(dāng)前應(yīng)用中如果它們中間某些存在的話骑脱,SessionAutoConfiguration
// 自動(dòng)配置的執(zhí)行必須要在這些自動(dòng)配置結(jié)束之后完成,本文的分析使用 Redis 支持 Spring Session,
// 并且是 Servlet Web 應(yīng)用,所以 RedisAutoConfiguration 會(huì)被啟用
// 為什么要在 RedisAutoConfiguration 之后執(zhí)行沦偎?
// 因?yàn)楫?dāng)前配置類會(huì)引入 RedisSessionConfiguration 其需要 RedisAutoConfiguration 自動(dòng)裝配的 RedisConnectionFactory咐吼。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HazelcastAutoConfiguration.class,
        JdbcTemplateAutoConfiguration.class, MongoDataAutoConfiguration.class,
        MongoReactiveDataAutoConfiguration.class, RedisAutoConfiguration.class,
        RedisReactiveAutoConfiguration.class })
//  在自動(dòng)配置  HttpHandlerAutoConfiguration 執(zhí)行前執(zhí)行      
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
public class SessionAutoConfiguration {

    // 內(nèi)嵌配置子類吹缔,針對(duì) Servlet Web 的情況
    @Configuration
    @ConditionalOnWebApplication(type = Type.SERVLET)
    // 1. 導(dǎo)入 ServletSessionRepositoryValidator,確保存儲(chǔ)庫類型被指定以及相應(yīng)的存儲(chǔ)庫類的存在(校驗(yàn)用戶填寫的 spring.session.store-type 屬性锯茄,校驗(yàn)容器中有 SessionRepository)厢塘;
    // 2. 導(dǎo)入 SessionRepositoryFilterConfiguration,配置 “注冊(cè) SessionRepositoryFilter 到 Servlet 容器”的 FilterRegistrationBean
    @Import({ ServletSessionRepositoryValidator.class,
            SessionRepositoryFilterConfiguration.class })
    static class ServletSessionConfiguration {

        // 定義一個(gè) bean  cookieSerializer
        @Bean
        // 僅在條件 DefaultCookieSerializerCondition 被滿足時(shí)才生效
        // 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;
        }


        // 內(nèi)嵌配置類
        // 該類自身沒有提供任何實(shí)現(xiàn)肌幽,其效果主要通過注解來實(shí)現(xiàn) :
        // 僅在 bean SessionRepository 不存在時(shí)導(dǎo)入 ServletSessionRepositoryImplementationValidator 和 ServletSessionConfigurationImportSelector
        // ServletSessionRepositoryImplementationValidator:檢查類路徑下是否有多個(gè) SessionRepository 實(shí)現(xiàn)類晚碾,如果有多個(gè)則檢查是否指定了 StoreType,如果沒指定則報(bào)錯(cuò)
        // ServletSessionConfigurationImportSelector:引入所有支持的類型的自動(dòng)配置類喂急,例如 Redis 的是 RedisSessionConfiguration
        @Configuration
        @ConditionalOnMissingBean(SessionRepository.class)
        @Import({ ServletSessionRepositoryImplementationValidator.class,
                ServletSessionConfigurationImportSelector.class })
        static class ServletSessionRepositoryConfiguration {

        }

    }

    // 內(nèi)嵌配置子類格嘁,針對(duì) Reactive Web 的情況
    @Configuration
    @ConditionalOnWebApplication(type = Type.REACTIVE)
    @Import(ReactiveSessionRepositoryValidator.class)
    static class ReactiveSessionConfiguration {


        // 內(nèi)嵌配置類
        // 該類自身沒有提供任何實(shí)現(xiàn),其效果主要通過注解來實(shí)現(xiàn) :
        // 僅在 bean ReactiveSessionRepository 不存在時(shí)導(dǎo)入 ReactiveSessionRepositoryImplementationValidator
        // 和 ReactiveSessionConfigurationImportSelector
        @Configuration
        // 僅在 bean ReactiveSessionRepository 不存在時(shí)生效 
        @ConditionalOnMissingBean(ReactiveSessionRepository.class)
        // 導(dǎo)入 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.
     * 觸發(fā)創(chuàng)建 DefaultCookieSerializer 的條件 :
     * 1. Bean HttpSessionIdResolver 和 CookieSerializer 都不存在 
     * 或者
     * 2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
     *
     * DefaultCookieSerializerCondition 是一個(gè) AnyNestedCondition,
     * 這種條件被滿足的條件是:某個(gè)內(nèi)嵌子條件被滿足
     */
    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 環(huán)境下決定導(dǎo)入哪些 Session Store 配置類
     */
    abstract static class SessionConfigurationImportSelector implements ImportSelector {

        protected final String[] selectImports(WebApplicationType webApplicationType) {
            List<String> 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 情況下使用糕簿,用于導(dǎo)入相應(yīng)的 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 情況下使用涣易,用于導(dǎo)入相應(yīng)的 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 未設(shè)置的情況下僅有一個(gè)session repository 實(shí)現(xiàn)類存在于 classpath
     */
    abstract static class AbstractSessionRepositoryImplementationValidator {

        private final List<String> candidates;

        private final ClassLoader classLoader;

        private final SessionProperties sessionProperties;

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

        @PostConstruct
        public void checkAvailableImplementations() {
            List<Class<?>> availableCandidates = new ArrayList<>();
            for (String candidate : this.candidates) {
                addCandidateIfAvailable(availableCandidates, candidate);
            }
            StoreType storeType = this.sessionProperties.getStoreType();
            if (availableCandidates.size() > 1 && storeType == null) {
                // 這里通過異常方式確保storeType 屬性未設(shè)置時(shí)必須只有一個(gè)session存儲(chǔ)庫實(shí)現(xiàn)類存在
                throw new NonUniqueSessionRepositoryException(availableCandidates);
            }
        }

        // 對(duì)類型 type 進(jìn)行檢查冶伞,如果該類型對(duì)應(yīng)的類能夠被 classLoader 加載成功,則將其作為候選類步氏,
        // 也就是添加到列表 candidates 中响禽,否則該類型 type 不作為候選。
        private void addCandidateIfAvailable(List<Class<?>> 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.
     * 抽象基類荚醒,用于確保只有一個(gè) session repository bean 實(shí)例存在芋类,如果有多個(gè),則拋出異常
     */
    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<SessionRepository<?>> 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<ReactiveSessionRepository<?>> sessionRepositoryProvider) {
            super(sessionProperties, sessionRepositoryProvider);
        }

    }

}

從以上源代碼可以看出,SessionAutoConfiguration自身沒有提供任何配置方法或者進(jìn)行任何bean定義界阁,其配置效果主要通過自身所使用的注解和它的嵌套配置類來完成侯繁。

SessionAutoConfiguration自身的注解約定了如下配置效果 :

  1. SessionAutoConfiguration

    生效的條件

    • Session必須存在于classpath上,換句話講泡躯,也就是要求必須依賴包Spring Session Core;
    • 當(dāng)前應(yīng)用必須是一個(gè)Web應(yīng)用贮竟,Servlet Web應(yīng)用,Reactive Web應(yīng)用均可
  2. 導(dǎo)入了如下配置到相應(yīng)的bean
    – 前綴為server的配置項(xiàng)到類型為ServerPropertiesbean
    – 前綴為spring.session的配置項(xiàng)到類型為SessionPropertiesbean

  3. SessionAutoConfiguration

    自動(dòng)配置(以及嵌套配置)的執(zhí)行時(shí)機(jī)

    • 在以下自動(dòng)配置執(zhí)行之后

      這些自動(dòng)配置主要是配置Spring Session存儲(chǔ)庫機(jī)制所使用底層基礎(chǔ)設(shè)施较剃,所以要在SessionAutoConfiguration之前完成

      • DataSourceAutoConfiguration
      • HazelcastAutoConfiguration
      • JdbcTemplateAutoConfiguration
      • MongoDataAutoConfiguration
      • MongoReactiveDataAutoConfiguration
      • RedisAutoConfiguration
    • 在以下自動(dòng)配置執(zhí)行之前

      • HttpHandlerAutoConfiguration
  4. ServletSessionConfiguration

ServletSessionConfiguration配置類定義了一個(gè)bean :

  • DefaultCookieSerializer cookieSerializer

    僅在條件DefaultCookieSerializerCondition被滿足時(shí)定義 :

    1. Bean HttpSessionIdResolverCookieSerializer 都不存在 或者
    2. Bean CookieHttpSessionIdResolver 存在 但 bean CookieSerializer 不存在
  • 導(dǎo)入配置類 SessionRepositoryFilterConfiguration 用于配置注冊(cè) SessionRepositoryFilter 到 Servlet 容器的 FilterRegistrationBean

    SessionRepositoryFilterSpring Session機(jī)制在運(yùn)行時(shí)工作的核心組件,用于服務(wù)用戶請(qǐng)求處理過程中所有HttpSession操作請(qǐng)求

  • 導(dǎo)入驗(yàn)證器組件ServletSessionRepositoryValidator確保只存在一個(gè)SessionRepository bean或者指定的SessionRepository bean存在

  • 定義嵌套配置類 ServletSessionRepositoryConfiguration

    • 僅在bean SessionRepository不存在時(shí)生效

    • 導(dǎo)入 ServletSessionConfigurationImportSelector 以選擇合適的存儲(chǔ)庫配置類

      針對(duì)本文所使用的應(yīng)用的情形咕别,最終會(huì)選擇RedisSessionConfiguration
      RedisSessionConfiguration配置類會(huì)應(yīng)用sping.session/spring.session.redis為前綴的配置項(xiàng),并定義如下bean :

      1. RedisOperationsSessionRepository sessionRepository, (重要)
      2. RedisMessageListenerContainer redisMessageListenerContainer,
      3. InitializingBean enableRedisKeyspaceNotificationsInitializer,
      4. SessionRepositoryFilter springSessionRepositoryFilter, (重要)
      5. SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter,
    • 導(dǎo)入驗(yàn)證器組件ServletSessionRepositoryImplementationValidator以確保相應(yīng)的存儲(chǔ)庫配置類存在于classpath

總結(jié)

  1. 檢查用戶配置的 StoreType 的正確性
  2. 配置 SessionRepositoryFilter 到 Servlet 容器中
  3. 配置默認(rèn)的 DefaultCookieSerializer
  4. 檢查當(dāng)前類路徑下至少存在一個(gè) SessionRepository 的實(shí)現(xiàn)
  5. 導(dǎo)入配置類 RedisSessionConfiguration

RedisSessionConfiguration

上面的 ServletSessionConfigurationImportSelector 會(huì)導(dǎo)入一系列配置類写穴,但是配置類上有條件(@Conditional)惰拱,所以在本例中只有 RedisSessionConfiguration 會(huì)生效。

package org.springframework.boot.autoconfigure.session;

// 省略 import 行

@Configuration
// 僅在指定類存在于 classpath 上時(shí)才生效
@ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class })
// 僅在 bean SessionRepository 不存在時(shí)才生效
@ConditionalOnMissingBean(SessionRepository.class)
// 僅在 bean RedisConnectionFactory 存在時(shí)才生效
@ConditionalOnBean(RedisConnectionFactory.class)
// 僅在條件 ServletSessionCondition 被滿足時(shí)才生效
@Conditional(ServletSessionCondition.class)
// 確保前綴為 spring.session.redis 的配置參數(shù)被加載到 bean RedisSessionProperties
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {

    // 配置 redis 的鍵空間通知功能啊送,notify-keyspace-events Exg偿短,這里先買個(gè)關(guān)子。
    @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() + "'.");
    }

    // 內(nèi)置配置類
    // 1. 應(yīng)用配置參數(shù)
    // 2. 繼承自 RedisHttpSessionConfiguration 以定義 sessionRepository馋没,springSessionRepositoryFilter 等運(yùn)行時(shí)工作組件 bean
    @Configuration
    public static class SpringBootRedisHttpSessionConfiguration
            extends RedisHttpSessionConfiguration {

        // 應(yīng)用用戶配置的 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 的定時(shí)任務(wù)的 cron 表達(dá)式昔逗,
    static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";

    // 會(huì)話被允許處于不活躍狀態(tài)的最長時(shí)間, 超過該事件,會(huì)話會(huì)被認(rèn)為是過期無效
    // 使用缺省值 30 分鐘
    private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

   // 所創(chuàng)建的 session 在 redis 中的命名空間披泪, 使用缺省值 : spring:session 
    private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
  
    private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;

    // 清除過期 session 的定時(shí)任務(wù)的 cron 表達(dá)式纤子,
    // 使用缺省值 : "0 * * * * *", 表示每個(gè)分鐘的0秒執(zhí)行一次
    private String cleanupCron = DEFAULT_CLEANUP_CRON;

    // 對(duì) redis 的配置動(dòng)作,缺省是 : notify-keyspace-events
    // 該缺省值確保 redis keyspace 事件通知機(jī)制啟用
    private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();

   // 創(chuàng)建連接到目標(biāo) redis 數(shù)據(jù)庫的工廠類款票,由外部提供
    private RedisConnectionFactory redisConnectionFactory;

    private RedisSerializer<Object> defaultRedisSerializer;

    private ApplicationEventPublisher applicationEventPublisher;

    // redis 消息監(jiān)聽器容器使用的異步執(zhí)行器控硼,用于監(jiān)聽到消息時(shí)執(zhí)行監(jiān)聽器邏輯
    private Executor redisTaskExecutor;

    private Executor redisSubscriptionExecutor;

    private ClassLoader classLoader;

    private StringValueResolver embeddedValueResolver;

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

    // 定義一個(gè) bean EnableRedisKeyspaceNotificationsInitializer 幔妨,這是一個(gè) InitializingBean,
    // 他在自己的初始化階段對(duì) redis 配置 notify-keyspace-events鹦赎, 確保 redis keyspace 事件
    // 通知機(jī)制啟動(dòng),用于確保 key 超時(shí)和刪除邏輯误堡。
    @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 由外部提供古话,
    // 關(guān)于 RedisConnectionFactory 工廠組件的創(chuàng)建,可以參考 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;
        }
    }

    // 注冊(cè)后臺(tái)執(zhí)行任務(wù)锁施,用來清除過期的 session
    @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 則自己創(chuàng)建一個(gè)默認(rèn)的
        CookieSerializer cookieSerializer = (this.cookieSerializer != null)
                ? this.cookieSerializer
                : createDefaultCookieSerializer();
        this.defaultHttpSessionIdResolver.setCookieSerializer(cookieSerializer);
    }

   // 定義bean SessionEventHttpSessionListenerAdapter陪踩,一個(gè)ApplicationListener,
   // 它會(huì)監(jiān)聽 Spring Session 的事件 SessionDestroyedEvent,SessionCreatedEvent
   // 并將其轉(zhuǎn)換為HttpSessionEvent,然后轉(zhuǎn)發(fā)給所注冊(cè)的各個(gè) HttpSessionListener
    @Bean
    public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
        return new SessionEventHttpSessionListenerAdapter(this.httpSessionListeners);
    }

    // 定義從 Servlet 容器層面可見的 Filter SessionRepositoryFilter, 它會(huì)對(duì) Servlet 容器原生 
    // request/response 進(jìn)行包裝悉抵,從而攔截 HttpSession 的獲取肩狂,創(chuàng)建和刪除等操作,這些
    // 操作最終會(huì)由底層的 Spring Session 機(jī)制支持姥饰,在本文所使用的項(xiàng)目例子中傻谁,其實(shí)就是
    // 使用 redis 以及相關(guān)工作組件來支持 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;
    }

}

總結(jié)

  1. 注入 ConfigureRedisAction 以 redis 啟用鍵空間通知功能
  2. 應(yīng)用用戶配置的 spring.sessionspring.session.redis 屬性
  3. 配置 RedisIndexedSessionRepository
  4. 配置 RedisMessageListenerContainer 來處理 redis 的消息
  5. 執(zhí)行 ConfigureRedisAction 啟用鍵空間通知功能
  6. 注冊(cè)后臺(tái)執(zhí)行任務(wù),以清除過期的 session
  7. 如果 CookieSerializer 為空則創(chuàng)建默認(rèn)的列粪,并設(shè)置到 CookieHttpSessionIdResolver 中
  8. 創(chuàng)建 SessionEventHttpSessionListenerAdapter 以監(jiān)聽 Spring Session 相關(guān)的事件
  9. 創(chuàng)建 SessionRepositoryFilter

SessionRepositoryFilterConfiguration

SessionAutoConfiguration 引入的用來配置 SessionRepositoryFilter 的配置類审磁。

package org.springframework.boot.autoconfigure.session;

// 省略 import 行

@Configuration
// 在 bean SessionRepositoryFilter 存在的的情況下才生效
@ConditionalOnBean(SessionRepositoryFilter.class)
// 確保配置屬性項(xiàng) server.session.* 提取到 bean SessionProperties
@EnableConfigurationProperties(SessionProperties.class)
class SessionRepositoryFilterConfiguration {

    // 定義bean FilterRegistrationBean, 這是一個(gè)過濾器注冊(cè)bean,它的任務(wù)是將一個(gè)過濾器注冊(cè)到
    // Servlet 容器,這里的過濾器指的就是 bean SessionRepositoryFilter
    @Bean
    public FilterRegistrationBean<SessionRepositoryFilter<?>> sessionRepositoryFilterRegistration(
            SessionProperties sessionProperties, SessionRepositoryFilter<?> filter) {
        FilterRegistrationBean<SessionRepositoryFilter<?>> registration = new FilterRegistrationBean<>(
                filter);
        registration.setDispatcherTypes(getDispatcherTypes(sessionProperties));
        registration.setOrder(sessionProperties.getServlet().getFilterOrder());
        return registration;
    }

    // 從配置屬性項(xiàng)中獲取所指定的 DispatcherType 集合岂座,如果配置屬性中沒有指定該信息力图,則使用
    // 缺省值 : DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST
    private EnumSet<DispatcherType> 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 方法

先不點(diǎn)進(jìn)去了,先看下 request.getSession() 的流程

request#getSession()

request#getSession()
createSession()

看一下 RedisSession 吧

final class RedisSession implements Session {

    // 緩存對(duì)象掺逼、委托對(duì)象吃媒,這個(gè)類中的所有方法幾乎都是委托這個(gè)對(duì)象來做的
    private final MapSession cached;

    private Instant originalLastAccessTime;

    // 存儲(chǔ) Session 域中的數(shù)據(jù)
    private Map<String, Object> delta = new HashMap<>();

    // 標(biāo)識(shí)當(dāng)前 Session 是否是新創(chuàng)建的,當(dāng)修改了 SessionId的時(shí)候用來決定是否使用 rename 命令
    private boolean isNew;

    private String originalPrincipalName;

    // 當(dāng)前會(huì)話的 id
    private String originalSessionId;

    RedisSession(MapSession cached, boolean isNew) {
        this.cached = cached;
        this.isNew = isNew;
        this.originalSessionId = cached.getId();
        Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
        this.originalPrincipalName = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
        // 如果是新創(chuàng)建的吕喘,則添加幾個(gè)屬性
        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> 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<String> 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() 會(huì)調(diào)用這個(gè)方法
    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();
        // 將數(shù)據(jù)保存到 redis 中
        getSessionBoundHashOperations(sessionId).putAll(this.delta);

        // 下面這部分好像和 redis 索引和安全相關(guān)赘那,等學(xué)完 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<String, String> 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);
            }
        }

        // 將當(dāng)前 Session 中的 delta 清空
        this.delta = new HashMap<>(this.delta.size());

        // 計(jì)算過期時(shí)間
        Long originalExpiration = (this.originalLastAccessTime != null)
                ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
        // 設(shè)置過期時(shí)間
        RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
    }

    private void saveChangeSessionId() {
        String sessionId = getId();
            
        if (sessionId.equals(this.originalSessionId)) {
            return;
        }        
        // 如果不是新創(chuàng)建的 Session 對(duì)象,則使用 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);
            }
            // 獲取到和過期時(shí)間相關(guān)的兩個(gè) key氯质,對(duì)他們重命名
            //  "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 中已有的數(shù)據(jù)募舟,加入操作 session 的時(shí)候數(shù)據(jù)庫中的信息過期了,那么就會(huì)有數(shù)據(jù)丟失了闻察」敖福看代碼好像吧 SaveModel 改為 ALWAYS 可以解決這個(gè)問題。

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

看接下來的內(nèi)容之前強(qiáng)烈建議看下這篇文章:https://www.iocoder.cn/Spring-Session/laoxu/spring-session-4/?self

image

太亂了辕漂,總結(jié)一下:

  1. 對(duì)過期時(shí)間四舍五入到下一分鐘
  2. 將 B 類型鍵移動(dòng)到新的過期時(shí)間桶中呢灶,設(shè)置過期時(shí)間為 35min
  3. 將 C 類型鍵(它相當(dāng)于 A 類型鍵的引用),設(shè)置過期時(shí)間為 30min
  4. 設(shè)置 A 類型鍵的過期時(shí)間為 35min

后臺(tái)執(zhí)行刪除操作的線程(在 RedisHttpSessionConfiguration 中配置的)

image

回頭看下 request#getSession() 中的

image
image
image

很簡單钉嘹,就是從數(shù)據(jù)庫里找到 A 類型鍵鸯乃,獲取他的數(shù)據(jù),封裝成 MapSession跋涣,判斷是否過期缨睡,再次封裝成 RedisSession鸟悴。

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

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

Spring 使用的三個(gè)參數(shù)

這里有一個(gè)使用鍵空間通知功能的一個(gè) demo,可以看下:

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

我們?cè)倩仡^看 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 的實(shí)現(xiàn)
    // 配置了兩個(gè)監(jiān)聽的 topic 分別是:
    // __keyevent@0__:del
    // __keyevent@0__:expired
    container.addMessageListener(sessionRepository,
            Arrays.asList(new ChannelTopic(sessionRepository.getSessionDeletedChannel()),
                    new ChannelTopic(sessionRepository.getSessionExpiredChannel())));

    // 這里配置了一個(gè)基于模式匹配的 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: 開頭的奖年,則發(fā)布 SessionCreatedEvent 
    if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
        // TODO: is this thread safe?
        @SuppressWarnings("unchecked")
        Map<Object, Object> loaded = (Map<Object, Object>) 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);

        // 如果是刪除則觸發(fā) SessionDeletedEvent
        if (isDeleted) {
            handleDeleted(session);
        }
        // 如果是過期則觸發(fā) SessionExpiredEvent
        else {
            handleExpired(session);
        }
    }
}

但是 Spring 并沒有寫這些事件的監(jiān)聽器,是留給我們的一個(gè)鉤子陋守。他把 A 類型鍵設(shè)置多 5min 也是為了讓我們?cè)谶@段時(shí)間內(nèi)做一些我們想做的事揍堰,比如日志記錄等。

其實(shí)嗅义,如果說 Spring 使用 A 類型、B 類型隐砸、C 類型鍵來保證到指定時(shí)間會(huì)釋放內(nèi)存是錯(cuò)誤的之碗,因?yàn)锳 類型鍵實(shí)際上還是由 redis 來清除的,而且增加了 B季希、C 類型鍵會(huì)增加 redis 的消耗褪那,所以他這樣做的目的就是為了讓我們?cè)谶@段時(shí)間內(nèi)做一些我們想做的事。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末式塌,一起剝皮案震驚了整個(gè)濱河市博敬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌峰尝,老刑警劉巖偏窝,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異武学,居然都是意外死亡祭往,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門火窒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硼补,“玉大人,你說我怎么就攤上這事熏矿∫押В” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵票编,是天一觀的道長褪储。 經(jīng)常有香客問我,道長慧域,這世上最難降的妖魔是什么乱豆? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮吊趾,結(jié)果婚禮上宛裕,老公的妹妹穿的比我還像新娘伴挚。我一直安慰自己春叫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著染簇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪窥翩。 梳的紋絲不亂的頭發(fā)上恭金,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音勇边,去河邊找鬼犹撒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛粒褒,可吹牛的內(nèi)容都是我干的识颊。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼奕坟,長吁一口氣:“原來是場噩夢啊……” “哼祥款!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起月杉,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤刃跛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后苛萎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桨昙,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年腌歉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绊率。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡究履,死狀恐怖滤否,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情最仑,我是刑警寧澤藐俺,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站泥彤,受9級(jí)特大地震影響欲芹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吟吝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一菱父、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦浙宜、人聲如沸官辽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽同仆。三九已至,卻和暖如春裙品,著一層夾襖步出監(jiān)牢的瞬間俗批,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工市怎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岁忘,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓区匠,卻偏偏與公主長得像干像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辱志,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容