RequestParamMethodArgumentResolver参数解析过程
RequestParamMethodArgumentResolver
是最重要的几个参数解析器(HandlerMethodArgumentResolver
)之一。在实际开发中,大部分的Get请求参数都是通过它来解析的。
哪些参数会通过RequestParamMethodArgumentResolver
解析?
我们先来弄清楚一个问题,如果参数上没有加@RequestParam
注解,那这个参数的解析会交给RequestParamMethodArgumentResolver
来进行解析吗?
默认情况下RequestParamMethodArgumentResolver
只处理带有@RequestParam
注解以及MultipartFile
、Part
类型和一些简单类型参数。也就是说MultipartFile
、Part
类型和一些简单类型参数哪怕没有@RequestParam
注解,也会交给RequestParamMethodArgumentResolver
来解析。
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
}
else {
return true;
}
}
else {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
// 判断是否是简单类型
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
}
}
}
可以从org.springframework.beans.BeanUtils#isSimpleProperty
的源码中看出简单类型包括哪些。
public static boolean isSimpleProperty(Class<?> type) {
Assert.notNull(type, "'type' must not be null");
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.componentType()));
}
public static boolean isSimpleValueType(Class<?> type) {
return ClassUtils.isSimpleValueType(type);
}
public static boolean isSimpleValueType(Class<?> type) {
return (!isVoidType(type) &&
(isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
ZoneId.class.isAssignableFrom(type) ||
TimeZone.class.isAssignableFrom(type) ||
File.class.isAssignableFrom(type) ||
Path.class.isAssignableFrom(type) ||
Charset.class.isAssignableFrom(type) ||
Currency.class.isAssignableFrom(type) ||
InetAddress.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
UUID.class == type ||
Locale.class == type ||
Pattern.class == type ||
Class.class == type));
}
注:
org.springframework.util.ClassUtils#isSimpleValueType
是Spring 6.1之后的API。
因此只要我们的参数类型是上面列出来的类型,默认都是通过RequestParamMethodArgumentResolver
来解析参数的。这也解释了为什么参数是Set
或者List
这些类型时,不会通过RequestParamMethodArgumentResolver
来进行解析。
如果参数类型为字符串数组,且没有加
@RequestParam
注解,也会通过RequestParamMethodArgumentResolver
来进行解析,并且自动进行类型转换,是不会报错,且能正常解析的。只有集合类的需要加@RequestParam
注解。
为什么会有两个RequestParamMethodArgumentResolver
在HandlerMethodArgumentResolver一文的加载顺序部分,我们提到了Spring创建了两个RequestParamMethodArgumentResolver
。这是为什么呢?
这里就要注意useDefaultResolution
这个字段了。
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && StringUtils.hasText(requestParam.name()));
}
else {
return true;
}
}
else {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return false;
}
parameter = parameter.nestedIfOptional();
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
else if (this.useDefaultResolution) {
// 判断是否是简单类型
return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
}
else {
return false;
}
}
}
创建第一个实例的时候,这个字段的值是false
,也就是说第一个只会解析带有@RequestParam
注解以及MultipartFile
、Part
类型的参数。
而第二个实例useDefaultResolution
为true
。因此第二个才会处理简单类型参数。
这样做的目的是为了给用户自定义的HandlerMethodArgumentResolver
让出更多的可能性。也就是说我们可以自行扩展对一些简单类型的支持逻辑。只有当用户自定义的HandlerMethodArgumentResolver
不处理这些简单类型时,RequestParamMethodArgumentResolver
才会接管。
RequestParamMethodArgumentResolver
的解析流程
RequestParamMethodArgumentResolver
是AbstractNamedValueMethodArgumentResolver
的子类。大部分的核心代码都在AbstractNamedValueMethodArgumentResolver
中,以下类图列出了一些核心方法以及其所在的类。
RequestParamMethodArgumentResolver
解析参数的过程如下
AbstractNamedValueMethodArgumentResolver#resolveArgument
:方法为参数解析的入口方法AbstractNamedValueMethodArgumentResolver#getNamedValueInfo
:用户获取请求参数名称,及其配置,如默认值,是否必要等RequestParamMethodArgumentResolver#resolveName
:根据请求参数名称从request中获取参数值AbstractNamedValueMethodArgumentResolver#convertIfNecessary
:如果有必要,将获取到的参数值(一般为字符串或字符串数组)转换为方法参数的类型
核心处理方法org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument
的源码如下:
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 解析参数名称
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
boolean hasDefaultValue = KotlinDetector.isKotlinReflectPresent()
&& KotlinDetector.isKotlinType(parameter.getDeclaringClass())
&& KotlinDelegate.hasDefaultValue(nestedParameter);
// @ReuqestParam中配置的name可以是SPEL表达式,如果是SPEL表达式,则在此处解析为真正的请求参数名称
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 根据请求参数名称,从request中获取请求参数的值。
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
// 处理值为null和空串的情况
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
if (!hasDefaultValue) {
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
// 请求参数类型到方法参数类型的转换在这里处理
if (binderFactory != null && (arg != null || !hasDefaultValue)) {
arg = convertIfNecessary(parameter, webRequest, binderFactory, namedValueInfo, arg);
// Check for null value after conversion of incoming argument value
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
arg = convertIfNecessary(parameter, webRequest, binderFactory, namedValueInfo, arg);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest);
}
}
}
// 此方法在RequestParamMethodArgumentResolver中是空实现,只有在PathVariableMethodArgumentResolver中才有具体实现
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
- 首先通过
AbstractNamedValueMethodArgumentResolver#getNamedValueInfo
方法用于获取请求参数配置,包括参数名称、是否必要、默认值。
//org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#getNamedValueInfo
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
if (namedValueInfo == null) {
namedValueInfo = createNamedValueInfo(parameter);
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
this.namedValueInfoCache.put(parameter, namedValueInfo);
}
return namedValueInfo;
}
// org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#createNamedValueInfo
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
// 如果参数上没有@RequestParam注解,则创建一个空NamedValueInfo。
RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
}
// org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#updateNamedValueInfo
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
// 如果NamedValueInfo中的name是空的,则获取方法参数的参数名,以此作为从request中获取参数的名称依据。
String name = info.name;
if (info.name.isEmpty()) {
name = parameter.getParameterName();
if (name == null) {
throw new IllegalArgumentException("""
Name for argument of type [%s] not specified, and parameter name information not \
available via reflection. Ensure that the compiler uses the '-parameters' flag."""
.formatted(parameter.getNestedParameterType().getName()));
}
}
String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
return new NamedValueInfo(name, info.required, defaultValue);
}
- 然后通过
resolveName
方法从request
中获取到参数的值。
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
// 处理MultipartFile参数,单个文件
if (servletRequest != null) {
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
}
// 处理MultipartFile参数,文件数组
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);
}
}
// 从request中获取参数值
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
而从request中读取到的参数都是字符串,或字符串数组类型。因此如果方法参数不是此类型要进行类型转换。
其类型转换过程与ModelAttributeMethodProcessor
中类似,在此不再赘述。
扩展SpringMVC的参数解析能力
考虑这样一个功能,我们有一个地址字段,格式为:省/市/县
。前端传值为字符串。我们如何用一个地址类型(Address
)来接收这个参数呢?地址类型的字段如下:
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Address {
private String province;
private String city;
private String street;
}
我们假设格式是固定的,每个地址一定有省市县,不存在缺失一项的情况。当然现实中肯定是有这种情况的,此处只是为了说明如何对SpringMVC的参数解析能力进行扩展,因此不考虑特殊情况的处理。
首先我们定义一个StringToAddressConverter
,用于将字符串转换为Address
对象。
public class StringToAddressConverter implements Converter<String, Address> {
@Override
public Address convert(@NonNull String source) {
String[] parts = source.split("/");
if (parts.length == 3) {
return new Address(parts[0], parts[1], parts[2]);
}
return new Address();
}
}
然后修改SpringMVC配置,将其添加到ConversionService
中。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToAddressConverter());
}
}
控制层在使用时,直接在要转为地址类型的参数前增加@RequestParam
注解即可。
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/address")
public String address(@RequestParam Address address) {
System.out.println(address);
return "OK";
}
}
然后启动服务,发送请求
GET http://localhost:8080/test/address?address=河南省/郑州市/高新区
控制台输出
Address(province=河南省, city=郑州市, street=高新区)
可以看到已经可以正常解析了。这种解析方式是通过RequestParamMethodArgumentResolver
来解析的。
如果不加@RequestParam
也是可以正常解析的。 这时候则是通过ModelAttributeMethodProcessor
来进行解析的。