前两篇介绍了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. 【餐厅】 What kind of food would you like to eat tonight?

    核心句型 What kind of food would you like to eat tonight? 你今晚想吃哪种菜? What would you like to eat ? 你想吃什么? ...

  2. 初始v4l2(六)-------根据虚拟驱动vivi的使用彻底分析摄像头驱动

    前面的几篇文章已经分析了v4l2的框架,对框架的分析是比较粗浅的,能基本清楚函数之间的调用过程.但是很多内容并没有分析,比如说里面有很多ioctl,并没有分析哪些ioctl是必须的,也没有分析如何从应 ...

  3. c# 第14节 字符方法、转义字符、字符串的方法

    本节内容: 1:字符的定义 2:字符的方法 3: 转义字符 4:字符串简介 5:字符串方法 1:字符的定义 char与Unicode一一对应,一个char 2个字节. 2:字符的使用方法: 实例: s ...

  4. shell 之for循环几种写法

    参见博客 http://blog.csdn.net/babyfish13/article/details/52981110 ,此博客写的非常清晰明了.

  5. 莫烦TensorFlow_10 过拟合

    import tensorflow as tf from sklearn.datasets import load_digits from sklearn.cross_validation impor ...

  6. 编码-转义2-mark

    文本编辑器utf8 "一".encode("gbk") 保存:"一"+utf8     保存为16进制的\xe4\xb8\x80,\x标识了 ...

  7. ASP.NET开发实战——(五)ASP.NET MVC & 分层

    上一篇文章简要说明了MVC所代表的含义并提供了详细的项目及其控制器.视图等内容的创建步骤,最终完成了一个简单ASP.NET MVC程序. 注:MVC与ASP.NET MVC不相等,MVC是一种开发模式 ...

  8. TensorFlow基础篇

    Tensor(张量)意味着N维数组,Flow(流)意味着基于数据流图的计算.TensorFlow的运行机制属于“定义”和“运行”相分离.模型的构建只是相当于定义了一个图结构(代表一个计算任务),图中有 ...

  9. CSP前的板子

    板子A(扩展欧几里得) 题目描述 求关于x的同余方程 ax≡1(modb) 的最小正整数解. 输入格式 一行,包含两个正整数 a,b,用一个空格隔开. 输出格式 一个正整数 x​,即最小正整数解.输入 ...

  10. 请求与上传文件,Session简介,Restful API,Nodemon

    作者 | Jeskson 来源 | 达达前端小酒馆 请求与上传文件 GET请求和POST请求 const express = require('express'); const app = expre ...