Spring MVC request 获取方式大总结

前言

普通的 Java Web 项目中,我们经常使用 HttpServletRequest 获取请求参数,请求头等信息。

到了 Spring MVC 项目,我们通常会使用 Spring 提供的注解获取参数,如 @RequestParam、@RequestHeader。

不过在某些场景下,我们可能还是想获取 HttpServletRequest 对象,如获取请求 IP,获取请求域名等。这篇我们来学习如何在 Spring MVC 环境下获取 HttpServletRequest,以及它们的实现方式,以做到知其所以然。

Controller 方法参数

快速上手

使用注解后的 Spring MVC,controller 方法可以作为 handler 处理请求,如果想获取 request 对象,只需要在方法中添加 ServletRequest 或 HttpServletRequest 类型参数即可。示例代码如下。

@RestController
public class TestController {

    @GetMapping("/test")
    public String test(HttpServletRequest request) {
        return "request ip is : " + request.getRemoteHost();
    }

}

原理分析

是不是很简单?不过哪有什么岁月静好,不过是有人替你负重前行,Spring 在背后为此也做了很多工作,这里我们简单做一些分析。

再看下 DispatchServlet 处理请求流程
在这里插入图片描述DispatchServlet 处理请求时先根据 HandlerMapping 查找 handler,RequestMappingHandlerMapping 会根据 controller 方法上的 @RequestMapping 注解查找合适的 controller 方法作为 handler,并表示为 HandlerMethod,经不同的 HandlerAdapter 对 handler 进行适配后开始处理请求并生成视图。

HandlerMethod 处理请求时自然需要调用我们自定义的 controller 方法,那么不可避免就需要提供参数值,Spring 为了根据请求信息解析出 controller 方法参数抽象出了 HandlerMethodArgumentResolver 接口,例如我们这篇要讲的 ServletRequest 实现就是 ServletRequestMethodArgumentResolver。核心代码如下。

public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Override
	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
								  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		Class<?> paramType = parameter.getParameterType();

	    ...省略部分代码
	    
		// 解析 ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
		if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
			return resolveNativeRequest(webRequest, paramType);
		}

		// HttpServletRequest required for all further argument types
		return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
	}
	
}

这里解析 ServletRequest 使用的 NativeWebRequest 则是从 DispatcherServlet 到 HandlerAdapter ,再到 HandlerMethod ,最后到当前方法的参数不断传递的,不再进行分析。

适用场景

利用 controller 方法获取 HttpServletRequest 参数,如果调用链比较长,如 A->B->C->D->E,后面的方法需要使用 HttpServletRequest 参数的话,那么参数需要从 controller 中依次传递。

这将导致代码中到处充斥着这个参数,因此仅适用于调用链不太长的场景,例如直接在 controller 方法中使用或者在 service 中使用。

静态方法

快速上手

除了通过 controller 方法参数获取 HttpServletRequest 对象,Spring 还允许通过其提供的工具类的静态方法来获取 HttpServletRequest。示例如下。

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

原理分析

静态方法获取 request 的方式也很简单。上述的示例中,RequestContextHolder 表示一个请求上下文的持有者,内部将请求上下文信息存储到 ThreadLocal 中。代码如下。

public abstract class RequestContextHolder {

	/**
	 * 线程上下文 RequestAttributes
	 */
	private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");

	/**
	 * 支持继承的线程上下文 RequestAttributes
	 */
	private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");

}

请求上下文使用 RequestAttributes 表示,DispatcherServlet 处理请求前会将 request 存至 ServletRequestAttributes,然后放到 RequestContextHolder 中。代码如下。

public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {

	// 处理请求
	protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {

		... 省略部分代码
		
		RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

		... 省略部分代码

		initContextHolders(request, localeContext, requestAttributes);

		try {
			doService(request, response);
		... 省略部分代码
	}

	// 上下文信息存储至 RequestContextHolder
	private void initContextHolders(HttpServletRequest request,
									@Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
		... 省略部分代码
		if (requestAttributes != null) {
			RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
		}
	}
	
}

等包含 request 的上下文信息存至 RequestContextHolder 之后,我们的代码就可以从这个上下文持有者获取 request 了。

适用场景

静态方法相比 controller 方法参数来说,更为灵活,不管调用链有多深都可以获取 request。其缺点在于 API 由 Spring 提供,因此增加了学习使用的成本。如果一定要使用的话,在 Spring 的基础上再次包装一层,提供一个工具类也是一个不错的选择。

直接注入

快速上手

Spring MVC 环境下,还可以将 HttpServletRequest 当做普通的 bean 注入。代码如下。

@RestController
public class TestController {

    @Autowired
    private HttpServletRequest request;

    @GetMapping("/test")
    public String test() {
        return "request ip is : " + request.getRemoteHost();
    }

}

原理分析

通过 @Autowired 的方式引入 request 也很简单。等等,controller 不是一个单例 bean 么?在一个 Spring 容器内只有一个实例,而每次请求都对应一个 request 对象,Spring 是怎样做到使用一个 request 表示多个请求的?

经过仔细分析,我们可以发现 Spring 注入 bean 时使用了底层的 DefaultListableBeanFactory 获取 bean 实例,相关代码如下。

public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
		implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
	// 游离对象
	private final Map<Class<?>, Object> resolvableDependencies = new ConcurrentHashMap<>(16);
	
	// 查找候选 bean
	protected Map<String, Object> findAutowireCandidates(
			@Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {	
		... 省略部分代码
		Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
		
		for (Map.Entry<Class<?>, Object> classObjectEntry : this.resolvableDependencies.entrySet()) {
			Class<?> autowiringType = classObjectEntry.getKey();
			if (autowiringType.isAssignableFrom(requiredType)) {
				Object autowiringValue = classObjectEntry.getValue();
				// 解析 ObjectFactory
				autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
				if (requiredType.isInstance(autowiringValue)) {
					result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
					break;
				}
			}
		}
		... 省略部分代码
    }
}

DefaultListableBeanFactory 查找候选 bean 时会先从保存游离对象的 resolvableDependencies 中查找,找到后调用 AutowireUtils.resolveAutowiringValue方法再次解析。

游离对象是 Spring 中特殊的存在,不属于 Spring 管理的 bean,需要手动注册到 DefaultListableBeanFactory。这个静态方法是实现 request 注入的核心,我们继续跟踪源码。

abstract class AutowireUtils {

	public static Object resolveAutowiringValue(Object autowiringValue, Class<?> requiredType) {
		if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) {
			// ObjectFactory 类型值和所需类型不匹配,创建代理对象
			ObjectFactory<?> factory = (ObjectFactory<?>) autowiringValue;
			if (autowiringValue instanceof Serializable && requiredType.isInterface()) {
				// 创建代理对象,可用于处理 HttpServletRequest 注入等问题
				autowiringValue = Proxy.newProxyInstance(requiredType.getClassLoader(),
						new Class<?>[]{requiredType}, new ObjectFactoryDelegatingInvocationHandler(factory));
			} else {
				return factory.getObject();
			}
		}
		return autowiringValue;
	}

}

如果游离对象是 ObjectFactory 类型,并且与所需的类型不匹配,Spring 使用 ObjectFactory 创建了一个 JDK 代理,看代理是如何实现的。

	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 {
			... 省略部分代码
			try {
				return method.invoke(this.objectFactory.getObject(), args);
			} catch (InvocationTargetException ex) {
				throw ex.getTargetException();
			}
		}
	}

代理的实现也很简单,每当所需类型的方法调用时,就调用 ObjectFactory 中获取的实例对象的对应方法。

到了这里好像和我们分析的 request 对象获取也并没有什么关系。不过想想,如果我们将获取 HttpServletRequest 的 ObjectFactory 注册为游离对象,等我们注入的 HttpServletRequest 对象方法调用时,让代理对象调用 ObjectFactory 获取的正确的 HttpServletRequest 方法是不是就可以了,而 HttpServletRequest 已经存至上下文中,因此可以正确获取。

Spring 也确实是这样做的,Spring 在上下文启动时会注册 Web 环境相关的游离对象。

public abstract class WebApplicationContextUtils {

	public static void registerWebApplicationScopes(ConfigurableListableBeanFactory beanFactory,
													@Nullable ServletContext sc) {

		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);
			// Register as ServletContext attribute, for ContextCleanupListener to detect it.
			sc.setAttribute(ServletContextScope.class.getName(), appScope);
		}
		// ServletRequest 类型对应 ObjectFactory 注册
		beanFactory.registerResolvableDependency(ServletRequest.class, new RequestObjectFactory());
		beanFactory.registerResolvableDependency(ServletResponse.class, new ResponseObjectFactory());
		beanFactory.registerResolvableDependency(HttpSession.class, new SessionObjectFactory());
		beanFactory.registerResolvableDependency(WebRequest.class, new WebRequestObjectFactory());
		if (jsfPresent) {
			FacesDependencyRegistrar.registerFacesDependencies(beanFactory);
		}
	}
	
}

这里 Spring 为 ServletRequest 注入的是 RequestObjectFactory 类型,看看这个类型的实现。

public abstract class WebApplicationContextUtils {

	private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {

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

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

实现是不是很简单,直接获取了上下文的 request。

这段逻辑相对复杂,总结如下。

  1. Spring 容器启动时为 ServletRequest 注册 RequestObjectFactory 类型的游离对象。
  2. Spring 为 @Autowired HttpServletRequest 注入的是一个代理对象。
  3. HttpServletRequest 代理对象的方法执行时,底层调用通过 RequestObjectFactory 获取的线程上下文存储的真实 HttpServletRequest 的方法。

使用场景

通过 @Autorired 的方式引入 HttpServletRequest,可以直接在 bean 中注册,解决了 controller 方法无法解决调用链过长的问题,不过如果在非 bean 中获取,可能还需要使用静态方法的方式获取 request。

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大鹏cool

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值