Spring注入的成员属性HttpServletRequest是线程安全的吗?【享学Spring MVC】
团队的问题就是你脱颖而出的机会,抱怨和埋怨团队就是打自己耳光,说自己无能,更是在放弃机会。
前言
我们知道一个
Http
请求就是一个
Request
对象,Servlet规范中使用
HttpServletRequest
来表示一个Http请求。然而在
Spring MVC
中,官方并不建议你直接使用
Servlet
源生的API,如常见的
HttpServletRequest/HttpServletResponse
等,因为官方认为
Servlet
技术只是web的落地实现之一,它并不希望你使用具体API而和某项技术耦合,比如从
Spring 5.0
开始就出现了web的另一种实现方式:Reactive,它让Servlet技术从之前的必选项变成了可选项。
可即便如此,在日常开发中我们还是希望能得到表示一个请求的
HttpServletRequest
实例,
Spring MVC
也考虑到了这种诉求的“合理性”,所以获取起来其实也非常的方便。
正文
在讨论如题的疑问前,先简单的了解下
Spring MVC
有哪些方式可以得到一个
HttpServletRequest
,也就是每个请求都能对应一个
HttpServletRequest
。
得到HttpServletRequest的三种方式
粗略的统计一下,在
Spring MVC
中
直接
得到
HttpServletRequest
的方式有三种。
方式一:方法参数
在
Controller
的方法参数上写上
HttpServletRequest
,这样每次请求过来得到就是对应的
HttpServletRequest
喽。
@GetMapping("/test/request")
public Object testRequest(HttpServletRequest request) {
System.out.println(request.getClass());
return "success";
}
访问接口,控制台输出:该类属于Servlet自己的实现类,一切正常。
class org.apache.catalina.connector.RequestFacade
据我统计,使用这种方式获取每次请求对象实例是 最多的 ,同时我认为它也是相对来说最为“低级”的一种方式。
想想你的Controller里有10个方法需要得到
HttpServletRequest
,20个?30个呢?会不会疯掉?
方式二:从RequestContextHolder上下文获取
注意:必须强转为
ServletRequestAttributes
才能获取到
HttpServletRequest
,毕竟它属于Servlet专用的API,需要专用的Attr来获取。
@GetMapping("/test/request")
public Object testRequest(HttpServletRequest request) {
// 从请求上下文里获取Request对象
ServletRequestAttributes requestAttributes = ServletRequestAttributes.class.cast(RequestContextHolder.getRequestAttributes());
HttpServletRequest contextRequest = requestAttributes.getRequest();
System.out.println(contextRequest.getClass());
// 比较两个是否是同一个实例
System.out.println(contextRequest == request);
return "success";
}
请求接口,控制台输出:
class org.apache.catalina.connector.RequestFacade
true
需要注意的是,第二个输出的是true哦,证明从请求上下文里获取出来的是和方式一是 同一个对象 。
使用这种方式的
唯一优点
:在
Service
层,甚至
Dao
层需要
HttpServletRequest
对象的话比较方便,而不是通过方法参数传过来,更不优雅。
说明:虽然并不建议,甚至是禁止
HttpServletRequest
进入到Service甚至Dao层,但是万一有这种需求,请使用这种方式把而不要放在方法参数上传参了,很low的有木有。
它的缺点还是比较明显的:代码太长了,就为了获取个请求实例而已写这么多代码,有点小题大做了。况且若是10处要这个实例呢?岂不也要疯掉。当然你可以采用
BaseController
的方案试图缓解一下这个现象,形如这样:
public abstract class BaseController {
public HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
public HttpServletResponse getResponse() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
public HttpSession getSession() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getSession();
}
方式三:依赖注入@Autowired
这种方式是 最为优雅 的获取方式,也是本文将要讲述的重点。
@Autowired
HttpServletRequest requestAuto;
@GetMapping("/test/request")
public Object testRequest(HttpServletRequest request) {
System.out.println(requestAuto.getClass());
System.out.println(requestAuto == request);
return "success";
}
访问接口,打印:
class com.sun.proxy.$Proxy70
false
有没有觉得很奇怪:
@Autowired
注入进来的竟然是个JDK动态代理对象,当然这确是它
保证线程安全的关键点之一
。
使用这种方式获取
HttpServletRequest
为最优雅方式,推荐使用,这样你有再多方法需要都不用怕了,书写一次即可。
当然喽,用这种方式的选手少之又少,原因很简单:
Controller
是单例的,多疑成员属性线程不安全,会有线程安全问题。对自己掌握的知识不自信,从而导致不敢使用这是最直接的原因。
方式四:使用@ModelAttribute(错误方式)
这里特别演示一种错误方式:使用
@ModelAttribute
来获取
HttpServletRequest
实例,形如这样:
private HttpServletRequest request;
@ModelAttribute
public void bindRequest(HttpServletRequest request) {
this.request = request;
}
请注意:
这么做是100%不行的,因为线程不安全
。虽然每次请求进来都会执行一次
bindRequest()
方法得到一个新的request实例,但是**成员属性
request
**它是所有线程共享的,所以这么做是绝对线程不安全的,请各位小伙伴注意喽。
依赖注入@Autowired方式是线程安全的吗?
作为一个有技术敏感性的程序员,你理应提出这样的质疑:
-
Spring MVC中的
@Controller
默认是单例的,其成员变量是在初始化时候就赋值完成了,就不会再变了 -
而对于每一次请求,
HttpServletRequest
理应都是不一样的,否则不就串了吗
既然不可能在每次请求的时候给成员变量重新赋值(即便是这样也无法保证线程安全呀),那么到底什么什么原因使得这种方式靠谱呢?这一切的谜底都在 它是个JDK动态代理对象 上。
@Autowired与代理对象
这里其实设计到
Spring
依赖注入的原理解读,但很显然此处不会展开(有兴趣的朋友可出门左拐,我博客有不少相关文章),直接通过现象反推到结论:
所有的
@Autowired
进来的JDK动态代理对象的
InvocationHandler
处理器均为
AutowireUtils.ObjectFactoryDelegatingInvocationHandler
。
AutowireUtils:
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
private final ObjectFactory<?> objectFactory;
public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
return (proxy == args[0]);
} else if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
} else if (methodName.equals("toString")) {
return this.objectFactory.toString();
// 执行目标方法。注意:目标实例对象是objectFactory.getObject()
try {
return method.invoke(this.objectFactory.getObject(), args);
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
该
InvocationHandler
处理器实现其实很“简陋”,最关键的点在于:最终invoke调用的实例是来自于
objectFactory.getObject()
,而这里使用的
ObjectFactory
是:
WebApplicationContextUtils.RequestObjectFactory
。
RequestObjectFactory
至于为何使用的是这个Factory来处理,请参考web 容器 初始化时的这块代码:
WebApplicationContextUtils:
public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory, @Nullable ServletContext sc) {
// web容器下新增支持了三种scope
// 非web容器(默认)只有单例和多例两种嘛
beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope());
beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope());
if (sc != null) {
ServletContextScope appScope = new ServletContextScope(sc);
beanFactory.registerScope(WebApplicationContext.SCOPE_APPLICATION, appScope);
sc.setAttribute(ServletContextScope.class.getName(), appScope);
// ==================依赖注入=================
// 这里决定了,若你依赖注入ServletRequest的话,就使用RequestObjectFactory来处理你
beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
}
RequestObjectFactory
自己的代码非常非常简单:
WebApplicationContextUtils:
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
// 从当前请求上下文里找到Request对象
@Override
public ServletRequest getObject() {
return currentRequestAttributes().getRequest();
// 从当前请求上下文:RequestContextHolder里找到请求属性,进而就可以拿到请求对象、响应对象等等了
private static ServletRequestAttributes currentRequestAttributes() {
RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes();
if (!(requestAttr instanceof ServletRequestAttributes)) {
throw new IllegalStateException("Current request is not a servlet request");
return (ServletRequestAttributes) requestAttr;
}
到这个节点可以知道,关键点就在于:
RequestContextHolder.currentRequestAttributes()
的值哪儿来的,或者说是什么时候放进去的,放了什么进去?
Spring何时把Request信息放进RequestContextHolder?
首先必须清楚:
RequestContextHolder
它代表着请求上下文,内部使用
ThreadLocal
来维护着,用于在
线程间
传递
RequestAttributes
数据。
// 它是个工具类:用抽象类表示而已 所有方法均静态
public abstract class RequestContextHolder {
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<>("Request context");
... // 省略set、get、reset等方法
}
说明:关于
ThreadLocal
的使用,以及误区什么的,请务必参阅此文: ThreadLocal能解决线程安全问题?胡扯!本文教你正确的使用姿势
需要说明的是:Spring此处使用了
InheritableThreadLocal
用于传递,所以即使你在子线程里也是可以通过上下文
RequestContextHolder
获取到
RequestAttributes
数据的。
要想找到何时向
RequestContextHolder
里放值的,仅需知道何时调用的set方法便可(它有两个set方法,其中一个set方法仅在
RequestContextListener
里被调用,可忽略):
RequestContextFilter
该过滤器
RequestContextFilter
主要是用于第三方serlvet比如
JSF FacesServlet
。在Spring
自己的
Web应用中,如果一个请求最终被
DispatcherServlet
处理,它自己完成请求上下文的维护(比如对
RequestContextHolder
的维护)。
但是,
并不是所有的请求都最终会被DispatcherServlet处理
,比如匿名用户访问一个登录用户才能访问的资源,此时请求只会被
安全过滤器(如TokenFilter)处理,而不会到达DispatcherServlet
,在这种情况下,该过滤器
RequestContextFilter
就起了担当了相应的职责。
RequestContextFilter
负责LocaleContextHolder
和RequestContextHolder
,而在过滤器内部很轻松的可以拿到HttpServletRequest
,所以在不继承第三方Servlet技术的情况下,此Filter几乎用不着~
FrameworkServlet
“排除”上面一种设置的机会,只剩下
FrameworkServlet
了。它的
initContextHolders()
方法和
resetContextHolders()
方法均会维护请求上下文:
FrameworkServlet:
// 处理请求的方法
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
initContextHolders(request, localeContext, requestAttributes);
try {
// 抽象方法:交给DispatcherServlet去实现
doService(request, response);
} catch {
} finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
private void initContextHolders(...) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
说明:
initContextHolders
的另外一处调用处在RequestBindingInterceptor
里,在Async
异步支持时用于绑定的,略。
由此可见,只要请求交给了
FrameworkServlet
处理,那么请求上下文里就必然有
Request/Response
等实例,并且是和每个请求线程绑定的(独享)。而我们绝大多数情况下都是在
Controller
或者后续流程中希望得到
HttpServletRequest
,那时请求上下文就已经把其和当先线程绑定好啦~
依赖注入【确定安全】流程总结
经过这一波分析,通过
@Autowired
方式依赖注入得到
HttpServletRequest
是线程安全的结论是显而易见的了:通过JDK动态代理,每次方法调用实际调用的是实际请求对象
HttpServletRequest
。先对它的关键流程步骤总结如下:
-
在Spring解析
HttpServletRequest
类型的@Autowired
依赖注入时,实际注入的是个JDK动态代理对象 -
该代理对象的处理器是:
ObjectFactoryDelegatingInvocationHandler
,内部实际实例由ObjectFactory
动态提供 ,数据由RequestContextHolder
请求上下文提供,请求上下文的数据在请求达到时被赋值,参照下面步骤 -
该
ObjectFactory
是一个RequestObjectFactory
(这是由web上下文初始化时决定的) -
请求进入时,单反只要经过了
FrameworkServlet
处理,便会在处理时(调用Controller
目标方法前)把Request相关对象设置到RequestContextHolder
的ThreadLocal
中去 -
这样便完成了:调用
Controller
目标方法前完成了Request对象和线程的绑定,所以在目标方法里,自然就可以通过当前线程把它拿出来喽,这一切都拜托的是ThreadLocal
去完成的~
值得注意的是:若有不经过
FrameworkServlet
的请求(比如被过滤器过滤了,Spring MVC拦截器不行的哦它还是会经过FrameworkServlet
处理的),但却又想这么使用,那么请主动配置RequestContextFilter
这个过滤器来达到目的吧。
谨防线程池里使用HttpServletRequest的坑
源码也已经分析了,Spring的
RequestContextHolder
使用的
InheritableThreadLocal
,所以最多支持到
父线程向子线程
的数据传递,因此若你这么使用:
@Autowired
HttpServletRequest requestAuto;
@GetMapping("/test/request")
public Object testRequest() {
new Thread(() -> {
String name = requestAuto.getParameter("name");
System.out.println(name);
}).start();
return "success";
}
是可以 正常work的 ,但若你放在线程池里面执行,形如这样:
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
@Autowired
HttpServletRequest requestAuto;
@GetMapping("/test/request")
public Object testRequest() {
THREAD_POOL.execute(() -> {