前兩篇介紹了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的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
編寫登陸控制器,登陸時建立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失效的事件源。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-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,很是合理。
Tips:
配置類也有至關強的設計模式。遵循開閉原則:對修改關閉,對擴展開放。遵循接口隔離原則:變化的就要單獨分離,使用不一樣的接口隔離。SpringHttpSessionConfiguration和RedisHttpSessionConfiguration的設計深深體現這兩大原則。