1、参数解析器
前篇提到过,参数解析器是HandlerAdapters中的组件,用于解析controller层方法中加了注解的参数信息。
有一个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") A21.User user1,          // name=zhang&age=18
            A21.User user2,                          // name=zhang&age=18
            @RequestBody A21.User user3              // json
    ) {
    }
}在测试类中定义一个方法,模拟各种参数的请求信息:
   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);
    }测试类中获取ApplicationContext,准备测试请求:
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
//获取beanFactory,为了解析${} 
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 准备测试 Request
HttpServletRequest request = mockRequest();由于我们没有使用AnnotationConfigServletWebServerApplicationContext实现,不具备在初始化时收集所有 @RequestMapping 映射信息,封装为 Map(K:路径,V:HandlerMethod)的能力,
所以需要手动准备HandlerMethod:
// 要点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));
因为表单传递的参数类型默认都是String字符串,但是方法参数中的类型可能是其他,例如int,long等,所以还需要定义类型转换:
 // 要点2. 准备对象绑定与类型转换
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);还需要定义容器存储中间结果:
// 要点3. 准备 ModelAndViewContainer 用来存储中间 Model 结果
ModelAndViewContainer container = new ModelAndViewContainer();对参数值进行解析:
 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());
            String str = annotations.length() > 0 ? " @" + annotations + " " : " ";
            parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
            if (resolver.supportsParameter(parameter)) {
                // 支持此参数
                Object v = resolver.resolveArgument(parameter, container, new ServletWebRequest(request), factory);
//                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());
            }只解析了@RequestParam注解的参数值

但是可以看test中的第二个参数是没有解析到值的,该参数是隐式的使用了@RequestParam注解。RequestParamMethodArgumentResolver构造的第二个参数,如果填true,则可以进行识别。

RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory,false);在上面的代码中添加了针对@RequestParam注解参数解析器:
RequestParamMethodArgumentResolver resolver = new RequestParamMethodArgumentResolver(beanFactory,false);如果需要对剩下的注解添加解析器,可以使用组合的方式,一次性加入所有的参数解析器:
   // 多个解析器组合
            HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
            composite.addResolvers(
                    //                                          false 表示必须有 @RequestParam
                    new RequestParamMethodArgumentResolver(beanFactory, false),
                    new PathVariableMethodArgumentResolver(),
                    new RequestHeaderMethodArgumentResolver(beanFactory),
                    new ServletCookieValueMethodArgumentResolver(beanFactory),
                    new ExpressionValueMethodArgumentResolver(beanFactory),
                    new ServletRequestMethodArgumentResolver(),
                    new ServletModelAttributeMethodProcessor(false), // 必须有 @ModelAttribute
                    new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
                    new ServletModelAttributeMethodProcessor(true), // 省略了 @ModelAttribute
                    new RequestParamMethodArgumentResolver(beanFactory, true) // 省略 @RequestParam
            );后续判断是否支持参数,获取对应的参数时,只需要采用组合的对象即可,此外,加上了@ModelAttribute注解的参数,还会将模型数据放入ModelAndViewContainer中。
2、参数名的获取
在上面的案例中,能获取到参数名是因为加入了参数名解析器:
 parameter.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());下面模拟一种无法获取参数名的情况,首先创建一个类,其中有foo(String name,int age)方法:

手动进行编译,会发现参数名丢了:

那么如何才能保留参数名?可以在编译时加-parameters参数:

没有加参数时,通过javap -c -v反编译的结果,在方法上没有与参数名有关的信息:

加上参数反编译后,多了一些信息记录方法参数名称

也可以在编译时加入-g选项:

这样做反编译后会生成一个本地变量表:

两者大致的区别:MethodParameters中的信息可以通过反射获取,但是LocalVariableTable可以通过ASM获取。
3、转换接口
3.1、类型转换
在参数解析器的案例中,存在这样的情况:
@RequestParam("age") int age因为表单传递的参数类型默认都是String字符串,在案例中我们是定义了类型转换,在Spring中,类型转换又分为两套底层转换和一套高层实现:
第一套底层转换:

-  Printer 把其它类型转为 String 
-  Parser 把 String 转为其它类型 
-  Formatter 是Printer 和Parser 共同实现的接口,综合 Printer 与 Parser 功能 
-  Converter 可转换任意类型 
-  Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合 
-  FormattingConversionService 的主要作用是将一个对象从一种表示形式转换为另一种表示形式,或者将一个对象格式化为字符串形式,以便在用户界面中显示或者从用户界面中读取。 
第二套底层转换:

-  PropertyEditor 把 String 与其它类型相互转换 
-  PropertyEditorRegistry 可以注册多个 PropertyEditor 对象 
-  与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配 
高层接口:

-  SimpleTypeConverter 仅做类型转换 
-  BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,通过get()、set()方法 
-  DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,无需get()、set()方法,直接通过字段即可 
-  ServletRequestDataBinder 为 bean 的属性执行绑定(将请求参数中的信息绑定到java对象上),当需要时做类型转换,根据 directFieldAccess的布尔值选择通过get()、set()方法还是通过字段,具备校验与获取校验结果功能。 
-  上述四个接口都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式) -  首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor) 
-  再看有没有 ConversionService 转换(是第一套底层FormattingConversionService 的顶级接口) 
-  再利用默认的 PropertyEditor 转换(是第二套底层PropertyEditor的顶级接口) 
-  最后有一些特殊处理 
 
-  
SimpleTypeConverter:只有类型转换的功能
// 仅有类型转换的功能
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
Integer number = typeConverter.convertIfNecessary("13", int.class);
Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class);
System.out.println(number);
System.out.println(date);BeanWrapperImpl:为bean的属性赋值,需要bean具有get()、set()方法
 // 利用反射原理, 为 bean 的属性赋值
 MyBean target = new MyBean();
 BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
 wrapper.setPropertyValue("a", "10");
 wrapper.setPropertyValue("b", "hello");
 wrapper.setPropertyValue("c", "1999/03/04");
 System.out.println(target);DirectFieldAccessor:为 bean 的属性赋值,bean无需get()、set()方法
// 利用反射原理, 为 bean 的属性赋值
 MyBean target = new MyBean();
 DirectFieldAccessor accessor = new DirectFieldAccessor(target);
 accessor.setPropertyValue("a", "10");
 accessor.setPropertyValue("b", "hello");
 accessor.setPropertyValue("c", "1999/03/04");
 System.out.println(target);ServletRequestDataBinder:在web环境下,将请求参数中的信息绑定到java对象上
  // web 环境下数据绑定
  MyBean target = new MyBean();
  ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
  MockHttpServletRequest request = new MockHttpServletRequest();
  request.setParameter("a", "10");
  request.setParameter("b", "hello");
  request.setParameter("c", "1999/03/04");
  dataBinder.bind(new ServletRequestParameterPropertyValues(request));
  System.out.println(target);3.2、绑定器工厂
假设我们现在有两个内部类:
public static class User {
//        @DateTimeFormat(pattern = "yyyy|MM|dd")
        private Date birthday;
        private Address address;
        public Address getAddress() {
            return address;
        }
        public void setAddress(Address address) {
            this.address = address;
        }
        public Date getBirthday() {
            return birthday;
        }
        public void setBirthday(Date birthday) {
            this.birthday = birthday;
        }
        @Override
        public String toString() {
            return "User{" +
                   "birthday=" + birthday +
                   ", address=" + address +
                   '}';
        }
    }
    public static class Address {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Address{" +
                   "name='" + name + '\'' +
                   '}';
        }
    }发送模拟请求:
  MockHttpServletRequest request = new MockHttpServletRequest();
  request.setParameter("birthday", "1999|01|02");
  request.setParameter("address.name", "西安");通过默认的ServletRequestDataBinder将请求参数中的信息绑定到java对象上
  ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
  dataBinder.bind(new ServletRequestParameterPropertyValues(request));        birthday字段的值并没有被绑定上,原因在于,默认的转换器无法识别yyyy|MM|dd这样的日期格式:
这种情况就需要自定义转换器进行扩展:
在进行自定义扩展前,我们需要换一种ServletRequestDataBinder的实现方式,即使用ServletRequestDataBinderFactory,以便于加入各种扩展:
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
factory.createBinder(new ServletWebRequest(request),target,"user");注意此时是没有任何扩展功能的,依旧无法对birthday字段的值进行绑定。
ServletRequestDataBinderFactory的有参构造:
- List<InvocableHandlerMethod> binderMethods:它封装了处理程序方法的相关信息,如方法本身、所属的 Controller、方法参数等。通常与PropertyEditor转换接口配合使用
- initializer:在数据绑定过程中应用自定义的初始化逻辑。通常与ConversionService 转换接口配合使用
3.2.1、自定义转换方式一
使用第二套底层转换接口PropertyEditor
首先自定义一个类对转换器进行扩展,将来在执行factory.createBinder 时会回调该方法
@InitBinder: 用于标记一个方法,该方法用于初始化 DataBinder对象,从而自定义数据绑定的行为。在控制器类中使用 @InitBinder注解标记的方法会在控制器处理请求之前被调用,可以用来注册自定义的属性编辑器、验证器等。
 static class MyController {
        @InitBinder
        public void aaa(WebDataBinder dataBinder) {
            // 扩展 dataBinder 的转换器
            dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
        }
    }MyDateFormatter中编写了具体扩展的逻辑,实现了Formatter接口:
public class MyDateFormatter implements Formatter<Date> {
    private static final Logger log = LoggerFactory.getLogger(MyDateFormatter.class);
    private final String desc;
    public MyDateFormatter(String desc) {
        this.desc = desc;
    }
    @Override
    public String print(Date date, Locale locale) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
        return sdf.format(date);
    }
    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        log.debug(">>>>>> 进入了: {}", desc);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
        return sdf.parse(text);
    }
}
将转换器扩展类封装成InvocableHandlerMethod对象,并且新建工厂,创建绑定器:
InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(method), null);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
3.2.2、自定义转换方式二
使用第一套底层转换接口的ConversionService
FormattingConversionService service = new FormattingConversionService();
        service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
 ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
3.2.3、两种转换方式结合使用
当两种转换方式结合使用时,第二套底层转换接口PropertyEditor的优先级别更高,上文也提到过:

InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("aaa", WebDataBinder.class));
FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(method), initializer); WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));
3.2.4、使用默认方式转换
最后还可以通过默认方式进行转换:
ApplicationConversionService service = new ApplicationConversionService();
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(service);
ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);
WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
dataBinder.bind(new ServletRequestParameterPropertyValues(request));但是在实体类对应的字段上要加上
@DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;





![[机器学习系列]深入解析K-Means聚类算法:理论、实践与优化](https://img-blog.csdnimg.cn/direct/aea916edec0b4750a552cceef9a766e2.png)












