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
这个过滤器来达到目的吧。
总结
该文讲述的内容虽然并不难,但我认为还是比较“时髦”的,相信能给到很多人予以帮助,那就足够了。
最后提示一小点:有人留言我说可以使用RequestContextListener
这个监听器,它也能给RequestContext
赋值完成绑定。答案是可以的,因为它是一个源生的Servlet请求监听器:javax.servlet.ServletRequestListener
可以监听到每个请求,RequestContextListener
是Spring给出的监听器实现,因此只要你在xml里配置上它/or @Bean的方式也是可行的,只是上面已经说了,绝大部分情况下并不需自己麻烦自己的这么做。
声明
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
- ThreadLocal能解决线程安全问题?胡扯!本文教你正确的使用姿势【享学Java】
- [享学Jackson] 一、初识Jackson – 世界上最好的JSON库
- [享学Jackson] 二、jackson-core之流式API与JsonFactory、JsonGenerator、JsonParser
- [享学Jackson] 三、jackson-databind之ObjectMapper与数据绑定、树模型
- [享学Jackson] 四、控制Jackson行为的特征们之JsonFactory.Feature、JsonGenerator.Feature、JsonParser.Feature
- [享学Jackson] 五、控制Jackson行为的特征们之JsonWriteFeature、JsonReadFeature
- [享学Jackson] 六、控制Jackson行为的特征们之MapperFeature、SerializationFeature、DeserializationFeature
- [享学Jackson] 七、Jackson使用bit位运算来开启/禁用Feature的原理解析
- [享学Jackson] 八、jackson-databind数据绑定基础配置之BaseSettings、MapperConfig、MapperConfigBase
- [享学Jackson] 九、jackson-databind数据绑定序列化/反序列化配置之SerializationConfig、DeserializationConfig
- [享学Jackson] 十、jackson-databind序列化之ObjectMapper序列化原理、序列化器匹配原理
- [享学Jackson] 十一、jackson-databind之JsonSerializer序列化器全解析
- [享学Jackson] 十二、jackson-databind反序列化之ObjectMapper反序列化原理、JsonDeserializer反序列化器全解析
- [享学Jackson] 十三、jackson-annotation注解模块全解析及Jackson注解大全
- [享学Jackson] 十四、深入理解Jackson的Module模块化设计及原理分析
- [享学Jackson] 十五、第三方模块Module的深度实践:JavaTimeModule、JSR310Module、ParameterNamesModule、Jdk8Module
- [享学Jackson] 十六、Jackson在Spring MVC中的使用之Date、JSR310时间类型的处理
- [享学Jackson] 十七、spring-web整合Jackson源码解析之Jackson2ObjectMapperBuilder
- [享学Jackson] 十八、Spring容器深度整合Jackson的桥梁之SpringHandlerInstantiator
- [享学Jackson] 十九、Spring下使用ObjectMapper的正确姿势 — Jackson2ObjectMapperFactoryBean
- [享学Jackson] 二十、Spring MVC下的Jackson — MappingJackson2HttpMessageConverter
- [享学Jackson] 二十一、Spring Boot下的Jackson — JacksonAutoConfiguration自动配置
- [享学Jackson] 二十二、Jackson与Fastjson的恩怨情仇(完结篇)
标签:HttpServletRequest,Jackson,享学,RequestContextHolder,Spring,request,MVC,请求 来源: https://blog.csdn.net/f641385712/article/details/104579949