简单分析 SpringMVC 参数解析器
概述
之前在实现支持多@RequestBody的自定义注解 - 今日说码 (96xl.top)的时候了解了 SpringMVC 的参数解析器以及部分实现类,这里记录下学习过程。
参数解析器概览
HandlerMethodArgumentResolver
是参数解析器的接口,所有参数解析器都要实现该接口,它有非常多的实现类:
为了理解方便,我们可以将这些参数解析器分为四大类:
xxxMethodArgumentResolver
:这就是一个普通的参数解析器。xxxMethodProcessor
:不仅可以当作参数解析器,还可以处理对应类型的返回值。xxxAdapter
:这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。HandlerMethodArgumentResolverComposite
:这是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。
其中最重要的就是前两种。
参数解析器介绍
MapMethodProcessor
:这个用来处理 Map/ModelMap 类型的参数,解析完成后返回 model。PathVariableMethodArgumentResolver
:这个用来处理使用了@PathVariable
注解并且参数类型不为 Map 的参数,参数类型为 Map 则使用PathVariableMapMethodArgumentResolver
来处理。PathVariableMapMethodArgumentResolver
:见上。ErrorsMethodArgumentResolver
:这个用来处理 Error 参数,例如我们做参数校验时的 BindingResult。AbstractNamedValueMethodArgumentResolver
:这个用来处理 key/value 类型的参数,如请求头参数、使用了@PathVariable
注解的参数以及 Cookie 等。RequestHeaderMethodArgumentResolver
:这个用来处理使用了@RequestHeader
注解,并且参数类型不是 Map 的参数(参数类型是 Map 的使用RequestHeaderMapMethodArgumentResolver
)。RequestHeaderMapMethodArgumentResolver
:见上。RequestAttributeMethodArgumentResolver
:这个用来处理使用了@RequestAttribute
注解的参数。RequestParamMethodArgumentResolver
:这个功能就比较广了,使用了@RequestParam
注解的参数、文件上传的类型 MultipartFile、或者一些没有使用任何注解的基本类型(Long、Integer)以及 String 等,都使用该参数解析器处理。需要注意的是,如果@RequestParam
注解的参数类型是 Map,则该注解必须有 name 值,否则解析将由RequestParamMapMethodArgumentResolver
完成。RequestParamMapMethodArgumentResolver
:见上。AbstractCookieValueMethodArgumentResolver
:这个是一个父类,处理使用了@CookieValue
注解的参数。ServletCookieValueMethodArgumentResolver
:这个处理使用了@CookieValue
注解的参数。SessionAttributeMethodArgumentResolver
:这个用来处理使用了@SessionAttribute
注解的参数。MatrixVariableMethodArgumentResolver
:这个处理使用了@MatrixVariable
注解并且参数类型不是 Map 的参数,如果参数类型是 Map,则使用MatrixVariableMapMethodArgumentResolver
来处理。MatrixVariableMapMethodArgumentResolver
:见上。ExpressionValueMethodArgumentResolver
:这个用来处理使用了@Value
注解的参数。ServletResponseMethodArgumentResolver
:这个用来处理 ServletResponse、OutputStream 以及 Writer 类型的参数。ModelMethodProcessor
:这个用来处理 Model 类型参数,并返回 model。ModelAttributeMethodProcessor
:这个用来处理使用了@ModelAttribute
注解的参数。ServletModelAttributeMethodProcessor
:这个用来处理使用了@ModelAttribute
注解的参数。SessionStatusMethodArgumentResolver
:这个用来处理 SessionStatus 类型的参数。PrincipalMethodArgumentResolver
:这个用来处理 Principal 类型的参数。ContinuationHandlerMethodArgumentResolver
:参数类型为 kotlin.coroutines.Continuation 的无操作解析器。AbstractMessageConverterMethodArgumentResolver
:这是一个父类,当使用 HttpMessageConverter 解析 requestbody 类型参数时,相关的处理类都会继承自它。RequestPartMethodArgumentResolver
:这个用来处理使用了@RequestPart
注解、MultipartFile 以及 Part 类型的参数。AbstractMessageConverterMethodProcessor
:这是一个工具类,不承担参数解析任务。RequestResponseBodyMethodProcessor
:这个用来处理添加了@RequestBody
注解的参数。HttpEntityMethodProcessor
:这个用来处理 HttpEntity 和 RequestEntity 类型的参数。AbstractWebArgumentResolverAdapter
:这种不做参数解析,仅仅用来作为 WebArgumentResolver 类型的参数解析器的适配器。ServletWebArgumentResolverAdapter
:这个给父类提供 request。UriComponentsBuilderMethodArgumentResolver
:这个用来处理 UriComponentsBuilder 类型的参数。ServletRequestMethodArgumentResolver
:这个用来处理 WebRequest、ServletRequest、MultipartRequest、HttpSession、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId 类型的参数。HandlerMethodArgumentResolverComposite
:这是一个组合解析器,它是一个代理,具体代理其他干活的那些参数解析器。RedirectAttributesMethodArgumentResolver
:这个用来处理 RedirectAttributes 类型的参数。
AbstractNamedValueMethodArgumentResolver
AbstractNamedValueMethodArgumentResolver
是一个抽象类,一些键值对类型的参数解析器都是通过继承它实现的,它里边定义了很多这些键值对类型参数解析器的公共操作。其中也是应用了很多模版模式,例如它没有实现 supportsParameter()
方法,该方法的具体实现在不同的子类中,不过它实现了 resolveArgument()
方法:
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
} else {
Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
} else if (namedValueInfo.required && !nestedParameter.isOptional()) {
this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
} else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);
try {
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
} catch (ConversionNotSupportedException var11) {
throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
} catch (TypeMismatchException var12) {
throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
}
if (arg == null && namedValueInfo.defaultValue == null && namedValueInfo.required && !nestedParameter.isOptional()) {
this.handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
}
根据代码可以看到,resolveArgument()
方法的流程如下:
-
首先根据当前请求获取一个 NamedValueInfo 对象,这个对象中保存了参数的三个属性:参数名、参数是否必须以及参数默认值。具体的获取过程就是先去缓存中拿,缓存中如果有,就直接返回,缓存中如果没有,则调用
createNamedValueInfo()
方法去创建,将创建结果缓存起来并返回。createNamedValueInfo()
方法是一个模版方法,具体的实现在子类中。 -
接下来处理 Optional 类型参数。
-
resolveEmbeddedValuesAndExpressions()
方法是为了处理注解中使用了 SpEL 表达式的情况,例如如下接口:@GetMapping("/hello2") public void hello2(@RequestParam(value = "${aa.bb}") String name) { System.out.println("name = " + name); }
参数名使用了表达式,那么
resolveEmbeddedValuesAndExpressions()
方法的目的就是解析出表达式的值,如果没用到表达式,那么该方法会将原参数原封不动返回。 -
接下来调用
resolveName()
方法解析出参数的具体值,这个方法也是一个模版方法,具体的实现在子类中。 -
如果获取到的参数值为 null,先去看注解中有没有默认值,然后再去看参数值是否是必须的,如果是,则抛异常出来,否则就设置为 null 即可。
-
如果解析出来的参数值为空字符串 "",则也去
resolveEmbeddedValuesAndExpressions()
方法中走一遭。 -
最后则是 WebDataBinder 的处理,解决一些全局参数的问题。
在这个流程中,可以看到主要有两个方法是在子类中实现的,分别是 createNamedValueInfo()
方法和 resolveName()
方法,再加上 supportsParameter()
方法,子类中一共有三个方法需要实现。下面以 RequestParamMethodArgumentResolver
为例,来看下这三个方法。
RequestParamMethodArgumentResolver
supportsParameter
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (!Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
return true;
} else {
RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
return requestParam != null && StringUtils.hasText(requestParam.name());
}
} else if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
} else {
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
} else {
return this.useDefaultResolution ? BeanUtils.isSimpleProperty(parameter.getNestedParameterType()) : false;
}
}
}
从 supportsParameter()
方法中可以非常方便的看出支持的参数类型:
- 首先参数如果有
@RequestParam
注解的话,则分两种情况:参数类型如果是 Map,则@RequestParam
注解必须配置 name 属性,否则不支持;如果参数类型不是 Map,则支持。 - 参数如果含有
@RequestPart
注解,则不支持。 - 检查下是不是文件上传请求,如果是,则支持。
- 如果前面都没能返回,则使用默认的解决方案,调用
BeanUtils.isSimpleProperty()
方法判断是不是简单类型,主要就是 Void、枚举、字符串、数字、日期等等。
createNamedValueInfo
protected AbstractNamedValueMethodArgumentResolver.NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
RequestParam ann = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
return ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo();
}
获取注解,读取注解中的属性,构造 RequestParamNamedValueInfo 对象返回。
resolveName
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
Object arg;
if (servletRequest != null) {
arg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (arg != MultipartResolutionDelegate.UNRESOLVABLE) {
return arg;
}
}
arg = null;
MultipartRequest 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;
}
}
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = paramValues.length == 1 ? paramValues[0] : paramValues;
}
}
return arg;
}
根据代码可以看到,resolveName()
方法的逻辑比较简单:
- 前面两个 if 主要是为了处理文件上传请求。
- 如果不是文件上传请求,则调用
request.getParameterValues()
方法取出参数返回。