在中介绍了用于处理控制器方法的参数解析器和返回值解析器,本篇文章展开讨论 Spring 框架提供的不同类型的参数解析器的用途。
添加一个控制器类:
"/test")
(private static class TestController {
public String test( String name,
Integer age,
String sex,
defaultValue = "${JAVA_HOME}") String javaHome,
( 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
用于解析路径参数。比如:
"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
用于解析请求头:
"Content-type") String contentType (
添加解析器:
// 添加解析请求头的解析器
resolverComposite.addResolver(new RequestHeaderMethodArgumentResolver(ctx.getBeanFactory()));
模拟请求的假数据中添加请求头信息:
mockHttpServletRequest.setContentType("application/json");
Cookie 解析器
ServletCookieValueMethodArgumentResolver
用于解析 Cookie 信息:
"token") String token (
添加解析器:
// 添加用于 Cookie 的解析器
resolverComposite.addResolver(new ServletCookieValueMethodArgumentResolver(ctx.getBeanFactory()));
模拟请求中加入 Cookie 信息:
// 添加 cookie 信息
mockHttpServletRequest.setCookies(new Cookie("token", "123abc"));
表达式解析器
Spring 支持使用${...}
表达式获取环境变量或资源文件中的配置信息。特别的,在控制器方法中同样可以利用参数获取:
"${JAVA_HOME}") String javaHomeVal (
这是因为使用了支持表达式的参数解析器:
// 添加表达式解析器
resolverComposite.addResolver(new ExpressionValueMethodArgumentResolver(ctx.getBeanFactory()));
特殊类型解析器
对于一些请求相关的特殊类型,比如HttpServletRequest
或Session
等对象,可以直接从控制器方法的参数中获取:
HttpServletRequest httpServletRequest
这是因为参数处理器ServletRequestMethodArgumentResolver
:
// 添加特殊类型解析器
resolverComposite.addResolver(new ServletRequestMethodArgumentResolver());
实际上这个参数处理器可以处理多种类型:
ModelAttribute 解析器
@ModelAttribute
标记的处理器方法参数,将会从查询参数中获取属性值组装成对象。比如:
User user
添加解析器:
// 添加 ModelAttribute 解析器
resolverComposite.addResolver(new ServletModelAttributeMethodProcessor(false));
这个解析器可以通过构造器参数指定是否需要@ModelAttribute
注解,也就是说这个解析器也可以在无注解的情况下工作。
比如:
User user2
添加解析器:
resolverComposite.addResolver(new ServletModelAttributeMethodProcessor(true));
这个不需要注解的解析器同样需要放在最后,原因和前面说的一样。
请求体解析器
从请求体获取 JSON 数据是很常见的操作:
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.
文章评论