跳到主要内容

RequestParamMethodArgumentResolver参数解析过程

RequestParamMethodArgumentResolver是最重要的几个参数解析器(HandlerMethodArgumentResolver)之一。在实际开发中,大部分的Get请求参数都是通过它来解析的。

哪些参数会通过RequestParamMethodArgumentResolver解析?

我们先来弄清楚一个问题,如果参数上没有加@RequestParam注解,那这个参数的解析会交给RequestParamMethodArgumentResolver来进行解析吗?

默认情况下RequestParamMethodArgumentResolver只处理带有@RequestParam注解以及MultipartFilePart类型和一些简单类型参数。也就是说MultipartFilePart类型和一些简单类型参数哪怕没有@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;
}
}
}
java

可以从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);
}
java
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));
}
java

注: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;
}
}
}
java

创建第一个实例的时候,这个字段的值是false,也就是说第一个只会解析带有@RequestParam注解以及MultipartFilePart类型的参数。

而第二个实例useDefaultResolutiontrue。因此第二个才会处理简单类型参数。

这样做的目的是为了给用户自定义的HandlerMethodArgumentResolver让出更多的可能性。也就是说我们可以自行扩展对一些简单类型的支持逻辑。只有当用户自定义的HandlerMethodArgumentResolver不处理这些简单类型时,RequestParamMethodArgumentResolver才会接管。

RequestParamMethodArgumentResolver的解析流程

RequestParamMethodArgumentResolverAbstractNamedValueMethodArgumentResolver的子类。大部分的核心代码都在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;
}
java
  • 首先通过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);
}
java
  • 然后通过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;
}
java

而从request中读取到的参数都是字符串,或字符串数组类型。因此如果方法参数不是此类型要进行类型转换。

其类型转换过程与ModelAttributeMethodProcessor中类似,在此不再赘述。

扩展SpringMVC的参数解析能力

考虑这样一个功能,我们有一个地址字段,格式为:省/市/县。前端传值为字符串。我们如何用一个地址类型(Address)来接收这个参数呢?地址类型的字段如下:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Address {
private String province;
private String city;
private String street;
}
java

我们假设格式是固定的,每个地址一定有省市县,不存在缺失一项的情况。当然现实中肯定是有这种情况的,此处只是为了说明如何对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();
}
}

java

然后修改SpringMVC配置,将其添加到ConversionService中。

@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToAddressConverter());
}

}
java

控制层在使用时,直接在要转为地址类型的参数前增加@RequestParam注解即可。

@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("/address")
public String address(@RequestParam Address address) {
System.out.println(address);
return "OK";
}
}
java

然后启动服务,发送请求

GET http://localhost:8080/test/address?address=河南省/郑州市/高新区

控制台输出

Address(province=河南省, city=郑州市, street=高新区)

可以看到已经可以正常解析了。这种解析方式是通过RequestParamMethodArgumentResolver来解析的。

如果不加@RequestParam也是可以正常解析的。 这时候则是通过ModelAttributeMethodProcessor来进行解析的。