参数解析器
构造一个如下的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 ) { } static class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } } }
RequestMappingHandlerAdapter 的调用过程
- 控制器方法被封装为 HandlerMethod
- 准备对象绑定与类型转换
- 准备 ModelAndViewContainer 用来存储中间 Model 结果
- 解析每个参数值
获取每个参数
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 factory = new ServletRequestDataBinderFactory(null, null); // 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果 ModelAndViewContainer container = new ModelAndViewContainer(); // 要点4. 解析每个参数值 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining()); System.out.println(annotations); String str = annotations.length() > 0 ? " @" + annotations + " " : " "; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); 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))); Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123"); System.out.println(map); 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"); request.setContent(""" { "name":"李四", "age":20 } """.getBytes(StandardCharsets.UTF_8)); return new StandardServletMultipartResolver().resolveMultipart(request); } }
RequestParamMethodArgumentResolver
解析参数依赖的就是各种参数解析器,它们都有两个重要方法
- supportsParameter 判断是否支持方法参数
- resolveArgument 解析方法参数
解析
@RequestParam
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 factory = new ServletRequestDataBinderFactory(null, null); // 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果 ModelAndViewContainer container = new ModelAndViewContainer(); // 要点4. 解析每个参数值 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, false); String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining()); System.out.println(annotations); String str = annotations.length() > 0 ? " @" + annotations + " " : " "; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); if (resolver.supportsParameter(parameter)){ //如果方法参数标注了@RequestParam,则会调用resolver.resolveArgument方法 Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), null); System.out.println(v.getClass()); System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + "->" + v); }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))); Map<String, String> map = new AntPathMatcher().extractUriTemplateVariables("/test/{id}", "/test/123"); System.out.println(map); 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"); request.setContent(""" { "name":"李四", "age":20 } """.getBytes(StandardCharsets.UTF_8)); return new StandardServletMultipartResolver().resolveMultipart(request); }
原本的整型数字 18变成了字符串类型
[2] @RequestParam int age->18 RequestParam class java.lang.String
解决办法调用解析参数方法的时候补上
ServletRequestDataBinderFactory
// 要点2. 准备对象绑定与类型转换 ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null); Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
可以看到 参数age的类型已经解析为Integer
默认参数解析方式
没有带
@RequestParam
的参数name2没有解析request.setParameter("name2", "lisi");
改成true使用默认解析方式,这样没有识别到
@RequestParam
的方法参数都会走这个解析RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, true);
但是这个方法导致加了其他注解的也走了RequestParamMethodArgumentResolver
导致解析报错
组合解析器
- 常见参数的解析
- @RequestParam
- 省略 @RequestParam
- @RequestParam(defaultValue)
- MultipartFile
- @PathVariable
- @RequestHeader
- @CookieValue
- @Value
- HttpServletRequest 等
- @ModelAttribute
- 省略 @ModelAttribute
- @RequestBody
- 组合模式在 Spring 中的体现
- @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取
HandlerMethodArgumentResolverComposite
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 factory = new ServletRequestDataBinderFactory(null, null); // 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果 ModelAndViewContainer container = new ModelAndViewContainer(); // 要点4. 解析每个参数值 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { // 多个解析器组合 HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite(); composite.addResolvers( // false 表示必须有 @RequestParam new RequestParamMethodArgumentResolver(beanFactory, false), //解析@PathVariable new PathVariableMethodArgumentResolver(), // @RequestHeader new RequestHeaderMethodArgumentResolver(beanFactory), // @CookieValue new ServletCookieValueMethodArgumentResolver(beanFactory), //@Value new ExpressionValueMethodArgumentResolver(beanFactory), //HttpServletRequest new ServletRequestMethodArgumentResolver(), // 必须有 @ModelAttribute new ServletModelAttributeMethodProcessor(false), //解析json new RequestResponseBodyMethodProcessor(List.of(new MappingJackson2HttpMessageConverter())), // 省略了 @ModelAttribute new ServletModelAttributeMethodProcessor(true), // 省略 @RequestParam new RequestParamMethodArgumentResolver(beanFactory, true) ); String annotations = Arrays.stream(parameter.getParameterAnnotations()).map(a -> a.annotationType().getSimpleName()).collect(Collectors.joining()); String str = annotations.length() > 0 ? " @" + annotations + " " : " "; parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); if (composite.supportsParameter(parameter)) { // 支持此参数 Object v = composite.resolveArgument(parameter, container, new ServletWebRequest(request), factory); // System.out.println(v.getClass()); System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName() + "->" + v); System.out.println("模型数据为:" + container.getModel()); } else { System.out.println("[" + parameter.getParameterIndex() + "] " + str + parameter.getParameterType().getSimpleName() + " " + parameter.getParameterName()); } } /* 学到了什么 a. 每个参数处理器能干啥 1) 看是否支持某种参数 2) 获取参数的值 b. 组合模式在 Spring 中的体现 c. @RequestParam, @CookieValue 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取 */ }
参数名获取方式
java编译后不保留参数名
在src目录的平行目录下新建一个a22目录
新增一个类
a22/com/onethink/Bean2.java
public class Bean2 { public void foo(String name, int age) { } }
PS E:\code\java\spring_learn\show\a22\com\onethink> javac .\Bean2.java
手段编译后,查看class文件发现方法的参数名称变成var1,var2并没有保留public class Bean2 { public Bean2() { } public void foo(String var1, int var2) { } }
javap -c -v .\Bean2.class
将class文件反编译成字节码class文件保留参数名称的方法
- 编译时添加
-parameters
选项
javac -parameters .\Bean2.java
反编译保留了参数名称
javap -c -v .\Bean2.class
将class文件反编译成字节码字节码多了MethodParameters信息
- 编译时添加 -g
javac -g .\Bean2.java
class文件也保留了参数名称发现字节码多了本地变量表的方法参数信息,用 asm 可以拿到参数名
测试反射获取参数
将a22这个目录添加到show模块的类路径下
通过反射API方式
// 1. 反射获取参数名 Method foo = com.onethink.a22.Bean2.class.getMethod("foo", String.class, int.class); for (Parameter parameter : foo.getParameters()) { System.out.println(parameter.getName()); }
- 默认Java编译方式
javac .\Bean2.java
反射方式没有获取参数名称
javac -g .\Bean2.java
反射无法获取本地变量表的参数名称
javac -parameters .\Bean2.java
可以从字节码参数表获取
通过ASM方式
Spring提供的
LocalVariableTableParameterNameDiscoverer
类通过ASM方式获取参数LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = discoverer.getParameterNames(foo); System.out.println(Arrays.toString(parameterNames));
- 默认编译方式
-g
选项编译
-parameters
选项编译
接口
public interface Bean1 { public void foo(String name, int age); }
javac -g .\Bean1.java
接口生成的字节码不包含本地变量表
• 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
javac -parameters .\Bean1.java
通过反射的方式可以正常获取带
-parameters
选项编译后的方法名称通过ASM方式无法获取接口编译后的方法名称
总结
- 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名
- 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
- 普通类, 会包含局部变量表, 用 asm 可以拿到参数名
- 接口, 不会包含局部变量表, 无法获得参数名
- 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名