Spring注入的成员属性HttpServletRequest是线程安全的吗?

Spring注入的成员属性HttpServletRequest是线程安全的吗?

前言

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

谨防线程池里使用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(() -> {
        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的方式也是可行的,只是上面已经说了, 绝大部分情况下并不需自己麻烦自己的这么做


原文:https://cloud.tencent.com/developer/article/1600755

太棒了。学到许多