红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 10:参数解析器

2025年6月28日 8点热度 0人点赞 0条评论

在前文中介绍了用于处理控制器方法的参数解析器和返回值解析器,本篇文章展开讨论 Spring 框架提供的不同类型的参数解析器的用途。

添加一个控制器类:

@Controller
@RequestMapping("/test")
private static class TestController {
    @GetMapping
    public String test(@RequestParam String name,
                       @RequestParam Integer age,
                       String sex,
                       @RequestParam(defaultValue = "${JAVA_HOME}") String javaHome,
                       @RequestParam MultipartFile file) {
        return null;
    }
}

这个控制器类上的方法用多种方式接收 HTTP 请求的查询参数,这些都是平时的常见写法。

这里的查询参数指 URL 中的查询参数以及表单提交的表单参数。

前文提到了,控制器方法在 Spring 框架中被定义为HandlerMethod类型,我们可以利用它检视控制器方法的形参:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.registerBean(TestController.class);
ctx.refresh();
TestController controller = ctx.getBean(TestController.class);
HandlerMethod handlerMethod = new HandlerMethod(
    controller,
    TestController.class.getMethod("test", String.class, Integer.class, String.class, String.class, MultipartFile.class));
for (MethodParameter methodParameter : handlerMethod.getMethodParameters()) {
    Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
    String annotationsStr = Arrays.stream(parameterAnnotations).map(pa -> '@' + pa.annotationType().getSimpleName()).collect(Collectors.joining(" "));
    System.out.println("形参:注解(%s),类型(%s),参数名(%s)".formatted(
        annotationsStr,
        methodParameter.getParameterType().getSimpleName(),
        methodParameter.getParameterName()));
}

这里使用的容器是AnnotationConfigApplicationContext而非前文使用的AnnotationConfigServletWebServerApplicationContext,是因为这里不需要真的使用 Web Server,只会用相应的 API 进行模拟调用。

输出信息中缺少参数名称,这是因为缺少名称解析器:

methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());

请求参数解析器

使用RequestParamMethodArgumentResolver 从 HTTP 请求中解析实参:

// 用于处理 @RequestParam 标记的控制器参数的解析器
RequestParamMethodArgumentResolver argumentResolver = new RequestParamMethodArgumentResolver(null, false);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
// 模拟 HTTP 请求
MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest("GET", "/test");
mockHttpServletRequest.addParameter("name", "张三");
mockHttpServletRequest.addParameter("age", "20");
mockHttpServletRequest.addParameter("sex", "female");
mockHttpServletRequest.addPart(new MockPart("file", "test.txt", "hello".getBytes(StandardCharsets.UTF_8)));
MultipartHttpServletRequest multipartHttpServletRequest = new StandardServletMultipartResolver().resolveMultipart(mockHttpServletRequest);
ServletWebRequest servletWebRequest = new ServletWebRequest(multipartHttpServletRequest);
for (MethodParameter methodParameter : handlerMethod.getMethodParameters()) {
    // 添加参数名称解析器
    methodParameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
    Annotation[] parameterAnnotations = methodParameter.getParameterAnnotations();
    String annotationsStr = Arrays.stream(parameterAnnotations).map(pa -> '@' + pa.annotationType().getSimpleName()).collect(Collectors.joining(" "));
    System.out.println("==================================");
    System.out.println("形参:注解(%s),类型(%s),参数名(%s)".formatted(
        annotationsStr,
        methodParameter.getParameterType().getSimpleName(),
        methodParameter.getParameterName()));
    if (argumentResolver.supportsParameter(methodParameter)) {
        // 使用参数解析器从 http 请求中解析实参
        Object resolvedArgument = argumentResolver.resolveArgument(methodParameter, mavContainer, servletWebRequest, null);
        System.out.println("实参:类型(%s),值(%s)".formatted(resolvedArgument.getClass().getSimpleName(), resolvedArgument));
    }
}

从输出中可以看到,没有类型转换,得到的依然是原始的 String 类型:

形参:注解(@RequestParam),类型(Integer),参数名(age)
实参:类型(String),值(20)

这个工作由数据绑定工厂(Data Binding Factory)负责,创建数据绑定工厂:

DefaultDataBinderFactory defaultDataBinderFactory = new DefaultDataBinderFactory(null);

为参数解析器指定数据绑定工厂:

Object resolvedArgument = argumentResolver.resolveArgument(methodParameter, mavContainer, servletWebRequest, defaultDataBinderFactory);

没有任何注解标记的参数没有得到正确解析:

形参:注解(),类型(String),参数名(sex)

RequestParamMethodArgumentResolver的构造器提供一个参数useDefaultResolution,可以通过设置该参数为true让其编程默认的参数解析器,换言之,即使控制器方法的参数没有用 @RequestParam 注解,也会被它解析。

使用表达式用环境变量作为缺省值的参数也没有解析成功:

形参:注解(@RequestParam),类型(String),参数名(javaHome)
实参:类型(String),值(${JAVA_HOME})

这是因为该功能由 Bean 工厂提供,需要通过构造器为解析器提供 Bean 工厂的引用:

RequestParamMethodArgumentResolver argumentResolver = new RequestParamMethodArgumentResolver(ctx.getBeanFactory(), true);

组合解析器

通常有一组解析器用于解析控制器方法参数,因此 Spring 提供一个组合解析器,将这些解析器组合起来使用,调用组合解析器的supportsParameter方法时,会依次调用其内部解析器的相应方法,以检查哪个解析器可用,会将第一个可用的解析器应用于参数解析。

// 使用组合解析器
HandlerMethodArgumentResolverComposite resolverComposite = new HandlerMethodArgumentResolverComposite();
resolverComposite.addResolver(argumentResolver);

调用组合解析器:

if (resolverComposite.supportsParameter(methodParameter)) {
// 使用参数解析器从 http 请求中解析实参
Object resolvedArgument = resolverComposite.resolveArgument(methodParameter, mavContainer, servletWebRequest, defaultDataBinderFactory);
System.out.println("实参:类型(%s),值(%s)".formatted(resolvedArgument.getClass().getSimpleName(), resolvedArgument));
}

路径参数解析器

PathVariableMethodArgumentResolver用于解析路径参数。比如:

@PathVariable("id") Integer id

添加路径参数解析器:

// 使用组合解析器
HandlerMethodArgumentResolverComposite resolverComposite = new HandlerMethodArgumentResolverComposite();
// 添加用于解析路径参数的解析器
resolverComposite.addResolver(new PathVariableMethodArgumentResolver());
resolverComposite.addResolver(argumentResolver);

注意添加顺序,RequestParam 解析器应该在 PathVariable 解析器之后,因为前者作为默认解析器生效,如果在前边,就会总生效。

使用路径参数的请求产生后,会被前文提到过的 RequestMappingHandlerMapping 进行解析,会解析为路径参数名和值的键值对映射,并将映射保存在 Request 域中:

Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
mockHttpServletRequest.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);

这样 PathVariable 解析器就可以根据映射返回解析后的参数值。

请求头解析器

RequestHeaderMethodArgumentResolver用于解析请求头:

@RequestHeader("Content-type") String contentType

添加解析器:

// 添加解析请求头的解析器
resolverComposite.addResolver(new RequestHeaderMethodArgumentResolver(ctx.getBeanFactory()));

模拟请求的假数据中添加请求头信息:

mockHttpServletRequest.setContentType("application/json");

Cookie 解析器

ServletCookieValueMethodArgumentResolver用于解析 Cookie 信息:

@CookieValue("token") String token

添加解析器:

// 添加用于 Cookie 的解析器
resolverComposite.addResolver(new ServletCookieValueMethodArgumentResolver(ctx.getBeanFactory()));

模拟请求中加入 Cookie 信息:

// 添加 cookie 信息
mockHttpServletRequest.setCookies(new Cookie("token", "123abc"));

表达式解析器

Spring 支持使用${...}表达式获取环境变量或资源文件中的配置信息。特别的,在控制器方法中同样可以利用参数获取:

@Value("${JAVA_HOME}") String javaHomeVal

这是因为使用了支持表达式的参数解析器:

// 添加表达式解析器
resolverComposite.addResolver(new ExpressionValueMethodArgumentResolver(ctx.getBeanFactory()));

特殊类型解析器

对于一些请求相关的特殊类型,比如HttpServletRequest或Session等对象,可以直接从控制器方法的参数中获取:

HttpServletRequest httpServletRequest

这是因为参数处理器ServletRequestMethodArgumentResolver:

// 添加特殊类型解析器
resolverComposite.addResolver(new ServletRequestMethodArgumentResolver());

实际上这个参数处理器可以处理多种类型:

image-20250628174948424

ModelAttribute 解析器

@ModelAttribute标记的处理器方法参数,将会从查询参数中获取属性值组装成对象。比如:

@ModelAttribute User user

添加解析器:

// 添加 ModelAttribute 解析器
resolverComposite.addResolver(new ServletModelAttributeMethodProcessor(false));

这个解析器可以通过构造器参数指定是否需要@ModelAttribute注解,也就是说这个解析器也可以在无注解的情况下工作。

比如:

User user2

添加解析器:

resolverComposite.addResolver(new ServletModelAttributeMethodProcessor(true));

这个不需要注解的解析器同样需要放在最后,原因和前面说的一样。

请求体解析器

从请求体获取 JSON 数据是很常见的操作:

@RequestBody User user3

需要的解析器:

// RequestBody 解析器
resolverComposite.addResolver(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));

模拟的请求数据:

// 模拟请求体 JSON
mockHttpServletRequest.setContent("{\"name\":\"汤姆\",\"age\":15}".getBytes(StandardCharsets.UTF_8));

解析器顺序

组合解析器中的解析器顺序是要格外注意的,因为只要有一个解析器生效,后边的解析器就不会再生效。

因此需要将使用明确注解进行解析的解析器放在前面,不使用注解也可以生效的解析器放在最后:

// 使用组合解析器
HandlerMethodArgumentResolverComposite resolverComposite = new HandlerMethodArgumentResolverComposite();
// RequestBody 解析器
resolverComposite.addResolver(new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())));
// RequestParam 解析器
resolverComposite.addResolver(new RequestParamMethodArgumentResolver(false));
// 添加 ModelAttribute 解析器
resolverComposite.addResolver(new ServletModelAttributeMethodProcessor(false));
// 添加特殊类型解析器
resolverComposite.addResolver(new ServletRequestMethodArgumentResolver());
// 添加表达式解析器
resolverComposite.addResolver(new ExpressionValueMethodArgumentResolver(ctx.getBeanFactory()));
// 添加用于 Cookie 的解析器
resolverComposite.addResolver(new ServletCookieValueMethodArgumentResolver(ctx.getBeanFactory()));
// 添加用于解析路径参数的解析器
resolverComposite.addResolver(new PathVariableMethodArgumentResolver());
// 添加解析请求头的解析器
resolverComposite.addResolver(new RequestHeaderMethodArgumentResolver(ctx.getBeanFactory()));
resolverComposite.addResolver(new ServletModelAttributeMethodProcessor(true));
// 不需要使用注解的 RequestParam 解析器
resolverComposite.addResolver(new RequestParamMethodArgumentResolver(ctx.getBeanFactory(), true));

最后的两个解析器ServletModelAttributeMethodProcessor和RequestParamMethodArgumentResolver都可以解析没有注解的参数,ServletModelAttributeMethodProcessor解析非基本类型,RequestParamMethodArgumentResolver负责解析基本类型。

本文的完整示例可以从这里获取。

The End.

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: mvc spring boot 参数解析器
最后更新:2025年6月28日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号