spring-session(二)與spring-boot整合實戰

前兩篇介紹了spring-session的原理,這篇在理論的基礎上再實戰。
spring-boot整合spring-session的自動配置可謂是開箱即用,極其簡潔和方便。這篇文章即介紹spring-boot整合spring-session,這裏只介紹基於RedisSession的實戰。html

原理篇是基於spring-session v1.2.2版本,考慮到RedisSession模塊與spring-session v2.0.6版本的差別很小,且可以與spring-boot v2.0.0兼容,因此實戰篇是基於spring-boot v2.0.0基礎上配置spring-session。git

源碼請戮session-examplegithub

實戰

搭建spring-boot工程這裏飄過,傳送門:https://start.spring.io/redis

配置spring-session

引入spring-session的pom配置,因爲spring-boot包含spring-session的starter模塊,因此pom中依賴:spring

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

編寫spring boot啓動類SessionExampleApplication設計模式

/**
 * 啓動類
 *
 * @author huaijin
 */
@SpringBootApplication
public class SessionExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SessionExampleApplication.class, args);
    }
}

配置application.ymlcookie

spring:
  session:
    redis:
      flush-mode: on_save
      namespace: session.example
      cleanup-cron: 0 * * * * *
    store-type: redis
    timeout: 1800
  redis:
    host: localhost
    port: 6379
    jedis:
      pool:
        max-active: 100
        max-wait: 10
        max-idle: 10
        min-idle: 10
    database: 0
編寫controller

編寫登陸控制器,登陸時建立session,並將當前登陸用戶存儲sesion中。登出時,使session失效。session

/**
 * 登陸控制器
 *
 * @author huaijin
 */
@RestController
public class LoginController {

    private static final String CURRENT_USER = "currentUser";

    /**
     * 登陸
     *
     * @param loginVo 登陸信息
     *
     * @author huaijin
     */
    @PostMapping("/login.do")
    public String login(@RequestBody LoginVo loginVo, HttpServletRequest request) {
        UserVo userVo = UserVo.builder().userName(loginVo.getUserName())
                .userPassword(loginVo.getUserPassword()).build();
        HttpSession session = request.getSession();
        session.setAttribute(CURRENT_USER, userVo);
        System.out.println("create session, sessionId is:" + session.getId());
        return "ok";
    }

    /**
     * 登出
     *
     * @author huaijin
     */
    @PostMapping("/logout.do")
    public String logout(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        session.invalidate();
        return "ok";
    }
}

編寫查詢控制器,在登陸建立session後,使用將sessionId置於cookie中訪問。若是沒有session將返回錯誤。app

/**
 * 查詢
 *
 * @author huaijin
 */
@RestController
@RequestMapping("/session")
public class QuerySessionController {

    @GetMapping("/query.do")
    public String querySessionId(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return "error";
        }
        System.out.println("current's user is:" + session.getId() +  "in session");
        return "ok";
    }
}
編寫Session刪除事件監聽器

Session刪除事件監聽器用於監聽登出時使session失效的事件源。ide

/**
 * session事件監聽器
 *
 * @author huaijin
 */
@Component
public class SessionEventListener implements ApplicationListener<SessionDeletedEvent> {

    private static final String CURRENT_USER = "currentUser";

    @Override
    public void onApplicationEvent(SessionDeletedEvent event) {
        Session session = event.getSession();
        UserVo userVo = session.getAttribute(CURRENT_USER);
        System.out.println("invalid session's user:" + userVo.toString());
    }
}
驗證測試

編寫spring-boot測試類,測試controller,驗證spring-session是否生效。

/**
 * 測試Spring-Session:
 * 1.登陸時建立session
 * 2.使用sessionId能正常訪問
 * 3.session過時銷燬,可以監聽銷燬事件
 *
 * @author huaijin
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SpringSessionTest {

    @Autowired
    private MockMvc mockMvc;


    @Test
    public void testLogin() throws Exception {
        LoginVo loginVo = new LoginVo();
        loginVo.setUserName("admin");
        loginVo.setUserPassword("admin@123");
        String content = JSON.toJSONString(loginVo);

        // mock登陸
        ResultActions actions = this.mockMvc.perform(post("/login.do")
                .content(content).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(content().string("ok"));
        String sessionId = actions.andReturn()
                .getResponse().getCookie("SESSION").getValue();

        // 使用登陸的sessionId mock查詢
        this.mockMvc.perform(get("/session/query.do")
                .cookie(new Cookie("SESSION", sessionId)))
                .andExpect(status().isOk()).andExpect(content().string("ok"));

        // mock登出
        this.mockMvc.perform(post("/logout.do")
                .cookie(new Cookie("SESSION", sessionId)))
                .andExpect(status().isOk()).andExpect(content().string("ok"));
    }
}

測試類執行結果:

create session, sessionId is:429cb0d3-698a-475a-b3f1-09422acf2e9c
current's user is:429cb0d3-698a-475a-b3f1-09422acf2e9cin session
invalid session's user:UserVo{userName='admin', userPassword='admin@123'

登陸時建立Session,存儲當前登陸用戶。而後在以登陸響應返回的SessionId查詢用戶。最後再登出使Session過時。

spring-boot整合spring-session自動配置原理

前兩篇文章介紹spring-session原理時,總結spring-session的核心模塊。這節中探索spring-boot中自動配置如何初始化spring-session的各個核心模塊。

spring-boot-autoconfigure模塊中包含了spinrg-session的自動配置。包org.springframework.boot.autoconfigure.session中包含了spring-session的全部自動配置項。

其中RedisSession的核心配置項是RedisHttpSessionConfiguration類。

@Configuration
@ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class })
@ConditionalOnMissingBean(SessionRepository.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@Conditional(ServletSessionCondition.class)
@EnableConfigurationProperties(RedisSessionProperties.class)
class RedisSessionConfiguration {

    @Configuration
    public static class SpringBootRedisHttpSessionConfiguration
            extends RedisHttpSessionConfiguration {

        // 加載application.yml或者application.properties中自定義的配置項:
        // 命名空間:用於做爲session redis key的一部分
        // flushmode:session寫入redis的模式
        // 定時任務時間:即訪問redis過時鍵的定時任務的cron表達式
        @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());
        }

    }

}

RedisSessionConfiguration配置類中嵌套SpringBootRedisHttpSessionConfiguration繼承了RedisHttpSessionConfiguration配置類。首先看下該配置類持有的成員。

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration
        implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware,
        SchedulingConfigurer {


    // 默認的cron表達式,application.yml能夠自定義配置
    static final String DEFAULT_CLEANUP_CRON = "0 * * * * *";

    // session的有效最大時間間隔, application.yml能夠自定義配置
    private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;

    // session在redis中的命名空間,主要爲了區分session,application.yml能夠自定義配置
    private String redisNamespace = RedisOperationsSessionRepository.DEFAULT_NAMESPACE;

    // session寫入Redis的模式,application.yml能夠自定義配置
    private RedisFlushMode redisFlushMode = RedisFlushMode.ON_SAVE;

    // 訪問過時Session集合的定時任務的定時時間,默認是每整分運行任務
    private String cleanupCron = DEFAULT_CLEANUP_CRON;

    private ConfigureRedisAction configureRedisAction = new ConfigureNotifyKeyspaceEventsAction();

    // spring-data-redis的redis鏈接工廠
    private RedisConnectionFactory redisConnectionFactory;

    // spring-data-redis的RedisSerializer,用於序列化session中存儲的attributes
    private RedisSerializer<Object> defaultRedisSerializer;

    // session時間發佈者,默認注入的是AppliationContext實例
    private ApplicationEventPublisher applicationEventPublisher;

    // 訪問過時session鍵的定時任務的調度器
    private Executor redisTaskExecutor;

    private Executor redisSubscriptionExecutor;

    private ClassLoader classLoader;

    private StringValueResolver embeddedValueResolver;
}

該配置類中初始化了RedisSession的最爲核心模塊之一RedisOperationsSessionRepository。

@Bean
public RedisOperationsSessionRepository sessionRepository() {
    // 建立RedisOperationsSessionRepository
    RedisTemplate<Object, Object> redisTemplate = createRedisTemplate();
    RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(
            redisTemplate);
    // 設置Session Event發佈者。若是對此迷惑,傳送門:https://www.cnblogs.com/lxyit/p/9719542.html
    sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
    if (this.defaultRedisSerializer != null) {
        sessionRepository.setDefaultSerializer(this.defaultRedisSerializer);
    }
    // 設置默認的Session最大有效期間隔
    sessionRepository
            .setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
    // 設置命名空間
    if (StringUtils.hasText(this.redisNamespace)) {
        sessionRepository.setRedisKeyNamespace(this.redisNamespace);
    }
    // 設置寫redis的模式
    sessionRepository.setRedisFlushMode(this.redisFlushMode);
    return sessionRepository;
}

同時也初始化了Session事件監聽器MessageListener模塊

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
    // 建立MessageListener容器,這屬於spring-data-redis範疇,略過
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(this.redisConnectionFactory);
    if (this.redisTaskExecutor != null) {
        container.setTaskExecutor(this.redisTaskExecutor);
    }
    if (this.redisSubscriptionExecutor != null) {
        container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
    }
    // 模式訂閱redis的__keyevent@*:expired和__keyevent@*:del通道,
    // 獲取redis的鍵過時和刪除事件通知
    container.addMessageListener(sessionRepository(),
            Arrays.asList(new PatternTopic("__keyevent@*:del"),
                    new PatternTopic("__keyevent@*:expired")));
    // 模式訂閱redis的${namespace}:event:created:*通道,當該向該通道發佈消息,
    // 則MessageListener消費消息並處理
    container.addMessageListener(sessionRepository(),
            Collections.singletonList(new PatternTopic(
                    sessionRepository().getSessionCreatedChannelPrefix() + "*")));
    return container;
}

上篇文章中介紹到的spring-session event事件原理,spring-session在啓動時監聽Redis的channel,使用Redis的鍵空間通知處理Session的刪除和過時事件和使用Pub/Sub模式處理Session建立事件。

關於RedisSession的存儲管理部分已經初始化,可是spring-session的另外一個基礎設施模塊SessionRepositoryFilter是在RedisHttpSessionConfiguration父類SpringHttpSessionConfiguration中初始化。

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

spring-boot整合spring-session配置的層次:

RedisSessionConfiguration
    |_ _ SpringBootRedisHttpSessionConfiguration
            |_ _ RedisHttpSessionConfiguration
                    |_ _ SpringHttpSessionConfiguration

回顧思考spring-boot自動配置spring-session,很是合理。

  • SpringHttpSessionConfiguration是spring-session自己的配置類,與spring-boot無關,畢竟spring-session也能夠整合單純的spring項目,只須要使用該spring-session的配置類便可。
  • RedisHttpSessionConfiguration用於配置spring-session的Redission,畢竟spring-session還支持其餘的各類session:Map/JDBC/MogonDB等,將其從SpringHttpSessionConfiguration隔離開來,遵循開閉原則和接口隔離原則。可是其必須依賴基礎的SpringHttpSessionConfiguration,因此使用了繼承。RedisHttpSessionConfiguration是spring-session和spring-data-redis整合配置,須要依賴spring-data-redis。
  • SpringBootRedisHttpSessionConfiguration纔是spring-boot中關鍵配置
  • RedisSessionConfiguration主要用於處理自定義配置,將application.yml或者application.properties的配置載入。

Tips:
配置類也有至關強的設計模式。遵循開閉原則:對修改關閉,對擴展開放。遵循接口隔離原則:變化的就要單獨分離,使用不一樣的接口隔離。SpringHttpSessionConfiguration和RedisHttpSessionConfiguration的設計深深體現這兩大原則。

參考

Spring Session

相關文章
相關標籤/搜索