前两篇介绍了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。

源码请戮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>

编写spring boot启动类SessionExampleApplication

/**
* 启动类
*
* @author huaijin
*/
@SpringBootApplication
public 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
*/
@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将返回错误。

/**
* 查询
*
* @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
*/
@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

spring-session(二)与spring-boot整合实战的更多相关文章

  1. Spring Boot整合实战Spring Security JWT权限鉴权系统

    目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以.像以前做项目的安全认证基于 session 的登录拦截,属于后端全栈式的开发的模式 ...

  2. Re:从零开始的Spring Session(二)

    上一篇文章介绍了一些Session和Cookie的基础知识,这篇文章开始正式介绍Spring Session是如何对传统的Session进行改造的.官网这么介绍Spring Session: Spri ...

  3. Spring(二)--Spring入门案例

    Spring入门案例 1.需要的实体类 2.需要的接口和实现类 3.需要的service和实现类 /** * service层的作用 * 在不改变dao层代码的前提下,增加业务逻辑操作 */ publ ...

  4. Spring Cloud(二):Spring Cloud Eureka Server高可用注册服务中心的配置

    前言 Eureka 作为一个云端负载均衡,本身是一个基于REST的服务,在 Spring Cloud 中用于发现和注册服务. 那么当成千上万个微服务注册到Eureka Server中的时候,Eurek ...

  5. 攻城狮在路上(贰) Spring(二)--- Spring IoC概念介绍

    一.IoC的概念: IoC(控制反转)是Spring容器的核心.另一种解释是DI(依赖注入),即让调用类对某一个接口的依赖关系由第三方注入,以移除调用类对某一个接口实现类的一览. 定义如此,由此可见, ...

  6. Spring学习(二):Spring支持的5种Bean Scope

    序言 Scope是定义Spring如何创建bean的实例的.Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0以后,又引入了另外三种scope ...

  7. Spring系列(二):Spring IoC/DI的理解

    这几天重新学习了一下Spring,在网上找了相关的ppt来看,当看到Spring IoC这一章节的时候,先大致浏览了一下内容,有将近50页的内容,内心窃喜~QAQ~,看完这些内容能够对IoC有更深层次 ...

  8. Spring点滴二:Spring Bean

    Spring Bean: 被称作bean的对象是构成应用程序的支柱,是由Spring Ioc容器管理.bean是一个被实例化,配置.组装并由Spring Ioc容器管理对象. 官网API:A Spri ...

  9. Spring(十二)使用Spring的xml文件配置方式实现AOP

    配置文件与注解方式的有非常大不同,多了非常多配置项. beans2.xml <?xml version="1.0" encoding="UTF-8"? & ...

随机推荐

  1. java 图书馆初级编写

    import java.util.Scanner; import java.util.Arrays; public class book { public static void main(Strin ...

  2. Git 分支代码管理日记备注

    1〉  Bithucket 创建代码库 2〉  下载克隆代码 Git clone 代码链接 3〉  代码初始化完成之后,切换到代码文件夹 cd 文件夹名 4〉  查看分支情况 Git brach 5〉 ...

  3. Docker入门与简单使用

    前言:  Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的Linux或Windows机器上.近几年来,Docker 在国内发展的如 ...

  4. Rust第一次---centos 7下的安装,配置,测试

    现在安装都依赖于网络,命令比较简单. 一,运行脚本,安装runstup 1,运行如下脚本: curl https://sh.rustup.rs -sSf | sh 2,输入默认项1,完成余下的安装 二 ...

  5. 201871010131-张兴盼《面向对象程序设计(java)》第十三周学习总结

    项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...

  6. windows 上jenkins slave 执行脚本提示成功,但是没有运行

    控制台结果,但是脚本没有运行Started by user admin Building remotely on test_1110 in workspace D:\jenkins\workspace ...

  7. day21_7.25 面向对象之继承

    一.继承 什么是继承? 继承是一种关系,就是描述两者之间什么是什么的关系. 在程序中,继承描述的是类与类之间的关系. 例如a如果继承了b,a就具备了b的所有变量与方法,可以直接调用. class B: ...

  8. 09-tensorflow-tf.split()

    # 'value' is a tensor with shape [5, 30] # Split 'value' into 3 tensors with sizes [4, 15, 11] along ...

  9. Ant下载与安装

    1.登录http://ant.apache.org/bindownload.cgi 站点下载Ant最新版建议Windows平台下载*.zip压缩包 2.将下载到的压缩文件压缩到(G:software\ ...

  10. Maven仓库与坐标(五)

    一.Maven仓库 存放依赖的一个位置/文件夹/仓库,分为以下几种: 本地仓库 中央仓库 远程仓库 1. 本地仓库 第一次执行maven命令时被创建,maven运行时需要的构件都从本地仓库获取,本地仓 ...