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

我的梦境 提交于 2020-03-01 01:23:10

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

前言

我们知道一个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负责LocaleContextHolderRequestContextHolder,而在过滤器内部很轻松的可以拿到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。先对它的关键流程步骤总结如下:

  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这个过滤器来达到目的吧。


总结

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

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

分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!