前兩篇介紹了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的設計深深體現這兩大原則。