本文内容参考spring-session(一)揭秘,因为2.X新版本的源码差异较大,所以在这里重新进行整理
一、spring session的使用
利用spring boot可以很容易地实现spring session,传送门:
Spring Boot入门教程(五十六): Spring Session
二、总体介绍和文章结构
注:阅读本篇之前需要对java web体系有着基本的了解,例如servlet,HttpServletRequest,HttpServletResponse,filter,HttpSession等。
Spring session主要用于解决多web应用共享session的问题。它主要支持以下三个功能:
- 把servlet容器实现的httpSession替换为spring-session,把session存储到第三方存储容器
- 支持多个session问题。
- 不依赖于cookie。可通过header来传递JSessionId
本文将spring session的源码解析按照四个部分来介绍:
- session
- SessionRepositoryFilter
- request&response
- repository
同时本文的源码版本为2.2.x
三、Session
Java Servlet 规范中定义了HttpSession、HttpServletRequest、HttpServletResponse等接口,诸如tomcat等标准web容器都会遵循这个规约,都是基于这些接口进行开发使用。
而spring为了给自身使用,单独抽象出了一个Session接口,并设计了一套实现类。
但是,为了不影响tomcat等标准web容器的正常使用,spring最后输出的必须得是HttpSession类型的接口,而spring的session接口与HttpSession接口是两个完全独立的接口,就好比usb接口和type c接口,因此需要设计一个适配器来进行适配(适配器模式)。在拥有
session相关的类图如下所示:
HttpSession接口
Session接口
HttpSessionAdapter
作为适配器的具体原理有以下两部分组成:
- 实现了HttpSession接口的同时,内置了Spring的Session。
class HttpSessionAdapter<S extends Session> implements HttpSession {
...
private S session;
...
}
- 重写了HttpSession接口的所有方法,方法都是通过Spring的Session来实现的,这样就相当于USB接口的具体实现全部换成了Type C的具体实现。
以getAtribute()方法为例:
@Override
public Object getAttribute(String name) {
checkState();
return this.session.getAttribute(name);
}
HttpSessionWrapper
HttpSessionWrapper相对HttpSessionAdapter而言仅仅多了一个invalidate()方法,用于将session失效。
HttpSessionWrapper是spring session体系里,session的使用单位,它从表面上看是HttpSession,但其内部实现其实是spring session。
因此将该类作为输出,便可以满足”在遵循java servlet规范的同时,实现spring session功能“这一条件。
MapSession
spring的session接口根据存储工具的不同分为很多种类,有MapSession、RedisSession、JdbcSession、MongoSession等。
private Map<String, Object> sessionAttrs = new HashMap<>();
RedisSession
四、SessionRepositoryFilter
Filter过滤器是java web的三大组件之一,在Filter定义范围内的request或response都会被filter拦截并执行内部的方法。
SessionRepositoryFilter会将拦截的请求或响应进行以下操作:
- 将HttpServletRequest包装成SessionRepositoryRequestWrapper
- 将HttpServletResponse包装成SessionRepositoryResponseWrapper
- commitSession()提交session
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
//1.将HttpServletRequest包装成SessionRepositoryRequestWrapper
SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
//2.将HttpServletResponse包装成SessionRepositoryResponseWrapper
SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest,
response);
try {
filterChain.doFilter(wrappedRequest, wrappedResponse);
}
finally {
//3. commitSession()提交session
wrappedRequest.commitSession();
}
}
五、request&response
类的关系图:
HttpServletRequest
HttpServletRequestWrapper
(该类并不需要学会使用,清楚为什么要用这个即可)
HttpServletRequestWrapper 往往与拦截器Filter一共使用,java web的filter主要负责两个功能:
- 检查用户的输入
- 压缩web内容
而实际上,当我们在使用filter的时候却会发现至少有一半的时间我们都想改变HttpServletRequest对象的参数。
例如:用filter在HttpServletRequest对象到达Servlet之前将用户输入的空格去掉。但是java.util.Map包装的HttpServletRequest对象的参数是不可改变的。因此需要通过HttpServletRequestWrapper来改变HttpServletRequest对象。
具体使用方法:自定义一个装饰器,并继承HttpServletRequestWrapper。
Spring session便是使用这个机制,自定义了SessionRepositoryRequestWrapper和SessionRepositoryResponseWrapper。
其中SessionRepositoryRequestWrapper的作用就是改变HttpServletRequest里的session。
SessionRepositoryRequestWrapper
SessionRepositoryRequestWrapper的主要功能就是改变HttpServletRequest,将其内部的HttpSession切换至HttpSessionWrapper,这么做的原因是HttpSessionWrapper从表面上看是HttpSession,但其内部实现其实是spring session。
该类的主要方法有getSession()和commitSession(),具体流程图如下所示:
SessionRepositoryResponseWrapper
该装饰器的作用只有一个:确保response被commit时,session已被保存下来。
实现原理:
- 类里有一个成员变量,
private final SessionRepositoryRequestWrapper request;
- 重写了onResponseCommitted()方法
@Override
protected void onResponseCommitted() {
this.request.commitSession();
}
问题:Filter时已经commitSession过,这里为什么还要再commitSession一次?
答案可以去spring-session(一)揭秘上看看,该文的作者大神对这个问题在git上找spring session作者进行了提问并得到了回答。
个人总结的答案:
为了确保response被提交之前seesion已经被创建,这样在提交之前才能将session的一些信息写入到response中(例如在cookie中写入sessionId)。
调用onResponseCommitted()后,会接着调用flushBuffer()方法,将缓存区内的所有数据发送给客户端,因此要确保在这一步签名session已经被提交。
六、repository
类关系图:
RedisSessionRepository
该类用于在redis中对session进行操作。