Spring底层入门(五)

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注解的参数值

c6db9e18f2d347e5952397beb65ac6a9.png

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

986676a6033844e39ff4a39eca989ff3.png

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)方法:

c852c12f7b0645dcb967c2beb35bea57.png

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

b1dfa532bdcd414bbc5898724994854f.png

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

0b681051aa2a439aa91cb11b2183ce7f.png

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

a7f4e6690f794e1a93a42abdccba0fa2.png

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

29f4735c9fac42d0a4de14bd3df7b06f.png

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

a629a03722364fab90e9a509c8fed286.png

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

d71c39e1465e4ec98e4d3e1f6c3c179f.png

两者大致的区别:MethodParameters中的信息可以通过反射获取,但是LocalVariableTable可以通过ASM获取。

3、转换接口

3.1、类型转换

        在参数解析器的案例中,存在这样的情况:

@RequestParam("age") int age

        因为表单传递的参数类型默认都是String字符串,在案例中我们是定义了类型转换,在Spring中,类型转换又分为两套底层转换和一套高层实现:

        第一套底层转换:

ea00c69ba7584bb2907fea62ec8262ef.png

        

  • Printer 把其它类型转为 String

  • Parser 把 String 转为其它类型

  • Formatter 是Printer 和Parser 共同实现的接口,综合 Printer 与 Parser 功能

  • Converter 可转换任意类型

  • Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合

  • FormattingConversionService 的主要作用是将一个对象从一种表示形式转换为另一种表示形式,或者将一个对象格式化为字符串形式,以便在用户界面中显示或者从用户界面中读取。

        第二套底层转换:

fdaaf351258a42ebb77f32c26da097bf.png

  • PropertyEditor 把 String 与其它类型相互转换

  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象

  • 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配

        高层接口:

c46482b21a3a414eaf450b3bb364ec4b.png

  • 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这样的日期格式:
c155dbcb01214eae8906766c49b99c50.png

         这种情况就需要自定义转换器进行扩展:

         在进行自定义扩展前,我们需要换一种ServletRequestDataBinder的实现方式,即使用ServletRequestDataBinderFactory,以便于加入各种扩展,选择使用何种转换方式:

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

        然后利用工厂去创建DataBinder:

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注解标记的方法会在控制器处理请求之前被调用,可以用来注册自定义的属性编辑器、验证器等。(将factory创建的DataBinder作为参数传递):

 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);
    }


}

        将标注了@InitBinder的方法封装成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));

6b0413fb93774e838a572629c5919c38.png

        底层是调用的PropertyEditor(适配器模式)

3.2.2、自定义转换方式二

       使用第一套底层转换接口的ConversionService配合formatter:

        创建ConversionService对象,指定转换器:

FormattingConversionService service = new FormattingConversionService();
service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));

         将转换器包装成ConfigurableWebBindingInitializer类型:

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));

d5057d7f0ec543dfa05f6f7be40f8a5d.png

 3.2.3、两种转换方式结合使用

        当两种转换方式结合使用时,第二套底层转换接口PropertyEditor的优先级别更高,上文也提到过:

919a4ac044364eb7893e34faf50312a1.png

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));

90433eeba1724c19a8524b02c0fa800c.png

3.2.4、使用默认方式转换

        最后还可以通过默认方式进行转换:

        和自定义转换方式二的区别在于创建了ConversionService的DefaultFormattingConversionService实现,并且无需加入自定义的转换器类

        在Spring boot中是ApplicationConversionService实现

DefaultFormattingConversionService service = new DefaultFormattingConversionService();
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;

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值