参数解析器

参数解析器

 
构造一个如下的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 的调用过程
  1. 控制器方法被封装为 HandlerMethod
  1. 准备对象绑定与类型转换
  1. 准备 ModelAndViewContainer 用来存储中间 Model 结果
  1. 解析每个参数值
获取每个参数
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); } }
notion image
 
 

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()); } } }
notion image

类型转换器工

由于 没有类型转换器
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);
notion image
可以看到 参数age的类型已经解析为Integer

默认参数解析方式

没有带@RequestParam 的参数name2没有解析
notion image
request.setParameter("name2", "lisi");
改成true使用默认解析方式,这样没有识别到@RequestParam 的方法参数都会走这个解析
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory, true);
notion image
但是这个方法导致加了其他注解的也走了RequestParamMethodArgumentResolver
导致解析报错
notion image
 

组合解析器

  1. 常见参数的解析
      • @RequestParam
      • 省略 @RequestParam
      • @RequestParam(defaultValue)
      • MultipartFile
      • @PathVariable
      • @RequestHeader
      • @CookieValue
      • @Value
      • HttpServletRequest 等
      • @ModelAttribute
      • 省略 @ModelAttribute
      • @RequestBody
  1. 组合模式在 Spring 中的体现
  1. @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 等注解中的参数名、默认值, 都可以写成活的, 即从 ${ } #{ }中获取 */ }
notion image
 
 

参数名获取方式

java编译后不保留参数名

在src目录的平行目录下新建一个a22目录
notion image
新增一个类 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文件反编译成字节码
notion image

class文件保留参数名称的方法

  1. 编译时添加 -parameters 选项
    1. javac -parameters .\Bean2.java
      反编译保留了参数名称
      notion image
      javap -c -v .\Bean2.class 将class文件反编译成字节码
      字节码多了MethodParameters信息
      notion image
  1. 编译时添加 -g
    1. javac -g .\Bean2.java class文件也保留了参数名称
      notion image
      notion image
      发现字节码多了本地变量表的方法参数信息,用 asm 可以拿到参数名
       
 

测试反射获取参数

将a22这个目录添加到show模块的类路径下
notion image
通过反射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()); }
  1. 默认Java编译方式 javac .\Bean2.java
    1. 反射方式没有获取参数名称
      notion image
  1. javac -g .\Bean2.java
    1. 反射无法获取本地变量表的参数名称
      notion image
  1. javac -parameters .\Bean2.java
    1. 可以从字节码参数表获取
      notion image
       
 

通过ASM方式

Spring提供的 LocalVariableTableParameterNameDiscoverer 类通过ASM方式获取参数
notion image
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = discoverer.getParameterNames(foo); System.out.println(Arrays.toString(parameterNames));
  1. 默认编译方式
    1. notion image
  1. -g选项编译
    1. notion image
  1. -parameters 选项编译
    1. notion image
 

接口

public interface Bean1 { public void foo(String name, int age); }
  1. javac -g .\Bean1.java
    1. notion image
      接口生成的字节码不包含本地变量表
      • 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
  1. javac -parameters .\Bean1.java
    1. notion image
通过反射的方式可以正常获取带-parameters 选项编译后的方法名称
通过ASM方式无法获取接口编译后的方法名称
 

总结

  1. 如果编译时添加了 -parameters 可以生成参数表, 反射时就可以拿到参数名
  1. 如果编译时添加了 -g 可以生成调试信息, 但分为两种情况
      • 普通类, 会包含局部变量表, 用 asm 可以拿到参数名
      • 接口, 不会包含局部变量表, 无法获得参数名
        • 这也是 MyBatis 在实现 Mapper 接口时为何要提供 @Param 注解来辅助获得参数名
        •