加不加@RequestParam是一样的效果吗

572 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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可能会获取不到具体的参数.