开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
前言
@RequestParam注解是SpringMVC提供的注解之一,用于将请求参数绑定到后端服务的变量上.
问题
今天不讨论@RequestParam的用法,而是分析遇到的一个问题:
controller如下:
@GetMapping("test")
public List<String> exportTest(List<String> list) {
return list;
}
如上controller,在使用postman测试时,发生错误,错误如下:
而在给该方法的参数加上@RequestParam注解后,就可以正常绑定参数,这是为什么呢?
而平时的方法里也没有加入@RequestParam参数,但是仍然会自动绑定到对应变量上,这又是为什么?
今天走进源码剖析一下具体的原因:
源码
SpringMVC的入口源码不再详述,直接进入参数解析源码的入口:
不加@RequestParam
InvocableHandlerMethod#getMethodArgumentValues
参数解析入口:
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 获取方法的对应参数
MethodParameter[] parameters = getMethodParameters();
// 如果方法没参数,就直接返回
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
// 开始绑定参数 循环参数
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 寻找参数解析器 如果没有参数解析器支持,就抛出异常
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 找到参数解析器后,进行参数解析
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
从这里可以看到,参数解析的第一步就是找到调用方法的参数,循环每一个参数,根据参数的类型/注解来获取不同的参数解析器,进而解析参数.
SpringMVC提供的参数解析器有27个:
分别针对不同类型的参数进行处理,同时也可以自定义参数解析器
针对不同的参数解析器支持解析的类型,可以查看对应参数解析器的supportsParameter方法来看,不再详述
我们继续进入参数解析源码,追踪上文中的this.resolvers.resolveArgument方法
HandlerMethodArgumentResolverComposite#resolveArgument
上文中的this.resolvers对象的类型就是HandlerMethodArgumentResolverComposite,这属于java中的组合模式,简单的说就是这个对象里有个集合,存放了所有的参数解析器,当然在SpringMVC里,参数解析器是懒加载的.
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);
}
这里获取参数解析器的方法跟第一步中的 supportsParameter(parameter) 其实调用的是同一个方法:
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;
}
我们继续追踪源码:
调用对应参数解析器的resolver.resolveArgument方法:
ModelAttributeMethodProcessor#resolveArgument
进入ModelAttributeMethodProcessor类,这个类是27种参数解析器的其中一个,为什么List类型的参数会进入这个参数解析器?
看下supportParameter方法:
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
支持的参数类型规则为: 有ModelAttribute注解 或者 任何不是基本类型的参数都支持.
由于List参数并没有使用任何注解,并且不是基本参数类型,所以就到了这个参数解析器
查看resolveArgument方法:
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
// 关键点 获取参数的名称
String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
...
}
源码较多,只粘贴最核心的几行代码,这里根据参数类型获取参数的名称, 然后再根据参数名称去获取request中的参数.
查看获取参数名称的源码:
ModelFactory#getNameForParameter
public static String getNameForParameter(MethodParameter parameter) {
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
String name = (ann != null ? ann.value() : null);
return (StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter));
}
查看参数是否有ModelAttribute注解,如果有,取该注解的值即可,否则就使用Conventions.getVariableNameForParameter方法获取参数名
Conventions#getVariableNameForParameter
public static String getVariableNameForParameter(MethodParameter parameter) {
Assert.notNull(parameter, "MethodParameter must not be null");
boolean pluralize = false;
String reactiveSuffix = "";
Class valueClass;
// 如果是数组
if (parameter.getParameterType().isArray()) {
valueClass = parameter.getParameterType().getComponentType();
pluralize = true;
// 如果是集合
} else if (Collection.class.isAssignableFrom(parameter.getParameterType())) {
valueClass = ResolvableType.forMethodParameter(parameter).asCollection().resolveGeneric(new int[0]);
if (valueClass == null) {
throw new IllegalArgumentException("Cannot generate variable name for non-typed Collection parameter type");
}
pluralize = true;
} else {
valueClass = parameter.getParameterType();
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(valueClass);
if (adapter != null && !adapter.getDescriptor().isNoValue()) {
reactiveSuffix = ClassUtils.getShortName(valueClass);
valueClass = parameter.nested().getNestedParameterType();
}
}
// 根据是数组还是集合 来获取不同的name
String name = ClassUtils.getShortNameAsProperty(valueClass);
// 拼接name和后缀
return pluralize ? pluralize(name) : name + reactiveSuffix;
}
这块就比较明显了,根据参数的类型是数组还是集合来获取不同的名称,并决定是否拼接不同的后缀
我们的参数类型是List,属于集合,且集合元素的类型是String 因此name为string.
再看下pluralize方法:
private static String pluralize(String name) {
return name + "List";
}
直接拼接了一个List.
所以最终List参数的name就为 stringList,并不是我们参数中指定list,也就无法与request中的list参数做对应了.
加上@RequestParam
那么为什么加上@RequestParam就可以了?
我们继续看源码,前几步源码都一样,只是在选择参数解析器时,由于加上了一个@RequestParam注解,所以参数解析器就不再是ModelAttributeMethodProcessor了.
RequestParamMethodArgumentResolver#supportsParameter
参数解析器变为了RequestParamMethodArgumentResolver,注意这个参数解析器默认是第一位的,所以即便ModelAttributeMethodProcessor仍然可以解析List参数,但是会优先使用RequestParamMethodArgumentResolver参数解析器.
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;
}
}
}
可以看到,当具有注解@RequestParam且该注解的name不为空时,就会使用该参数解析器
我们继续看RequestParamMethodArgumentResolver参数解析器的resolveArgument方法,由于RequestParamMethodArgumentResolver是继承自AbstractNamedValueMethodArgumentResolver类,我们查看AbstractNamedValueMethodArgumentResolver的解析方法:
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();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 根据参数name获取request中对应的值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
...
}
这里也是要获取参数的名称,我们继续追踪查看获取参数名称的源码:
AbstractNamedValueMethodArgumentResolver#getNamedValueInfo
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
if (namedValueInfo == null) {
// 查看如何获取name的
namedValueInfo = createNamedValueInfo(parameter);
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
this.namedValueInfoCache.put(parameter, namedValueInfo);
}
return namedValueInfo;
}
从缓存中获取参数对应的参数名,如果不存在,就重新获取,我们是第一次进入,查看如何获取name的源码:
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
// 获取注解
RequestParam ann = parameter.getParameterAnnotation(RequestParam.class);
// 返回注解构建的name对象
return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo());
}
先获取对应属性的注解,将注解的name/required/defaultValue创建对象,返回.
结束
到此就可以知道为什么不加@RequestParam时,List参数无法绑定了,并提示错误,因为不加@RequestParam,会使用ModelAttributeMethodProcessor解析器, 该解析器不会使用方法中参数的名称作为参数名去绑定,而是根据参数的类型拼接了一个参数名称,这个名称与请求中的参数对不上,就无法获取了.
同时根据源码追踪中的理解,给参数加上ModelAttribute也应该可以获取到对应的参数名,经过验证是可以的.
同时在不加@RequestParam的前提下,将请求中的参数名改为stringList,也是可以解析到的.
总结
这次追踪源码,并没有去追踪参数绑定的具体过程,而是追踪参数名称的获取,发现不同参数解析器对参数名的获取也是不一样,这也是导致刚开始的问题,因此对于简单参数来说,加不加@RequestParam是效果一样的,但是对于复杂参数或者集合类的参数时,不加@RequestParam可能会获取不到具体的参数.