前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring注入的成员属性HttpServletRequest是线程安全的吗?【享学Spring MVC】

Spring注入的成员属性HttpServletRequest是线程安全的吗?【享学Spring MVC】

作者头像
YourBatman
发布2020-03-18 20:56:01
3.4K0
发布2020-03-18 20:56:01
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

团队的问题就是你脱颖而出的机会,抱怨和埋怨团队就是打自己耳光,说自己无能,更是在放弃机会。

前言

我们知道一个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喽。

代码语言:javascript
复制
@GetMapping("/test/request")
public Object testRequest(HttpServletRequest request) {
    System.out.println(request.getClass());
    return "success";
}

访问接口,控制台输出:该类属于Servlet自己的实现类,一切正常。

代码语言:javascript
复制
class org.apache.catalina.connector.RequestFacade

据我统计,使用这种方式获取每次请求对象实例是最多的,同时我认为它也是相对来说最为“低级”的一种方式。

想想你的Controller里有10个方法需要得到HttpServletRequest,20个?30个呢?会不会疯掉?


方式二:从RequestContextHolder上下文获取

注意:必须强转为ServletRequestAttributes才能获取到HttpServletRequest,毕竟它属于Servlet专用的API,需要专用的Attr来获取。

代码语言:javascript
复制
@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";
}

请求接口,控制台输出:

代码语言:javascript
复制
class org.apache.catalina.connector.RequestFacade
true

需要注意的是,第二个输出的是true哦,证明从请求上下文里获取出来的是和方式一是同一个对象

使用这种方式的唯一优点:在Service层,甚至Dao层需要HttpServletRequest对象的话比较方便,而不是通过方法参数传过来,更不优雅。

说明:虽然并不建议,甚至是禁止HttpServletRequest进入到Service甚至Dao层,但是万一有这种需求,请使用这种方式把而不要放在方法参数上传参了,很low的有木有。

它的缺点还是比较明显的:代码太长了,就为了获取个请求实例而已写这么多代码,有点小题大做了。况且若是10处要这个实例呢?岂不也要疯掉。当然你可以采用BaseController的方案试图缓解一下这个现象,形如这样:

代码语言:javascript
复制
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

这种方式是最为优雅的获取方式,也是本文将要讲述的重点。

代码语言:javascript
复制
@Autowired
HttpServletRequest requestAuto;

@GetMapping("/test/request")
public Object testRequest(HttpServletRequest request) {
    System.out.println(requestAuto.getClass());
    System.out.println(requestAuto == request);
    return "success";
}

访问接口,打印:

代码语言:javascript
复制
class com.sun.proxy.$Proxy70
false

有没有觉得很奇怪:@Autowired注入进来的竟然是个JDK动态代理对象,当然这确是它保证线程安全的关键点之一

使用这种方式获取HttpServletRequest为最优雅方式,推荐使用,这样你有再多方法需要都不用怕了,书写一次即可。 当然喽,用这种方式的选手少之又少,原因很简单:Controller是单例的,多疑成员属性线程不安全,会有线程安全问题。对自己掌握的知识不自信,从而导致不敢使用这是最直接的原因。


方式四:使用@ModelAttribute(错误方式)

这里特别演示一种错误方式:使用@ModelAttribute来获取HttpServletRequest实例,形如这样:

代码语言:javascript
复制
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

代码语言:javascript
复制
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容器初始化时的这块代码:

代码语言:javascript
复制
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自己的代码非常非常简单:

代码语言:javascript
复制
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数据。

代码语言:javascript
复制
// 它是个工具类:用抽象类表示而已  所有方法均静态
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负责LocaleContextHolderRequestContextHolder,而在过滤器内部很轻松的可以拿到HttpServletRequest,所以在不继承第三方Servlet技术的情况下,此Filter几乎用不着~


FrameworkServlet

“排除”上面一种设置的机会,只剩下FrameworkServlet了。它的initContextHolders()方法和resetContextHolders()方法均会维护请求上下文:

代码语言:javascript
复制
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。先对它的关键流程步骤总结如下:

  1. 在Spring解析HttpServletRequest类型的@Autowired依赖注入时,实际注入的是个JDK动态代理对象
  2. 该代理对象的处理器是:ObjectFactoryDelegatingInvocationHandler,内部实际实例由ObjectFactory动态提供,数据由RequestContextHolder请求上下文提供,请求上下文的数据在请求达到时被赋值,参照下面步骤
  3. ObjectFactory是一个RequestObjectFactory(这是由web上下文初始化时决定的)
  4. 请求进入时,单反只要经过了FrameworkServlet处理,便会在处理时(调用Controller目标方法前)把Request相关对象设置到RequestContextHolderThreadLocal中去
  5. 这样便完成了:调用Controller目标方法前完成了Request对象和线程的绑定,所以在目标方法里,自然就可以通过当前线程把它拿出来喽,这一切都拜托的是ThreadLocal去完成的~

值得注意的是:若有不经过FrameworkServlet的请求(比如被过滤器过滤了,Spring MVC拦截器不行的哦它还是会经过FrameworkServlet处理的),但却又想这么使用,那么请主动配置RequestContextFilter这个过滤器来达到目的吧。


谨防线程池里使用HttpServletRequest的坑

源码也已经分析了,Spring的RequestContextHolder使用的InheritableThreadLocal,所以最多支持到父线程向子线程的数据传递,因此若你这么使用:

代码语言:javascript
复制
@Autowired
HttpServletRequest requestAuto;

@GetMapping("/test/request")
public Object testRequest() {
    new Thread(() -> {
        String name = requestAuto.getParameter("name");
        System.out.println(name);
    }).start();
    return "success";
}

是可以正常work的,但若你放在线程池里面执行,形如这样:

代码语言:javascript
复制
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);

@Autowired
HttpServletRequest requestAuto;
@GetMapping("/test/request")
public Object testRequest() {
    THREAD_POOL.execute(() -> {
        String name = requestAuto.getParameter("name");
        System.out.println(name);
    });
    return "success";
}

那是会出问题的,不能正常work。究其原因是@Autowire注入进来的实际使用的Request对象获取使用的是RequestContextHolder,而它最多只支持向子线程传递数据,不支持线程池

说明:只有@Autowired进来的,或者自己在线程池内手动通过RequestContextHolder获取才有问题哦,HttpServletRequest通过请求参数进来的是木有问题哒~

至于底层原因,请参考文章:ThreadLocal垮线程池传递数据解决方案:TransmittableThreadLocal


总结

该文讲述的内容虽然并不难,但我认为还是比较“时髦”的,相信能给到很多人予以帮助,那就足够了。

最后提示一小点:有人留言我说可以使用RequestContextListener这个监听器,它也能给RequestContext赋值完成绑定。答案是可以的,因为它是一个源生的Servlet请求监听器:javax.servlet.ServletRequestListener可以监听到每个请求,RequestContextListener是Spring给出的监听器实现,因此只要你在xml里配置上它/or @Bean的方式也是可行的,只是上面已经说了,绝大部分情况下并不需自己麻烦自己的这么做

分隔线
分隔线

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • 得到HttpServletRequest的三种方式
      • 方式一:方法参数
      • 方式二:从RequestContextHolder上下文获取
      • 方式三:依赖注入@Autowired
      • 方式四:使用@ModelAttribute(错误方式)
    • 依赖注入@Autowired方式是线程安全的吗?
      • @Autowired与代理对象
      • Spring何时把Request信息放进RequestContextHolder?
      • 依赖注入【确定安全】流程总结
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档