Spring MVC : ServletRequest 注入原理分析 2 - 为什么可以正确访问?

概述

Spring应用中,将一个较大作用域的bean注入到一个较小作用域的bean中是很自然的一件事情,因为往较小作用域bean中注入较大作用域bean时,较大作用域的bean已经存在了。但是,如果反过来,将一个较小作用域的bean往一个较大作用域的bean中注入时,较小作用域的bean可能根本不存在,比如Spring MVC应用中,如果要将一个ServletRequest注入到一个单例作用域的Web控制器组件中,则该注入动作会在该单例Web控制器组件初始化时发生(容器对单例bean的创建过程决定的),而通常此时容器正在启动,任何用户请求尚未到达,所以注入任何一个ServletRequest听起来都是不正确的。以此类推,将一个较小作用域的bean往一个较大作用域的bean中注入概念上讲是不可行的。

但通过实践,我们明确知道,在一个Spring MVC应用中,将一个ServletRequest注入到一个单例作用域的Web控制器中是可以的。这又是为什么呢 ?这里面,我们有两个方面的问题要解答 :

  1. 注入的bean是什么?为什么ServletRequest能够注入成功 ?
  2. 注入的bean如何工作的?为什么请求过程中访问注入的ServletRequest能访问到正确的信息?

本文我们将通过分析回答上面的第二个问题。

本文所使用的例子如下 :

@Controller
public class HomeController {

    ServletRequest request;

    @Autowired
    public void setRequest(ServletRequest request){
        this.request=request;
    }

    @RequestMapping("/")
    @ResponseBody
    public String home() {
       return "Hello "+ request.getParameter("name");
    }
}

工作原理分析

1. 观察注入的ServletRequest对象

我们在上面例子中行

this.request=request;

设置一个断点,然后启动程序。我们发现,bean HomeController创建时,request属性被注入了如下对象 :

request = {$Proxy54@5166} "Current HttpServletRequest"
 h = {AutowireUtils$ObjectFactoryDelegatingInvocationHandler@5174} 
  objectFactory = {WebApplicationContextUtils$RequestObjectFactory@5175} "Current HttpServletRequest"

从这些信息不难看出,request是一个Java动态代理对象,该动态代理对象的InvocationHandler实现类是AutowireUtils$ObjectFactoryDelegatingInvocationHandler。我们先来看看这个实现类。

1.1 AutowireUtils$ObjectFactoryDelegatingInvocationHandler

ObjectFactoryDelegatingInvocationHandlerAutowireUtils的静态嵌套类,实现如下 :

	private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {

       // 这是一个对象工厂,该对象工厂用于获取目标调用方法的所属对象
		private final ObjectFactory<?> objectFactory;

       // 构造函数,参数objectFactory表示一个对象工厂,目标调用方法的所属对象将会从该对象工厂中获取
		public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
			this.objectFactory = objectFactory;
		}

       //  接口 InvocationHandler 所约定的 invoke 方法,对Java动态代理对象的调用会调用该方法,
       // 然后该方法又会从objectFactory中获取目标对象,然后将最终的方法调用应用在目标对象上。
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			String methodName = method.getName();
			if (methodName.equals("equals")) {
				// Only consider equal when proxies are identical.
				return (proxy == args[0]);
			}
			else if (methodName.equals("hashCode")) {
				// Use hashCode of proxy.
				return System.identityHashCode(proxy);
			}
			else if (methodName.equals("toString")) {
				return this.objectFactory.toString();
			}
			try {
				return method.invoke(this.objectFactory.getObject(), args);
			}
			catch (InvocationTargetException ex) {
				throw ex.getTargetException();
			}
		}
	}

从该类的实现我们可以看到,代理对象request一旦生成,其属性objectFactory对象就确定且不再更改了,但是该代理对象的每次调用,却作用在了objectFactory.getObject()返回的对象上。我们知道,getObject()方法其实是一段逻辑,而一段逻辑是可以每次调用时返回不同的对象的。那么我们可以试想,每次用户请求处理时,我们对预先注入的request代理对象的方法调用,比如上面例子中的getParameter("name"),之所以能够应用到正确的用户请求上,会不会就是这里getObject()每次调用时返回了正确的用户请求对象呢 ?

从上面我们调试观察到的request代理对象的内存结构我们可以看到,它的objectFactory实现类是WebApplicationContextUtils$RequestObjectFactory。我们接下来分析一个这个类。

1.2 WebApplicationContextUtils$RequestObjectFactory

RequestObjectFactoryWebApplicationContextUtils的静态嵌套类,实现如下 :

	/**
	 * Factory that exposes the current request object on demand.
     * 按需暴露当前请求对象的工厂类
	 */
	@SuppressWarnings("serial")
	private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {

		@Override
		public ServletRequest getObject() {
			return currentRequestAttributes().getRequest();
		}

		@Override
		public String toString() {
			return "Current HttpServletRequest";
		}
	}

RequestObjectFactory的实现很简单,它是一个对象工厂,用于从currentRequestAttributes()中获取一个请求对象ServletRequest交给调用者。我们再来看一下currentRequestAttributes() 的都做了些什么。

	/**
	 * Return the current RequestAttributes instance as ServletRequestAttributes.
	 * @see RequestContextHolder#currentRequestAttributes()
	 */
	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()返回一个ServletRequestAttributes对象。它具体是什么,我们就得看RequestContextHolder的实现了。

1.3 RequestContextHolder

package org.springframework.web.context.request;


public abstract class RequestContextHolder  {

	private static final boolean jsfPresent =
			ClassUtils.isPresent("javax.faces.context.FacesContext", 
			RequestContextHolder.class.getClassLoader());

    // 在当前线程中保存请求属性,适用于指定不要跨线程继承请求属性的情形
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");

     // 在当前线程中保存请求属性,适用于指定需要跨线程继承请求属性的情形
	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");


	/**
	 * Reset the RequestAttributes for the current thread.
     * 清除当前线程中保存的请求属性,
     * 对 requestAttributesHolder 和 inheritableRequestAttributesHolder 都会执行删除动作
	 */
	public static void resetRequestAttributes() {
		requestAttributesHolder.remove();
		inheritableRequestAttributesHolder.remove();
	}

	/**
	 * Bind the given RequestAttributes to the current thread,
	 * not exposing it as inheritable for child threads.
     * 记录请求属性到当前线程,假定当前情形是不要跨线程继承请求属性的情形
	 * @param attributes the RequestAttributes to expose
	 * @see #setRequestAttributes(RequestAttributes, boolean)
	 */
	public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
		setRequestAttributes(attributes, false);
	}

	/**
	 * Bind the given RequestAttributes to the current thread.
     * 记录请求属性到当前线程,参数 inheritable 指定是何种情形 :
     * true - 需要跨线程继承请求属性的情形 => 信息记录到  inheritableRequestAttributesHolder
     * false - 不要跨线程继承请求属性的情形 => 信息记录到 requestAttributesHolder
     * 如果参数 attributes 为 null,则从当前线程中清除所记录的请求属性
	 * @param attributes the RequestAttributes to expose,
	 * or  null to reset the thread-bound context
	 * @param inheritable whether to expose the RequestAttributes as inheritable
	 * for child threads (using an  InheritableThreadLocal)
	 */
	public static void setRequestAttributes(@Nullable RequestAttributes attributes, 
		boolean inheritable) {
		if (attributes == null) {
			resetRequestAttributes();
		}
		else {
			if (inheritable) {
				inheritableRequestAttributesHolder.set(attributes);
				requestAttributesHolder.remove();
			}
			else {
				requestAttributesHolder.set(attributes);
				inheritableRequestAttributesHolder.remove();
			}
		}
	}

	/**
	 * Return the RequestAttributes currently bound to the thread.
     * 获取当前线程中绑定的请求属性
	 * @return the RequestAttributes currently bound to the thread,
	 * or  null if none bound
	 */
	@Nullable
	public static RequestAttributes getRequestAttributes() {
		RequestAttributes attributes = requestAttributesHolder.get();
		if (attributes == null) {
			attributes = inheritableRequestAttributesHolder.get();
		}
		return attributes;
	}

	/**
	 * Return the RequestAttributes currently bound to the thread.
	 * Exposes the previously bound RequestAttributes instance, if any.
	 * Falls back to the current JSF FacesContext, if any.
     * 获取当前线程中绑定的请求属性,对 JSF 也提供了支持,如果当前线程中没有
     * 绑定请求属性,则抛出异常 IllegalStateException
	 * @return the RequestAttributes currently bound to the thread
	 * @throws IllegalStateException if no RequestAttributes object
	 * is bound to the current thread
	 * @see #setRequestAttributes
	 * @see ServletRequestAttributes
	 * @see FacesRequestAttributes
	 * @see javax.faces.context.FacesContext#getCurrentInstance()
	 */
	public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
		RequestAttributes attributes = getRequestAttributes();
		if (attributes == null) {
			if (jsfPresent) {
				attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
			}
			if (attributes == null) {
				throw new IllegalStateException("No thread-bound request found: " +
	"Are you referring to request attributes outside of an actual web request, " +
	"or processing a request outside of the originally receiving thread? " +
	"If you are actually operating within a web request and still receive this message, " +
	"your code is probably running outside of DispatcherServlet: " +
	"In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
			}
		}
		return attributes;
	}


	/**
	 * Inner class to avoid hard-coded JSF dependency.
 	 */
	private static class FacesRequestAttributesFactory {

		@Nullable
		public static RequestAttributes getFacesRequestAttributes() {
			FacesContext facesContext = FacesContext.getCurrentInstance();
			return (facesContext != null ? new FacesRequestAttributes(facesContext) : null);
		}
	}

}

从上面的代码实现不难看出,RequestContextHolder是一个静态工具类,它的作用主要在当前线程中保存请求属性 :

  1. 供使用者使用RequestContextHolder.setRequestAttributes将请求属性记录到当前线程,
  2. 这样即使逻辑执行到了其他地方,使用者仍然可以通过RequestContextHolder.currentRequestAttributes获取当前请求属性。

结合上面的分析,我们可以看出,bean HomeController属性request代理对象最终作用在了从RequestContextHolder中获取到的请求对象上。进一步讲,如果每次调用request代理对象的方法时,如果RequestContextHolder在当前线程中所记录的请求对象是正确的,那么通过bean HomeController属性request代理对象就能访问到正确的请求数据。

那么,RequestContextHolder有没有被正确设置呢 ?

我们在RequestContextHolder方法setRequestAttributes上增加一个断点,可以观察到,在Spring MVC的前端控制器DispatcherServlet处理该请求的主逻辑开始前,它被调用了,具体的代码如下 :

// DispatcherServlet 基类 FrameworkServlet
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
            // ...
            
            // 构建当前请求属性对象 requestAttributes
            RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes requestAttributes = buildRequestAttributes(request, 
            	response, previousAttributes);

            // ... 
            
            // 
            initContextHolders(request, ..., requestAttributes);
		
		
            // ...			
            
            // 请求处理主逻辑
            doService(request, response);
            
            // ...
  }

	private void initContextHolders(HttpServletRequest request,
			@Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {

		 // ...
         
		//  记录当前请求对象到RequestContextHolder,也就是当前线程
		if (requestAttributes != null) {
			RequestContextHolder.setRequestAttributes(requestAttributes, 
				this.threadContextInheritable);
		}
	}

到此,一切就很清楚了:每个请求到达后,被处理前,请求属性会被记录到当前线程,也就是RequestContextHolder所做的事情,这样随后在整个请求处理过程的任何地方,当然也包括上面bean HomeController的任何控制器方法,比如home()中,访问代理对象request时,真正访问到的就会是RequestContextHolder中保存的当前请求对象了。

总结

基于上述分析,我们可以总结出使bean HomeController的注入属性ServletRequest request可以正确工作的关键点 :

  1. 注入属性ServletRequest request是一个代理对象,它所代理的真正请求对象是RequestContextHolder请求上下文持有器中的请求对象;
  2. RequestContextHolder用于在当前线程中保持请求属性;
  3. Spring MVC前端控制器DispatcherServlet每处理一个请求前,先将请求属性记录在RequestContextHolder中。

相关文章

Spring MVC : ServletRequest 注入原理分析
Spring MVC : ServletRequest 注入原理分析 1 - 为什么可以注入成功?

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值