一、引言

当我们第一次接触到 Java Web 开发,从最原生的 Servlet 方法开始,我们就知道在 doGet() 或者 doPost() 方法有两个形参,分别是 HttpServletRequestHttpServletResponse,这两个参数代表了 web 容器为我们封装的 HTTP 请求和 HTTP 响应。

当 Java Web 进化到 SpringMVC 中,一系列的杂活脏活都交给了 DispatcherServlet 前端控制器来处理。

回到正文,传统情况下,我们访问一个接口,想要从中取得 request 对象,或者是 response 对象,亦或者是 httpSession 对象,都是直接作为形参传进来。举个例子,前端传递 token,先经过 filter 得到用户ID,并将它存入 request 中,那么在每个接口中取得用户ID,都要这样:

1
2
3
4
5
@GetMapping(/"test")
public ResponseResult test(HttpServletRequest request, HttpServletResponse response) {
Object userId = request.getAttribute("userId");
...
}

每个接口都要加 HttpServletRequest 或者 HttpServletResponse,第一写起来麻烦,第二看起来参数也很多。那么既然 Spring 可以依赖注入,我们可不可以这样做呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class DemoController {
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;

@GetMapping("/test")
public ResponseResult test() {
Object userId = request.getAttribute("userId");
...
}
}

试了一下竟然真的可以,但是仔细一想就会有几个疑惑:

  1. 既然我可以将它 Autowired 出来,那么它是啥时候被注入的呢?

  2. 我们知道 Spring 容器中的 Bean 默认是单例的,那么这样得到的 request 会不会有问题?并发情况下,一个接口会不会取到另一个接口的 request

二、结论

探讨之前,先说结论。

1. 啥时候注入的?

答:SpringMVC DispatcherServlet 每次处理 HTTP 请求时,会将 web 容器封装的 requestresponse 注入到 Spring 容器中。

2. 这样在并发情况下会不会有问题?

答:不会有问题。内部其实存在一个 ThreadLocal ,不同进程的 requestresponse 是隔离的。

3. 那我们以后是不是可以都这样写了?

答:理论上且实际上这样写都没有问题,但是一般认为接口形参上的 request 和 response 对应着一次 HTTP 请求,因此用注入的方式会让人感觉有点奇怪。

三、为什么注入的没有线程安全问题?

下面来实验下,分别用注入的方式和形参的方式,来看看这两种得到的 Request 和 Response 有什么区别。

因为 request 和 response 的原理是一样的,因此下文只以 request 为例,避免啰嗦。

注入与形参区别

IDEA十分智能,一上来就告诉我们注入 request 的是 $Proxy,形参的 request 是 RequestFacade ,初步得知是通过代理的方式取出 request。

点开注入的 request,发现它是从 AutowireUtils 中的 ObjectFactoryDelegatingInvocationHandler 取出了,点进去看看。

ObjectFactoryDelegatingInvocationHandler

可以看到这个静态内部类具有以下属性:

  • 成员变量:objectFactory
  • 构造方法
  • invoke 方法

invoke 这个名字是不是很熟悉,这不就是反射吗。在该方法内部,对 equalshashCodetoString 方法做了特殊处理,其余的都通过 method.invoke(this.objectFactory.getObject(), args) 反射调用原方法了。

那么就去看看这个 objectFactory.getObject() 是什么,点进去发现 ObjectFactory 是一个接口,根据一开始注入地方的截图,我们知道它的实现类是 WebApplicationContextUtils 中的 RequestObjectFactory,点进去看看。

RequestObjectFactory

它通过调用 currentRequestAttributes().getRequest() ,取出了 ServletRequest,那么点进去看看,它是怎么取得的。

WebApplicationContextUtils

经过一系列的调用,可以看到最后是通过 requestAttributesHolderinheritableRequestAttributesHolder 中取出来的,接着看看这俩的定义。

RequestContextHolder

看到这相信你已经知道了,是从ThreadLocal 中取出来的,这也就说明它是线程隔离的,因此通过注入方式得到的 request 和 response 是线程安全的。

再回想下我们是如何在普通类中取得当前线程的 request 对象,再结合上面的调用流程,是不是豁然开朗。

1
2
// 取得当前线程的 Request 对象
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();

四、它是何时被注入的?

引言中说过,SpringMVC有一个大管家 DispatcherServlet,它的作用及处理流程就不再赘述了,网上相关资料很多。

DispatcherServlet 的父类 FrameworkServlet 中,我们发现所有请求相关的方法,内部都调用了 processRequest 方法。

FrameworkServlet

看起来这个方法就是实际处理 HTTP 请求的,点进去看看。

FrameworkServlet#processRequest

这个方法逻辑也很明晰,我们看其中最关键的 initContextHolders 方法,它将本次请求的 request,以及新初始化的 localContext 和 requestAttributes 传入,点进去看看。

FrameworkServlet#initContextHolders

initContextHolders 方法内部分别调用了:

  • LocaleContextHolder.setLocaleContext()
  • RequestContextHolder.setRequestAttributes()

两个方法的逻辑大致一致,都是根据 inheritable 的取值,来决定 set 进哪个 Holder,remove 出哪个 Holder,下面看下这几个 Holoder 的定义。

Holder 定义

全都是 ThreadLocal,再看看 RequestContextHolder 中的那两个,是不是跟文章上一节最后取出来的那两个是同一个,至此就破案了。

五、番外

在上一节,我们发现Spring会根据 inheritable 的取值来决定从哪个 Holder 中设值,那么这个 inheritable 是个什么东西呢。

inheritable 英文直译为 “可遗传的”,我们知道,默认情况下 ThreadLocal 中的值在每个线程都是独立的,但是 InheritableThreadLocal 却可以在子线程访问父线程中的变量或属性。

RequestContextHolder 中那两个 Holder 为例,看看它的构造类。

NamedInheritableThreadLocal 与 NamedInheritableThreadLocal

不出所料,它们的父类分别就是 ThreadLocalInheritableThreadLocal。那么它是咋实现子访问父的呢,我们去 Thread 类里面看一看。

Thread#init

首先 Thread 类中有两个 ThreadLocalMap,分别是 threadLocalsinheritableThreadLocals

然后看下它的 init() 方法,首先形参传了一个 inheritThreadLocals,表示是否是要继承父线程,如果为 true 情况下,调用 ThreadLocal.createInheritedMap(parent.inheritableThreadLocals) ,也就是使用了父 inheritableThreadLocals 初始化了当前的 inheritableThreadLocals,点进去看看。

ThreadLocal#ThreadLocalMap

代码比较简单,就不细说了,就是一个拷贝。至此为啥 InheritableThreadLocal 能够访问父线程中的变量就破案了。

回归正题,既然注入的不存在线程安全问题,那么这个布尔值自然就是 false 了。如果你要手动改为 true 的话,那么这样注入的可就存在线程安全问题了。

threadContextInheritable