glmapper

SpringSession系列-请求与响应重写

字数统计: 2.1k阅读时长: 8 min
2018/11/24 Share

我们知道,HttpServletRequsetHttpServletResponseServlet标准所指定的Java语言与Web容器进行交互的接口。接口本身只规定java语言对web容器进行访问的行为方式,而具体的实现是由不同的web容器在其内部实现的。

那么在运行期,当我们需要对HttpServletRequsetHttpServletResponse的默认实例进行扩展时,我们就可以继承HttpServletRequestWrapperHttpServletResponseWrapper来实现。
  
SpringSession中因为我们要实现不依赖容器本身的getSession 实现,因此需要扩展 HttpServletRequset,通过重写getSession来实现分布式session的能力。下面就来看下SpringSession中对于HttpServletRequset的扩展。

1、请求重写

SpringSession 中对于请求重写,在能力上主要体现在存储方面,也就是getSession方法上。在 SessionRepositoryFilter 这个类中,是通过内部类的方式实现了对HttpServletRequsetHttpServletResponse的扩展。

1.1 HttpServletRequset 扩展实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private final class SessionRepositoryRequestWrapper
extends HttpServletRequestWrapper {
// HttpServletResponse 实例
private final HttpServletResponse response;
// ServletContext 实例
private final ServletContext servletContext;
// requestedSession session对象
private S requestedSession;
// 是否缓存 session
private boolean requestedSessionCached;
// sessionId
private String requestedSessionId;
// sessionId 是否有效
private Boolean requestedSessionIdValid;
// sessionId 是否失效
private boolean requestedSessionInvalidated;

// 省略方法
}

1.2 构造方法

1
2
3
4
5
6
private SessionRepositoryRequestWrapper(HttpServletRequest request,
HttpServletResponse response, ServletContext servletContext) {
super(request);
this.response = response;
this.servletContext = servletContext;
}

构造方法里面将 HttpServletRequestHttpServletResponse 以及 ServletContext 实例传递进来,以便于后续扩展使用。

1.3 getSession 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Override
public HttpSessionWrapper getSession(boolean create) {
// 从当前请求线程中获取 session
HttpSessionWrapper currentSession = getCurrentSession();
// 如果有直接返回
if (currentSession != null) {
return currentSession;
}
// 从请求中获取 session,这里面会涉及到从缓存中拿session的过程
S requestedSession = getRequestedSession();
if (requestedSession != null) {
// 无效的会话id(不支持的会话存储库)请求属性名称。
// 这里看下当前的sessionId是否有效
if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
// 设置当前session的最后访问时间,用于延迟session的有效期
requestedSession.setLastAccessedTime(Instant.now());
// 将requestedSessionIdValid置为true
this.requestedSessionIdValid = true;
// 包装session
currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
// 不是新的session,如果是新的session则需要改变sessionId
currentSession.setNew(false);
// 将session设置到当前请求上下文
setCurrentSession(currentSession);
// 返回session
return currentSession;
}
}
else {
// 这里处理的是无效的sessionId的情况,但是当前请求线程 session有效
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
}
// 将invalidSessionId置为true
setAttribute(INVALID_SESSION_ID_ATTR, "true");
}
// 是否需要创建新的session
if (!create) {
return null;
}
if (SESSION_LOGGER.isDebugEnabled()) {
SESSION_LOGGER.debug(
"A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
+ SESSION_LOGGER_NAME,
new RuntimeException(
"For debugging purposes only (not an error)"));
}
// 创建新的session
S session = SessionRepositoryFilter.this.sessionRepository.createSession();
// 设置最后访问时间,也就是指定了当前session的有效期限
session.setLastAccessedTime(Instant.now());
// 包装下当前session
currentSession = new HttpSessionWrapper(session, getServletContext());
//设置到当前请求线程
setCurrentSession(currentSession);
return currentSession;
}

上面这段代码有几个点,这里单独来解释下。

  • getCurrentSession
    • 这是为了在同一个请求过程中不需要重复的去从存储中获取session,在一个新的进来时,将当前的 session 设置到当前请求中,在后续处理过程如果需要getSession就不需要再去存储介质中再拿一次。
  • getRequestedSession
    • 这个是根据请求信息去取session,这里面就包括了sessionId解析,从存储获取session对象等过程。
  • 是否创建新的session对象
    • 在当前请求中和存储中都没有获取到session信息的情况下,这里会根据create参数来判断是否创建新的session。这里一般用户首次登录时或者session失效时会走到。

1.4 getRequestedSession

根据请求信息来获取session对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private S getRequestedSession() {
// 缓存的请求session是否存在
if (!this.requestedSessionCached) {
// 获取 sessionId
List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
.resolveSessionIds(this);
// 通过sessionId来从存储中获取session
for (String sessionId : sessionIds) {
if (this.requestedSessionId == null) {
this.requestedSessionId = sessionId;
}
S session = SessionRepositoryFilter.this.sessionRepository
.findById(sessionId);
if (session != null) {
this.requestedSession = session;
this.requestedSessionId = sessionId;
break;
}
}
this.requestedSessionCached = true;
}
return this.requestedSession;
}

这段代码还是很有意思的,这里获取sessionId返回的是个列表。当然这里是SpringSession的实现策略,因为支持session,所以这里以列表的形式返回的。OK,继续来看如何解析sessionId的:

这里可以看到SpringSession对于sessionId获取的两种策略,一种是基于cookie,一种是基于header;分别来看下具体实现。

1.4.1 CookieHttpSessionIdResolver 获取 sessionId

CookieHttpSessionIdResolver 中获取sessionId的核心代码如下:

其实这里没啥好说的,就是读cookie。从requestcookie信息拿出来,然后遍历找当前sessionId对应的cookie,这里的判断也很简单, 如果是以SESSION开头,则表示是 SessionId,毕竟cookie是共享的,不只有sessionId,还有可能存储其他内容。

另外这里面有个 jvmRoute,这个东西实际上很少能够用到,因为大多数情况下这个值都是null。这个我们在分析CookieSerializer时再来解释。

1.4.2 HeaderHttpSessionIdResolver 获取 sessionId


这个获取更直接粗暴,就是根据 headerNameheader 中取值。

回到getRequestedSession,剩下的代码中核心的都是和sessionRepository这个有关系,这部分就会涉及到存储部分。不在本篇的分析范围之内,会在存储实现部分来分析。

1.5 HttpSessionWrapper

上面的代码中当我们拿到session实例是通常会包装下,那么用到的就是这个HttpSessionWrapper

HttpSessionWrapper 继承了 HttpSessionAdapter,这个HttpSessionAdapter就是将SpringSession 转换成一个标准HttpSession的适配类。HttpSessionAdapter 实现了标准servlet规范的HttpSession接口。

1.5.1 HttpSessionWrapper

HttpSessionWrapper 重写了 invalidate方法。从代码来看,调用该方法产生的影响是:

  • requestedSessionInvalidated 置为true,标识当前 session 失效。
  • 将当前请求中的session设置为null,那么在请求的后续调用中通过getCurrentSession将拿不到session信息。
  • 当前缓存的 session 清楚,包括sessionId,session实例等。
  • 删除存储介质中的session对象。

1.5.2 HttpSessionAdapter

SpringSession和标准HttpSession的配置器类。这个怎么理解呢,来看下一段代码:

1
2
3
4
5
@Override
public Object getAttribute(String name) {
checkState();
return this.session.getAttribute(name);
}

对于基于容器本身实现的HttpSession来说,getAttribute的实现也是有容器本身决定。但是这里做了转换之后,getAttribute将会通过SpringSession中实现的方案来获取。其他的API适配也是基于此实现。

SessionCommittingRequestDispatcher

实现了 RequestDispatcher 接口。关于RequestDispatcher可以参考这篇文章【Servlet】关于RequestDispatcher的原理SessionCommittingRequestDispatcherforward的行为并没有改变。
对于include则是在include之前提交session。为什么这么做呢?

因为include方法使原先的Servlet和转发到的Servlet都可以输出响应信息,即原先的Servlet还可以继续输出响应信息;即请求转发后,原先的Servlet还可以继续输出响应信息,转发到的Servlet对请求做出的响应将并入原先Servlet的响应对象中。

所以这个在include调用之前调用commit,这样可以确保被包含的Servlet程序不能改变响应消息的状态码和响应头。

2 响应重写

响应重写的目的是确保在请求提交时能够把session保存起来。来看下SessionRepositoryResponseWrapper类的实现:


这里面实现还就是重写onResponseCommitted,也就是上面说的,在请求提交时能够通过这个回调函数将session保存到存储容器中。

2.1 session 提交

最后来看下 commitSession

这个过程不会再去存储容器中拿session信息,而是直接从当前请求中拿。如果拿不到,则在回写cookie时会将当前session对应的cookie值设置为空,这样下次请求过来时携带的sessionCookie就是空,这样就会重新触发登陆。

如果拿到,则清空当前请求中的session信息,然后将session保存到存储容器中,并且将sessionId回写到cookie中。

小结

本篇主要对SpringSession中重写RequestResponse进行了分析。通过重写Request请求来将session的存储与存储容器关联起来,通过重写Response来处理session提交,将session保存到存储容器中。

后面我们会继续来分析SpringSession的源码。最近也在学习链路跟踪相关的技术,也准备写一写,有兴趣的同学可以一起讨论。

原文作者:GuoLei Song

原文链接:http://www.glmapper.com/2018/11/24/spring-session-req-resp/

发表日期:November 24th 2018, 4:54:07 pm

更新日期:December 14th 2019, 11:01:45 am

版权声明:转载请注明出处

CATALOG
  1. 1. 1、请求重写
    1. 1.1. 1.1 HttpServletRequset 扩展实现
    2. 1.2. 1.2 构造方法
    3. 1.3. 1.3 getSession 方法
    4. 1.4. 1.4 getRequestedSession
      1. 1.4.1. 1.4.1 CookieHttpSessionIdResolver 获取 sessionId
      2. 1.4.2. 1.4.2 HeaderHttpSessionIdResolver 获取 sessionId
    5. 1.5. 1.5 HttpSessionWrapper
      1. 1.5.1. 1.5.1 HttpSessionWrapper
      2. 1.5.2. 1.5.2 HttpSessionAdapter
    6. 1.6. SessionCommittingRequestDispatcher
  2. 2. 2 响应重写
    1. 2.1. 2.1 session 提交
  3. 3. 小结
  4. 4.