spring-session(二)与spring-boot整合实战
本文内容纲要:
前两篇介绍了spring-session的原理,这篇在理论的基础上再实战。
spring-boot整合spring-session的自动配置可谓是开箱即用,极其简洁和方便。这篇文章即介绍spring-boot整合spring-session,这里只介绍基于RedisSession的实战。
原理篇是基于spring-sessionv1.2.2版本,考虑到RedisSession模块与spring-sessionv2.0.6版本的差异很小,且能够与spring-bootv2.0.0兼容,所以实战篇是基于spring-bootv2.0.0基础上配置spring-session。
源码请戮session-example
实战
搭建spring-boot工程这里飘过,传送门:https://start.spring.io/
配置spring-session
引入spring-session的pom配置,由于spring-boot包含spring-session的starter模块,所以pom中依赖:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
编写springboot启动类SessionExampleApplication
/**
*启动类
*
*@authorhuaijin
*/
@SpringBootApplication
publicclassSessionExampleApplication{
publicstaticvoidmain(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失效。
/**
*登录控制器
*
*@authorhuaijin
*/
@RestController
publicclassLoginController{
privatestaticfinalStringCURRENT_USER="currentUser";
/**
*登录
*
*@paramloginVo登录信息
*
*@authorhuaijin
*/
@PostMapping("/login.do")
publicStringlogin(@RequestBodyLoginVologinVo,HttpServletRequestrequest){
UserVouserVo=UserVo.builder().userName(loginVo.getUserName())
.userPassword(loginVo.getUserPassword()).build();
HttpSessionsession=request.getSession();
session.setAttribute(CURRENT_USER,userVo);
System.out.println("createsession,sessionIdis:"+session.getId());
return"ok";
}
/**
*登出
*
*@authorhuaijin
*/
@PostMapping("/logout.do")
publicStringlogout(HttpServletRequestrequest){
HttpSessionsession=request.getSession(false);
session.invalidate();
return"ok";
}
}
编写查询控制器,在登录创建session后,使用将sessionId置于cookie中访问。如果没有session将返回错误。
/**
*查询
*
*@authorhuaijin
*/
@RestController
@RequestMapping("/session")
publicclassQuerySessionController{
@GetMapping("/query.do")
publicStringquerySessionId(HttpServletRequestrequest){
HttpSessionsession=request.getSession(false);
if(session==null){
return"error";
}
System.out.println("current'suseris:"+session.getId()+"insession");
return"ok";
}
}
编写Session删除事件监听器
Session删除事件监听器用于监听登出时使session失效的事件源。
/**
*session事件监听器
*
*@authorhuaijin
*/
@Component
publicclassSessionEventListenerimplementsApplicationListener<SessionDeletedEvent>{
privatestaticfinalStringCURRENT_USER="currentUser";
@Override
publicvoidonApplicationEvent(SessionDeletedEventevent){
Sessionsession=event.getSession();
UserVouserVo=session.getAttribute(CURRENT_USER);
System.out.println("invalidsession'suser:"+userVo.toString());
}
}
验证测试
编写spring-boot测试类,测试controller,验证spring-session是否生效。
/**
*测试Spring-Session:
*1.登录时创建session
*2.使用sessionId能正常访问
*3.session过期销毁,能够监听销毁事件
*
*@authorhuaijin
*/
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
publicclassSpringSessionTest{
@Autowired
privateMockMvcmockMvc;
@Test
publicvoidtestLogin()throwsException{
LoginVologinVo=newLoginVo();
loginVo.setUserName("admin");
loginVo.setUserPassword("admin@123");
Stringcontent=JSON.toJSONString(loginVo);
//mock登录
ResultActionsactions=this.mockMvc.perform(post("/login.do")
.content(content).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk()).andExpect(content().string("ok"));
StringsessionId=actions.andReturn()
.getResponse().getCookie("SESSION").getValue();
//使用登录的sessionIdmock查询
this.mockMvc.perform(get("/session/query.do")
.cookie(newCookie("SESSION",sessionId)))
.andExpect(status().isOk()).andExpect(content().string("ok"));
//mock登出
this.mockMvc.perform(post("/logout.do")
.cookie(newCookie("SESSION",sessionId)))
.andExpect(status().isOk()).andExpect(content().string("ok"));
}
}
测试类执行结果:
createsession,sessionIdis:429cb0d3-698a-475a-b3f1-09422acf2e9c
current'suseris:429cb0d3-698a-475a-b3f1-09422acf2e9cinsession
invalidsession'suser: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)
classRedisSessionConfiguration{
@Configuration
publicstaticclassSpringBootRedisHttpSessionConfiguration
extendsRedisHttpSessionConfiguration{
//加载application.yml或者application.properties中自定义的配置项:
//命名空间:用于作为sessionrediskey的一部分
//flushmode:session写入redis的模式
//定时任务时间:即访问redis过期键的定时任务的cron表达式
@Autowired
publicvoidcustomize(SessionPropertiessessionProperties,
RedisSessionPropertiesredisSessionProperties){
Durationtimeout=sessionProperties.getTimeout();
if(timeout!=null){
setMaxInactiveIntervalInSeconds((int)timeout.getSeconds());
}
setRedisNamespace(redisSessionProperties.getNamespace());
setRedisFlushMode(redisSessionProperties.getFlushMode());
setCleanupCron(redisSessionProperties.getCleanupCron());
}
}
}
RedisSessionConfiguration配置类中嵌套SpringBootRedisHttpSessionConfiguration继承了RedisHttpSessionConfiguration配置类。首先看下该配置类持有的成员。
@Configuration
@EnableScheduling
publicclassRedisHttpSessionConfigurationextendsSpringHttpSessionConfiguration
implementsBeanClassLoaderAware,EmbeddedValueResolverAware,ImportAware,
SchedulingConfigurer{
//默认的cron表达式,application.yml可以自定义配置
staticfinalStringDEFAULT_CLEANUP_CRON="0*****";
//session的有效最大时间间隔,application.yml可以自定义配置
privateIntegermaxInactiveIntervalInSeconds=MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
//session在redis中的命名空间,主要为了区分session,application.yml可以自定义配置
privateStringredisNamespace=RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
//session写入Redis的模式,application.yml可以自定义配置
privateRedisFlushModeredisFlushMode=RedisFlushMode.ON_SAVE;
//访问过期Session集合的定时任务的定时时间,默认是每整分运行任务
privateStringcleanupCron=DEFAULT_CLEANUP_CRON;
privateConfigureRedisActionconfigureRedisAction=newConfigureNotifyKeyspaceEventsAction();
//spring-data-redis的redis连接工厂
privateRedisConnectionFactoryredisConnectionFactory;
//spring-data-redis的RedisSerializer,用于序列化session中存储的attributes
privateRedisSerializer<Object>defaultRedisSerializer;
//session时间发布者,默认注入的是AppliationContext实例
privateApplicationEventPublisherapplicationEventPublisher;
//访问过期session键的定时任务的调度器
privateExecutorredisTaskExecutor;
privateExecutorredisSubscriptionExecutor;
privateClassLoaderclassLoader;
privateStringValueResolverembeddedValueResolver;
}
该配置类中初始化了RedisSession的最为核心模块之一RedisOperationsSessionRepository。
@Bean
publicRedisOperationsSessionRepositorysessionRepository(){
//创建RedisOperationsSessionRepository
RedisTemplate<Object,Object>redisTemplate=createRedisTemplate();
RedisOperationsSessionRepositorysessionRepository=newRedisOperationsSessionRepository(
redisTemplate);
//设置SessionEvent发布者。如果对此迷惑,传送门: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);
returnsessionRepository;
}
同时也初始化了Session事件监听器MessageListener模块
@Bean
publicRedisMessageListenerContainerredisMessageListenerContainer(){
//创建MessageListener容器,这属于spring-data-redis范畴,略过
RedisMessageListenerContainercontainer=newRedisMessageListenerContainer();
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(newPatternTopic("__keyevent@*:del"),
newPatternTopic("__keyevent@*:expired")));
//模式订阅redis的${namespace}:event:created:*通道,当该向该通道发布消息,
//则MessageListener消费消息并处理
container.addMessageListener(sessionRepository(),
Collections.singletonList(newPatternTopic(
sessionRepository().getSessionCreatedChannelPrefix()+"*")));
returncontainer;
}
上篇文章中介绍到的spring-sessionevent事件原理,spring-session在启动时监听Redis的channel,使用Redis的键空间通知处理Session的删除和过期事件和使用Pub/Sub模式处理Session创建事件。
关于RedisSession的存储管理部分已经初始化,但是spring-session的另一个基础设施模块SessionRepositoryFilter是在RedisHttpSessionConfiguration父类SpringHttpSessionConfiguration中初始化。
@Bean
public<SextendsSession>SessionRepositoryFilter<?extendsSession>springSessionRepositoryFilter(
SessionRepository<S>sessionRepository){
SessionRepositoryFilter<S>sessionRepositoryFilter=newSessionRepositoryFilter<>(
sessionRepository);
sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
returnsessionRepositoryFilter;
}
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的设计深深体现这两大原则。
参考
SpringSession
本文内容总结:
原文链接:https://www.cnblogs.com/lxyit/p/9720159.html