【源码在文末】SpringSession实战使用(基于SpringBoot项目)
作者:怀瑾握瑜
来源链接:
https://www.cnblogs.com/lxyit/p/9720159.html
spring-boot 整合 spring-session 的自动配置可谓是开箱即用,极其简洁和方便。这篇文章即介绍 spring-boot 整合 spring-session,这里只介绍基于 RedisSession 的实战。
考虑到 RedisSession 模块与 spring-session v2.0.6 版本的差异很小,且能够与 spring-boot v2.0.0 兼容,所以实战篇是基于 spring-boot v2.0.0 基础上配置 spring-session。
配置 spring-session
引入 spring-session 的 pom 配置,由于 spring-boot 包含 spring-session 的 starter 模块,所以 pom 中依赖:
org.springframework.session
spring-session-data-redis
编写 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 {
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
该配置类中初始化了 RedisSession 的最为核心模块之一 RedisOperationsSessionRepository。
@Bean
public RedisOperationsSessionRepository sessionRepository() {
// 创建RedisOperationsSessionRepository
RedisTemplate
同时也初始化了 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 SessionRepositoryFilter extends Session> springSessionRepositoryFilter(
SessionRepository sessionRepository) {
SessionRepositoryFilter 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参考文:https://spring.io/projects/spring-session#samples
本文例子源码
https://github.com/lixyou/spring-boot-example/tree/master/session-example
更多精彩推荐
☞ 外包程序员入职蚂蚁金服被质疑,网友:人生污点☞ 前后端分离三连问:为何分离?如何分离?分离后的接口规范?☞ 如何设计一个通用的权限管理系统
☞ 去一家小公司从0到1搭建后端架构,做个总结!
☞ 这应该是全网最全的Git分支开发规范手册~
最后,推荐给大家一个有趣有料的公众号:写代码的渣渣鹏,7年老程序员教你写bug,回复 面试或资源 送一你整套开发笔记 有惊喜哦