由SpringMVC接收LocalDate作为入参引发的思考

声明

源码基于Spring Boot 2.0.4 、Spring 5.0.8

背景

SpringMVC接收LocalDate类型作为参数时,需要借助@DateTimeFormat注解来指定日期的格式,代码如下

@RestController
public class ArgumentController {

    @GetMapping("/receiveLocalDate")
    public LocalDate receiveLocalDate(@DateTimeFormat(pattern = "yyyy-MM-dd") 
                                      LocalDate birthday) {
        return birthday;
    }
}

但是当在浏览器输入http://localhost:8080/receiveLocalDate?birthday=1997-05-03,收到一个错误

No primary or default constructor found for class java.time.LocalDate.

于是很自然的修改了下代码,把参数再加上一个@RequestParam注解,如下所示

@RestController
public class ArgumentController {

    @GetMapping("/receiveLocalDate")
    public LocalDate receiveLocalDate(@RequestParam
        @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate birthday) {
        return birthday;
    }
}

这样子修改之后就能正常接收参数了。对于我以往的理解,@RequestParam注解只是可以增加一些功能,如标注参数名字,提供默认值,必输性。省略该注解不会影响到参数接收,就如以下代码

@RestController
public class ArgumentController {

    @GetMapping("/receiveDate")
    public Date receiveDate(@DateTimeFormat(pattern = "yyyy-MM-dd") Date birthday) {
        return birthday;
    }
}

这引起了我强烈的迷惑以及探知欲

简要原理

SpringMVC封装参数的逻辑主要由HandlerMethodArgumentResolver接口处理,代码如下

public interface HandlerMethodArgumentResolver {

	/**
	 * 用于判断支不支持该种参数
	 */
	boolean supportsParameter(MethodParameter parameter);

	/**
	 * 获取参数值
	 */
	Object resolveArgument(MethodParameter parameter,
                           @Nullable ModelAndViewContainer mavContainer,
						NativeWebRequest webRequest, 
                           @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

通过IDEA可以找到该接口的很多实现类,很眼熟的如下所示

  • RequestParamMethodArgumentResolver
  • PathVariableMethodArgumentResolver
  • RequestResponseBodyMethodProcessor
  • ServletModelAttributeMethodProcessor

很容易猜到它们分别用于处理@RequestParam@PathVariable@RequestBody标注的参数,以及Pojo对象参数,代码举例如下所示

@GetMapping("/queryUser")
public Integer queryUser(@RequestParam Integer userId) {
    return userId;
}

@GetMapping("/detailUser/{userId}")
public Integer detailUser(@PathVariable Integer userId) {
    return userId;
}

@PostMapping("/createUserByResponseBody")
public User createUserByRequestBody(@RequestBody User user) {
    return user;
}

@PostMapping("/createUser")
public User createUser(User user) {
    return user;
}

而对于背景中提出的问题主要涉及RequestParamMethodArgumentResolverServletModelAttributeMethodProcessor,主要看这两个类的supportsParameter方法

对应代码如下:

/**
 * RequestParamMethodArgumentResolver
 * 可以看到处理的参数类型如下(大体上, 不细究)
 * 1. @RequestParam标注的参数
 * 2. useDefaultResolution为true时, 简单类型参数也被该类处理, 即使没有加@RequestParam注解
 * 
 * 何为简单类型?(详细请看BeanUtils.isSimpleProperty()方法)
 * 1. 基本类型以及对应的包装类型
 * 2. Date, String, Class, URL等
 */
@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;
        }
    }
}

/**
 * ServletModelAttributeMethodProcessor
 * 处理的参数类型如下:
 * 1. 被@ModelAttribute标注的参数
 * 2. annotationNotRequired为ture时, 非简单类型参数, 即使没有被@ModelAttribute标注
 */
@Override
public boolean supportsParameter(MethodParameter parameter) {
    return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
            (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}

看到这里就能明白当没有加@RequestParam注解时,为什么不能正常接收LocalDate类型参数,却能正常接收Date类型参数了。因为LocalDate不是一个简单类型,当没有被@RequestParam注解标注时,是由ServletModelAttributeMethodProcessor来处理的,它把LocalDate参数当作一个模型对象(简单理解POJO对象),也就是说先会调用构造方法初始化实例对象,然后赋值它的属性,于是报了如上的错误。

参数封装流程

入口在RequestMappingHandlerAdapter类中,封装参数的类为HandlerMethodArgumentResolverComposite,该类也实现了HandlerMethodArgumentResolver接口,使用组合模式,内部维护了一个List<HandlerMethodArgumentResolver>

/**
 * 初始化HandlerMethodArgumentResolverComposite对象
 */
@Override
public void afterPropertiesSet() {
    // Do this first, it may add ResponseBody advice beans
    initControllerAdviceCache();

    if (this.argumentResolvers == null) {
        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    // 略
}

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();

    // Annotation-based argument resolution
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    resolvers.add(new RequestParamMapMethodArgumentResolver());
    resolvers.add(new PathVariableMethodArgumentResolver());
    resolvers.add(new PathVariableMapMethodArgumentResolver());
    resolvers.add(new MatrixVariableMethodArgumentResolver());
    resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    resolvers.add(new ServletModelAttributeMethodProcessor(false));
    resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    resolvers.add(new SessionAttributeMethodArgumentResolver());
    resolvers.add(new RequestAttributeMethodArgumentResolver());

    // Type-based argument resolution
    resolvers.add(new ServletRequestMethodArgumentResolver());
    resolvers.add(new ServletResponseMethodArgumentResolver());
    resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    resolvers.add(new RedirectAttributesMethodArgumentResolver());
    resolvers.add(new ModelMethodProcessor());
    resolvers.add(new MapMethodProcessor());
    resolvers.add(new ErrorsMethodArgumentResolver());
    resolvers.add(new SessionStatusMethodArgumentResolver());
    resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

    // Custom arguments
    if (getCustomArgumentResolvers() != null) {
        resolvers.addAll(getCustomArgumentResolvers());
    }

    // Catch-all
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    resolvers.add(new ServletModelAttributeMethodProcessor(true));

    return resolvers;
}

可以看到RequestParamMethodArgumentResolverServletModelAttributeMethodProcessor这两个处理器被添加了两次,参数为true被放到了List的最后面,用于兜底。所以即使参数没有被@RequestParamModelAttribute标注,也能被处理到。

/**
 * 入口执行方法
 */
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
                                           HttpServletResponse response, 
                                           HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

        // 将argumentResolvers赋值给invocableMethod对象
        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }
        invocableMethod.setDataBinderFactory(binderFactory);
        invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

        AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
        asyncWebRequest.setTimeout(this.asyncRequestTimeout);

        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.setTaskExecutor(this.taskExecutor);
        asyncManager.setAsyncWebRequest(asyncWebRequest);
        asyncManager.registerCallableInterceptors(this.callableInterceptors);
        asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

        if (asyncManager.hasConcurrentResult()) {
            Object result = asyncManager.getConcurrentResult();
            mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
            asyncManager.clearConcurrentResult();
            LogFormatUtils.traceDebug(logger, traceOn -> {
                String formatted = LogFormatUtils.formatValue(result, !traceOn);
                return "Resume with async result [" + formatted + "]";
            });
            invocableMethod = invocableMethod.wrapConcurrentResult(result);
        }

        // 执行方法
        invocableMethod.invokeAndHandle(webRequest, mavContainer);
        if (asyncManager.isConcurrentHandlingStarted()) {
            return null;
        }

        return getModelAndView(mavContainer, modelFactory, webRequest);
    }
    finally {
        webRequest.requestCompleted();
    }
}
/**
 * ServletInvocableHandlerMethod类
 */
public void invokeAndHandle(ServletWebRequest webRequest, 
                            ModelAndViewContainer mavContainer,
			Object... providedArgs) throws Exception {

    // 看invokeForRequest方法
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    setResponseStatus(webRequest);

    if (returnValue == null) {
        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
            disableContentCachingIfNecessary(webRequest);
            mavContainer.setRequestHandled(true);
            return;
        }
    }
    else if (StringUtils.hasText(getResponseStatusReason())) {
        mavContainer.setRequestHandled(true);
        return;
    }

    mavContainer.setRequestHandled(false);
    Assert.state(this.returnValueHandlers != null, "No return value handlers");
    try {
        this.returnValueHandlers.handleReturnValue(
            returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }
    catch (Exception ex) {
        if (logger.isTraceEnabled()) {
            logger.trace(formatErrorForReturnValue(returnValue), ex);
        }
        throw ex;
    }
}

@Nullable
public Object invokeForRequest(NativeWebRequest request,
                               @Nullable ModelAndViewContainer mavContainer,
                               Object... providedArgs) throws Exception {

    // 看getMethodArgumentValues方法
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    if (logger.isTraceEnabled()) {
        logger.trace("Arguments: " + Arrays.toString(args));
    }
    return doInvoke(args);
}

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;
}
posted on 2022-04-29 17:00  wastonl  阅读(1731)  评论(0编辑  收藏  举报