目录
- 一、什么是session
- 1. session定义
- 2. session的机制
- 二、标准session实现和spring-session的区别
- 三、spring-session与springboot集成
- 1. 引入依赖包
- 2. 创建httpSession配置文件
- 3. 根据实际情况个性化redis配置
- 四、spring-session核心流程解析
- 1. SessionRepositoryFilter的包装
- 2. spring-session对request的包装SessionRepositoryRequestWrapper
- 五、总结
一、什么是session
1. session定义
根据Oracle的java servlet 4.0标准,可以找到对sessiont的明确定义:session是在http多次回话中,与client绑定的机制,用于构建有效的web应用,追踪用户的行为。由于http协议是无状态协议,所以session的存在对用户状态绑定非常重要。在上述提到的标准协议里,制定了HttpSession的标准接口,容器(例如tomcat)和各种网络框架都有对这个标准接口的实现。
2. session的机制
同时,协议里也规定了几种标准的session机制,分别是:
cookies: cookies机制是最常用的,同时也要求全部的servlet容器加以实现。容器会将一个cookie随response body一起返回给client,之后的每个请求头里面,client都会将cookie里的值加进去。
SSL Session: SSL回话中,有内建的机制来保证来自一个client中的多个请求,用作session的一部分。
URL Rewriting: 这种方法是很不常用的,当一个client禁用cookie的时候,URL重写会被服务器用作session机制。用于将不同的请求与session相关联。例如:
http://www.example.com/catalog/index.html;jsessionid=1234
二、标准session实现和spring-session的区别
根据Oracle的协议规范,我们知道,web容器需要实现session机制,但是web容器是将其存储在内存之中的。这样,在分布式的应用中,session也就只能在单台机器上有效。作为解决方案之一,我们可以使用spring-session,将session从内存中持久化到外部存储。用一张图可以清晰地表示出他们之间的区别:
三、spring-session与springboot集成
由于springboot开箱即用,集成起来非常简单,只需要三步(我们这里以使用redis存储为例)
1. 引入依赖包
<dependencies>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
</dependencies>
2. 创建httpSession配置文件
package example.helloworld;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@EnableRedisHttpSession
public class HttpSessionConfig {
}
3. 根据实际情况个性化redis配置
spring:
redis:
host: 127.0.0.1
port: 6380
四、spring-session核心流程解析
spring-session的机制很巧妙,主要是应用责任链模式和装饰器模式,在容器的filter_chain中插入session的过滤器SessionRepositoryFilter,在这里将request进行包装,把装饰后的request向下传递。用一个示意图可以进行描述:
接下来对请求过程中session相关的核心流程进行分析
1. SessionRepositoryFilter的包装
SessionRepositoryFilter的核心操作如下:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//根据配置设置session的持久化策略
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//包装request和response为spring-session支持的类型
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
//filterChain向下传递请求
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
//提交session,即将sessionId写入返回结果,并持久化session
wrappedRequest.commitSession();
}
}
2. spring-session对request的包装SessionRepositoryRequestWrapper
spring-session通过使用装饰器模式,来对session相关方法进行个性化。SessionRepositoryRequestWrapper的UML图如下所示:
getSession:
get是tomcat提供的HttpServletRequest的接口,wrapper对其的具体实现如下:
//create表示在当前请求没有session的时候是否创建session
public HttpSessionWrapper getSession(boolean create) {
//从内存中获取session----》关键
HttpSessionWrapper currentSession = getCurrentSession();
if (currentSession != null) {
return currentSession;
}
//从缓存和外部存储中获取session----》关键
S requestedSession = getRequestedSession();
if (requestedSession != null) {
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
//设置session最近活跃时间
requestedSession.setLastAccessedTime(Instant.now());
this.requestedSessionIdValid = true;
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
//将session设置为not new,这里要注意一下,判断一个session是否是new,取决于这 个session是否有client join过
currentSession.markNotNew();
setCurrentSession(currentSession);
return currentSession;
}
}
else {
// This is an invalid session id. No need to ask again if
// request.getSession is invoked for the duration of this request
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
if (!create) {
return null;
}
//根据session存储模式创建与之相适配的session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
//设置session活跃时间
session.setLastAccessedTime(Instant.now());
//创建currentSession
currentSession = new HttpSessionWrapper(session, getServletContext());
//将其放入request的atrributes map中
setCurrentSession(currentSession);
return currentSession;
}
这里的获取session分为两段,有点像分级缓存。其中两个核心的方法如下:
从request的attributes中获取:
private HttpSessionWrapper getCurrentSession() {
return (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR);
}
从外部存储空间获取:
private S getRequestedSession() {
if (!this.requestedSessionCached) {
//根据配置的sessionId解析器来从请求中解析sessionId,默认的session策略是cookies,所以这里会从cookie中读取sessionId的值,key是SESSION
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver.resolveSessionIds(this);
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
//根据session的存储策略,从外部存储空间中读取session
S session = SessionRepositoryFilter.this.sessionRepository.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}
commitSession
在filterChain路由结束之后,回到SessionRepositoryFilter的时候,会对session进行提交,在这里,也就是指在redis中进行持久化,代码如下:
private void commitSession() {
HttpSessionWrapper wrappedSession = getCurrentSession();
if (wrappedSession == null) {
//session不可用
if (isInvalidateClientSession()) {
//变更session状态,提示客户端,对于cookies策略,这里会把cookie里的sessionId清空
SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
}
}
else {
//获取原生的HttpSession
S session = wrappedSession.getSession();
clearRequestedSessionCache();
//持久化session
SessionRepositoryFilter.this.sessionRepository.save(session);
String sessionId = session.getId();
if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
}
}
}
总之,session创建和提交过程中,会根据配置中读取到的session存储模式和session策略来对session进行实例化,例如使用redis,实例化的就是RedisSession,通过抽象来实现解耦。
五、总结
通过上述的分析可知:
1、使用springboot集成spring-session,经过简单的个性化配置即可支持分布式session存储,对用户透明,是否友好;
2、spring-session内部通过责任链和装饰器模式,对tomcat原生的httpRequest进行包装,与其他的机制完美兼容;
3、不同的存储策略对应不同的session子类,便于解耦和拓展。