前两篇介绍了spring-session的原理,这篇在理论的基础上再实战。
spring-boot整合spring-session的自动配置可谓是开箱即用,极其简洁和方便。这篇文章即介绍spring-boot整合spring-session,这里只介绍基于RedisSession的实战。原理篇是基于spring-session v1.2.2版本,考虑到RedisSession模块与spring-session v2.0.6版本的差异很小,且能够与spring-boot v2.0.0兼容,所以实战篇是基于spring-boot v2.0.0基础上配置spring-session。
源码请戮
实战
搭建spring-boot工程这里飘过,传送门:https://start.spring.io/
配置spring-session
引入spring-session的pom配置,由于spring-boot包含spring-session的starter模块,所以pom中依赖:
org.springframework.session spring-session-data-redis
编写spring boot启动类SessionExampleApplication
/** * 启动类 * * @author huaijin */@SpringBootApplicationpublic class SessionExampleApplication { public static void main(String[] args) { SpringApplication.run(SessionExampleApplication.class, args); }}
配置application.yml
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失效。
/** * 登录控制器 * * @author huaijin */@RestControllerpublic 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将返回错误。
/** * 查询 * * @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失效的事件源。
/** * session事件监听器 * * @author huaijin */@Componentpublic class SessionEventListener implements ApplicationListener{ 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@AutoConfigureMockMvcpublic 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-09422acf2e9ccurrent's user is:429cb0d3-698a-475a-b3f1-09422acf2e9cin sessioninvalid 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@EnableSchedulingpublic 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
该配置类中初始化了RedisSession的最为核心模块之一RedisOperationsSessionRepository。
@Beanpublic RedisOperationsSessionRepository sessionRepository() { // 创建RedisOperationsSessionRepository RedisTemplate
同时也初始化了Session事件监听器MessageListener模块
@Beanpublic 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中初始化。
@BeanpublicSessionRepositoryFilter springSessionRepositoryFilter( SessionRepositorysessionRepository) { SessionRepositoryFiltersessionRepositoryFilter = 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的设计深深体现这两大原则。