赞
踩
被@RequestMapping
注解的方法就是一个请求处理器 handler,Spring MVC 会把该方法封装成 HandlerMethod 对象。HTTP 请求经过 RequestMappingInfo 条件匹配后最终路由到目标 HandlerMethod,接下来就是对目标方法的调用了。
调用方法你得有参数吧,所以 Spring MVC 会先依赖 HandlerMethodArgumentResolverComposite 组件解析参数列表,再反射调用目标方法,本文重点参数的解析。
DispatcherServlet 作为一个 Servlet,它能拿到的只有 HttpServletRequest 和 HttpServletResponse。如何把 HttpServletRequest 里的参数或属性转换成 Java 方法所需的参数列表,这本身是一件非常复杂的事情。
Spring MVC 专门抽象了一个策略接口 HandlerMethodArgumentResolver 来解析目标方法调用所需的参数,内置了很多实现类,每个实现类对应一种 Controller 方法的参数类型。
你不需要去看所有的实现类,等实际用到了再去研究也不迟,这里列举几个常用的实现类:
@RequestParam
注解的参数,从请求的 Parameters 里面获取参数@RequestHeader
注解的参数,从请求头里获取参数@RequestAttribute
注解的参数,从请求属性里获取参数@SessionAttribute
注解的参数,从 Session 里获取参数@CookieValue
注解的参数,从 Cookie 里获取参数@RequestBody
注解的参数,从请求体里面获取参数Spring MVC 调用目标方法时,使用 HandlerMethodArgumentResolverComposite 实现类来解析参数。
它内部维护了一组解析器,在RequestMappingHandlerAdapter#afterPropertiesSet
处被初始化。
private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
解析参数时,首先找到能支持给定参数类型的解析器,找不到就只能抛异常。
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
// 遍历解析器,找到能支持参数的解析器并缓存
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
再把 MethodParameter 交给找到的解析器解析:
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 查找支持参数类型的解析器 遍历列表,找到就返回
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
我们重点要看的是其它实现类!
篇幅原因,没必要把所有的解析器都分析一遍,这里挑选三个常用的解析器。
解析@RequestParam
注解的参数,从请求的 Parameters 里面获取参数。
它继承自 AbstractNamedValueMethodArgumentResolver,顾名思义,这一类解析器首先得确定参数的名称 name,才能从 Request 里去拿参数。例如:从 Cookie、Request#Parameters、Request#Attributes、Request#Headers 等等,都得先确定名称才好拿到参数。
解析参数时,父类实现了一些通用的逻辑:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { /** * 获得参数的名称、是否必须、以及默认值 * 对于参数的名称,如果参数加了指定注解,则取注解指定的名称 * 否则,使用ParameterNameDiscoverer组件解析参数名称 * 方法参数的真实名称解析是个麻烦的过程,Java反射获取有一定的限制,需要用到ASM技术 */ NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); // 解析占位符/表达式 Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); if (resolvedName == null) { throw new IllegalArgumentException( "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); } // 解析参数值(原始未经过转换的) Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); if (arg == null) { // 处理默认值 if (namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } else if (namedValueInfo.required && !nestedParameter.isOptional()) { // 抛出参数丢失异常 handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } // 处理空值 对于基本类型会抛出异常 arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } /** * 解析出来的原始参数 还要经过WebDataBinder转换成我们想要的Java对象 * 比如参数date=2023-01-01 我们想转换成LocalDate */ if (binderFactory != null) { // 创建数据绑定器 WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { // 数据转换 arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } catch (TypeMismatchException ex) { throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } // Check for null value after conversion of incoming argument value if (arg == null && namedValueInfo.defaultValue == null && namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest); } } // 留给子类扩展 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; }
这里要强调一下,解析方法的参数名不是一件容易的事。如果你给参数加上了注解,在注解里指定了 name,这是一个很好的习惯,Spring MVC 会直接拿注解里给定值作为 name。如果你没有加注解或没有在注解里指定 name,你会发现 Spring MVC 也可以正确的绑定参数,但是背后的处理逻辑就会复杂很多。你可能会说,有什么复杂的?直接反射获取方法的参数名不就好了吗?反射确实可以获取到参数名,但是有很大的限制。首先,你的 JDK 版本必须是 8 及以上,且编译时加上-parameters
参数才能让 javac 编译类时保留参数名称,否则反射获取到的参数名是 arg0、arg1 这样无意义的名称,因为对于 JVM 运行时来说,方法的参数名的确没有作用。
getNamedValueInfo()
会获取参数的名称,你可以看一下它的处理流程:
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
if (namedValueInfo == null) {
// 如果加了注解且指定了参数名,直接注解参数名
namedValueInfo = createNamedValueInfo(parameter);
// 否则通过ParameterNameDiscoverer解析参数名称
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
// 缓存起来,解析代价较高
this.namedValueInfoCache.put(parameter, namedValueInfo);
}
return namedValueInfo;
}
在updateNamedValueInfo()
里面,如果没有通过注解拿到参数名,Spring MVC 就必须尝试自己解析参数名了,这里又会依赖 ParameterNameDiscoverer 组件。
public String getParameterName() { if (this.parameterIndex < 0) { return null; } ParameterNameDiscoverer discoverer = this.parameterNameDiscoverer; if (discoverer != null) { String[] parameterNames = null; if (this.executable instanceof Method) { parameterNames = discoverer.getParameterNames((Method) this.executable); } else if (this.executable instanceof Constructor) { parameterNames = discoverer.getParameterNames((Constructor<?>) this.executable); } if (parameterNames != null) { this.parameterName = parameterNames[this.parameterIndex]; } this.parameterNameDiscoverer = null; } return this.parameterName; }
总之,确认参数名称以后,子类要做的,就是实现自己取参数的逻辑了。对于 RequestParamMethodArgumentResolver,自然是从 Request#Parameters 里面取了,不过你要注意的是,除了普通参数,还有文件上传需要处理哦。
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); if (servletRequest != null) { // 解析文件上传参数 MultipartFile Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { return mpArg; } } Object arg = null; MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class); if (multipartRequest != null) { List<MultipartFile> files = multipartRequest.getFiles(name); if (!files.isEmpty()) { arg = (files.size() == 1 ? files.get(0) : files); } } // 普通参数,直接从ServletRequest.getParameterValues取 if (arg == null) { String[] paramValues = request.getParameterValues(name); if (paramValues != null) { arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } return arg; }
至此,我们就拿到参数了。但是现在参数还是 String 类型的,Spring MVC 不能直接拿这个参数去调用目标方法,因为参数类型可能并不匹配。例如:请求参数date=2023-01-01
,Controller 方法里的参数类型是 LocalDate,这就需要做类型的转换了。Spring MVC 把这个职责交给了 WebDataBinder,它是数据绑定器,它会把 Request 里的参数绑定到我们想要的目标对象上,这里会调用DataBinder#convertIfNecessary
做参数类型的转换。
到这里,@RequestParam
参数的解析就完成了。为了便于理解,我画了一张流程图:
看完 RequestParamMethodArgumentResolver,再看@RequestHeader
参数解析就很简单了。
RequestHeaderMethodArgumentResolver 也是需要确定参数名的,所以它也继承了 AbstractNamedValueMethodArgumentResolver,整体流程都一样,区别是它是从请求头里拿参数:
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
String[] headerValues = request.getHeaderValues(name);
if (headerValues != null) {
return (headerValues.length == 1 ? headerValues[0] : headerValues);
} else {
return null;
}
}
同样的,拿到的参数值依然是 String,也需要经过 WebDataBinder 的类型转换,这些都是在父类中统一实现的。
最后再看一个十分常用的,@RequestBody
参数的解析器。它把请求体作为参数,所以不需要知道参数名,也就没继承 AbstractNamedValueMethodArgumentResolver。
它还实现了 HandlerMethodReturnValueHandler 接口,所以它不止能解析参数,还能处理返回值,因为本文只分析参数的解析,所以先跳过返回值的处理。
它是怎么把请求体转换成 Controller 方法参数的呢?
首先我们得知道,HTTP 请求体的媒体类型 Content-Type 可以有多种类型,它可以是 JSON、Xml、表单、甚至是二进制数据。而被@RequestBody
注解的参数,我们也可以用多种不同的类来接收,比如可以是 Object、Map、String、甚至是你自己定义的 POJO。
这就涉及到更加复杂的数据转换过程了,Spring MVC 通过一个叫 HttpMessageConverter 的组件,来完成这个复杂的转换过程。
解析器会把参数交给readWithMessageConverters()
方法解析,自己只处理解析后的数据校验工作:
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); /** * 通过HttpMessageConverter转换读取请求体 * 要根据请求的Content-Type和参数类型交给HttpMessageConverter解析 */ Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { // 数据校验 validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } return adaptArgumentIfNecessary(arg, parameter); }
先把 HttpServletRequest 封装成 ServletServerHttpRequest,方便读取请求头和请求体,再调用重载方法解析参数:
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
// Request封装 方便读取请求头和请求体
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
// 解析参数
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
数据类型的转换,由请求头 Content-Type 和参数类型共同决定。例如:当 Content-Type 是“application/json”时,参数类型可以是 Map,因为转换器知道如何把 JSON 转换成 Map;反之,如果 Content-Type 是“text/plain”,参数类型就只能是 String 了,如果声明 Map 就会报错,因为转换器不知道怎么把一段文本转换成 Map,即使它可能是个正确的 JSON。
数据的转换依赖 HttpMessageConverter 组件,这里先不细说,总之 Spring MVC 会先调用canRead()
判断当前转换器是否支持读取给定的参数,如果支持紧接着就会调用其read()
把请求体转换成我们期望的最终对象。
至此,@RequestBody
参数也就解析完成了。
RequestMappingHandlerAdapter 要想调用目标方法处理请求,首先得先解析出正确的参数。如何从 HttpServletRequest 解析出 Java 方法需要的参数列表,是个非常复杂的过程,方法的调用反而简单,通过反射就可以。
Spring MVC 的 Controller 方法支持很多种参数类型,通过注解就可以很简单的读取 Request 参数值、属性值、请求头的值、Cookie、Session 值等等,功能很丰富,如何解析这些参数就成了麻烦事。Spring MVC 为此专门抽象出了 HandlerMethodArgumentResolver 策略接口,不同类型的参数设计不同的实现类去解析,最后通过一个叫 HandlerMethodArgumentResolverComposite 的类把这些解析器聚集到一起,最终实现了这一套复杂的参数解析逻辑。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。