如下是所有的参数解析器,下面会通过代码演示常见的参数解析器
1、初步了解 RequestMappingHandlerAdapter 的调用过程
先了解一下 HandlerMethod,后面代码会用到。
HandlerMethod封装了很多属性,在访问请求方法的时候可以方便的访问到方法、方法参数、方法上的注解、所属类等并且对方法参数封装处理,也可以方便的访问到方法参数的注解等信息。
public HandlerMethod(Object bean, Method method) {
this((Object)bean, (Method)method, (MessageSource)null);
}
protected HandlerMethod(Object bean, Method method, @Nullable MessageSource messageSource) {
Assert.notNull(bean, "Bean is required");
Assert.notNull(method, "Method is required");
this.bean = bean;
this.beanFactory = null;
this.messageSource = messageSource;
this.beanType = ClassUtils.getUserClass(bean);
this.method = method;
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
ReflectionUtils.makeAccessible(this.bridgedMethod);
this.parameters = this.initMethodParameters();
this.evaluateResponseStatus();
this.description = initDescription(this.beanType, this.method);
}
public MethodParameter[] getMethodParameters() {
return this.parameters;
}
其中的 initMethodParameters() 方法会初始化方法的参数信息:
User类:
public class User {
private String name;
private int age;
// 省略了基本方法
}
Controller类
public class Controller {
public void test(
@RequestParam("name1") String name1, // name1=张三
String name2, // name2=李四
@RequestParam("age") int age, // age=18 格式转换
@RequestParam(name = "home", defaultValue = "${JAVA_HOME}") String home1, // spring 获取数据
@RequestParam("file") MultipartFile file, // 上传文件
@PathVariable("id") int id, // /test/124 /test/{id}
@RequestHeader("Content-Type") String header,
@CookieValue("token") String token,
@Value("${JAVA_HOME}") String home2, // spring 获取数据 ${} #{}
HttpServletRequest request, // request, response, session ...
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
@RequestBody User user3 // json
) {
}
}
配置类
@Configuration
public class WebConfig {
}
主启动类
public class A21 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 准备测试 Request
HttpServletRequest request = mockRequest();
// 要点1. 控制器方法被封装为 HandlerMethod
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test",
String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class,
String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));
//多个解析器的组合 组合器的设计模式
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
//beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
// false:表示必须有 @RequestParam true:表示不带 @RequestParam 的参数都会交给该解析器解析
new RequestParamMethodArgumentResolver(beanFactory, false)
);
// 要点4. 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
//获取参数上的注解,并连接起来
String annotions = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotions.length() > 0 ? "@" + annotions : " ";
//是否支持此参数
if (composite.supportsParameter(parameter)) {
//解析参数,获取参数的值
//参数一:方法参数 参数二:ModelAndViewContainer,处理保存Model和View
//参数三:请求 参数四:数据绑定工厂
Object v = composite.resolveArgument(parameter, null, new ServletWebRequest(request), null);
System.out.println("[" + parameter.getParameterIndex() + "] " + str + " " + parameter.getParameterType().getSimpleName()
+ " " + parameter.getParameterName() + " -> " + v + " [" + v.getClass() + "]");
} else {
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName()
+ " " + parameter.getParameterName());
}
}
}
// 模拟请求
private static HttpServletRequest mockRequest() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name1", "zhangsan");
request.setParameter("name2", "lisi");
// 参数一:参数名 参数二:文件名 参数三:内容
request.addPart(new MockPart("file", "abc", "hello".getBytes(StandardCharsets.UTF_8)));
// 找出/test/{id} 与 /test/123 的映射关系
// map:id=123
Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123");
//放到 request 作用域
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, map);
request.setContentType("application/json");
request.setCookies(new Cookie("token", "123456"));
request.setParameter("name", "张三");
request.setParameter("age", "18");
Gson gson = new Gson();
String json = gson.toJson(new User("王五", 4));
request.setContent(json.getBytes(StandardCharsets.UTF_8));
return new StandardServletMultipartResolver().resolveMultipart(request);
}
}
结果:所有带箭头的都是被解析的,发现有两个问题:
- 参数类型都为 String,方法参数的类型没有被解析
- 方法参数的名字为 null
添加数据绑定工厂:ServletRequestDataBinderFactory
- 方法参数的类型被成功解析
设置参数名的解析器
- parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
- 得到方法参数的名字
public class A21 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory(WebConfig.class);
// 准备测试 Request
HttpServletRequest request = mockRequest();
// 要点1. 控制器方法被封装为 HandlerMethod
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test",
String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class,
String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));
// 要点2. 准备对象绑定与类型转换
ServletRequestDataBinderFactory bindFactory = new ServletRequestDataBinderFactory(null, null);
//多个解析器的组合 组合器的设计模式
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
//beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
// false:表示必须有 @RequestParam true:表示不带 @RequestParam 的参数都会交给该解析器解析
new RequestParamMethodArgumentResolver(beanFactory, false)
);
// 要点4. 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
//获取参数上的注解,并连接起来
String annotions = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotions.length() > 0 ? "@" + annotions : " ";
// 设置参数名的解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
//是否支持此参数
if (composite.supportsParameter(parameter)) {
//解析参数,获取参数的值
//参数一:方法参数 参数二:ModelAndViewContainer,处理保存Model和View
//参数三:请求 参数四:数据绑定工厂
Object v = composite.resolveArgument(parameter, null, new ServletWebRequest(request), bindFactory);
System.out.println("[" + parameter.getParameterIndex() + "] " + str + " " + parameter.getParameterType().getSimpleName()
+ " " + parameter.getParameterName() + " -> " + v + " [" + v.getClass() + "]");
} else {
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName()
+ " " + parameter.getParameterName());
}
}
}
}
添加 @RequestMapping 解析器时参数设置为 true,表示不带 @RequestParam 的参数都会交给该解析器解析
composite.addResolvers(
//beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
// false:表示必须有 @RequestParam true:表示不带 @RequestParam 的参数都会交给该解析器解析
new RequestParamMethodArgumentResolver(beanFactory, true)
);
结果:
原因:解析 @PathVariable 时报错,@PathVariable 并不属于该解析器解析,解决办法后面会说。
2、常见参数的解析
准备 ModelAndViewContainer 用来存储中间 Model 结果,解析 @ModelAttribute 会把结果存储在里面。
Controller类有这样的写法,是因为存储的时候如果不指定名字,会按类型的小写存储,由于两个参数类型一样,第二个结果会覆盖掉第一个结果。
@ModelAttribute("abc") User user1, // name=zhang&age=18
User user2, // name=zhang&age=18
public class A21 {
public static void main(String[] args) throws Exception {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 准备测试 Request
HttpServletRequest request = mockRequest();
// 要点1. 控制器方法被封装为 HandlerMethod
HandlerMethod handlerMethod = new HandlerMethod(new Controller(), Controller.class.getMethod("test",
String.class, String.class, int.class, String.class, MultipartFile.class, int.class, String.class,
String.class, String.class, HttpServletRequest.class, User.class, User.class, User.class));
// 要点2. 准备对象绑定与类型转换
ServletRequestDataBinderFactory bindFactory = new ServletRequestDataBinderFactory(null, null);
// 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();
//多个解析器的组合 组合器的设计模式
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolvers(
//beanFactory:需要在Spring容器里获取值,如 ${JAVA_HOME}
// false:表示必须有 @RequestParam true:表示不带 @RequestParam 的参数都会交给该解析器解析(报错)
new RequestParamMethodArgumentResolver(beanFactory, false),
new PathVariableMethodArgumentResolver(),
new RequestHeaderMethodArgumentResolver(beanFactory),
new ServletCookieValueMethodArgumentResolver(beanFactory),
new ExpressionValueMethodArgumentResolver(beanFactory),
new ServletRequestMethodArgumentResolver(),
new ServletModelAttributeMethodProcessor(false), //必须有 @ModelAttribute
//位置,放在下面会被 ServletModelAttributeMethodProcessor 解析
new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true), //省略了 @ModelAttribute
new RequestParamMethodArgumentResolver(beanFactory, true)//省略了 @RequestParam
);
// 要点4. 解析每个参数值
for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
//获取参数上的注解,并连接起来
String annotions = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining());
String str = annotions.length() > 0 ? "@" + annotions : " ";
// 设置参数名的解析器
parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
//是否支持此参数
if (composite.supportsParameter(parameter)) {
//解析参数,获取参数的值
//参数一:方法参数 参数二:ModelAndViewContainer,处理保存Model和View
//参数三:请求 参数四:数据绑定工厂
Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), bindFactory);
System.out.println("[" + parameter.getParameterIndex() + "] " + str + " " + parameter.getParameterType().getSimpleName()
+ " " + parameter.getParameterName() + " -> " + v + " [" + v.getClass() + "]");
System.out.println("模型数据为:" + container.getModel());
} else {
System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName()
+ " " + parameter.getParameterName());
}
}
}
}
结果:
这样添加的原因:防止 @RequestBody 被 ServletModelAttributeMethodProcessor解析,并且保证省略 @ModelAttribute 的参数也可以被 ServletModelAttributeMethodProcessor 解析。
new ServletModelAttributeMethodProcessor(false)
new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),
new ServletModelAttributeMethodProcessor(true), //省略了 @ModelAttribute
如果我这样写
new ServletModelAttributeMethodProcessor(true)
new RequestResponseBodyMethodProcessor(Arrays.asList(new MappingJackson2HttpMessageConverter())),
可以看到解析的结果发生变化,ServletModelAttributeMethodProcessor 解析了 @RequestBody 注解,发生了错误的解析。
3、总结
-
初步了解 RequestMappingHandlerAdapter 的调用过程
-
控制器方法被封装为 HandlerMethod
-
准备对象绑定与类型转换
-
准备 ModelAndViewContainer 用来存储中间 Model 结果
-
解析每个参数值
-
-
解析参数依赖的就是各种参数解析器,它们都有两个重要方法
-
supportsParameter 判断是否支持方法参数
-
resolveArgument 解析方法参数
-
-
常见参数的解析
-
@RequestParam
-
省略 @RequestParam
-
@RequestParam(defaultValue)
-
MultipartFile
-
@PathVariable
-
@RequestHeader
-
@CookieValue
-
@Value
-
HttpServletRequest 等
-
@ModelAttribute
-
省略 @ModelAttribute
-
@RequestBody
-
-
组合模式在 Spring 中的体现
-
@RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取