spring高级篇(五)

news/2025/2/21 13:51:44/

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=18A21.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 表示必须有 @RequestParamnew RequestParamMethodArgumentResolver(beanFactory, false),new PathVariableMethodArgumentResolver(),new RequestHeaderMethodArgumentResolver(beanFactory),new ServletCookieValueMethodArgumentResolver(beanFactory),new ExpressionValueMethodArgumentResolver(beanFactory),new ServletRequestMethodArgumentResolver(),new ServletModelAttributeMethodProcessor(false), // 必须有 @ModelAttributenew RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),new ServletModelAttributeMethodProcessor(true), // 省略了 @ModelAttributenew 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;}@Overridepublic 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;}@Overridepublic 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 {@InitBinderpublic 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;}@Overridepublic String print(Date date, Locale locale) {SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");return sdf.format(date);}@Overridepublic 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;


http://www.ppmy.cn/news/1456760.html

相关文章

MySQL学习笔记9——触发器和权限管理

触发器和权限管理 一、触发器1、如何操作触发器2、触发器的优缺点 二、权限管理1、角色的作用2、角色的操作3、用户的操作 一、触发器 当商品信息和库存信息分别存放在两个不同的数据表中时&#xff0c;可以创建一个触发器&#xff0c; 让商品信息数据的插入操作自动触发库存数…

Python面向对象编程思想的深入学习

魔术方法的使用 案例体验 class Student:def __init__(self, name, age):self.name nameself.age age# __str__魔术方法, 如果不去写这个方法&#xff0c;那么print输出的则是信息存储的内存地址。def __str__(self):return fStudent类对象&#xff0c;name:{self.name}, ag…

用PyTorch实现卷积神经网络解决FashionMNIST分类挑战

其他项目(购买专栏任意项目一对一指导) 基于yolov8+LPRNet的车牌识别项目用PyTorch解决FashionMNIST分类挑战cnn FashionMNIST分类 前言一、FashionMNIST:从手写数字到时尚元素二、构建卷积神经网络三、超参数选择与优化方式四、训练结果总结与不足前言 在当前的机器学习领…

Linux--基础IO(文件描述符fd)

目录 1.回顾一下文件 2.理解文件 下面就是系统调用的文件操作 文件描述符fd&#xff0c;fd的本质是什么&#xff1f; 读写文件与内核级缓存区的关系 据上理论我们就可以知道&#xff1a;open在干什么 3.理解Linux一切皆文件 4.C语言中的FILE* 1.回顾一下文件 先来段代码…

Resilience的限流机制

常见的限流算法 漏桶算法 一个固定容量的漏桶&#xff0c;按照设定常量固定速率流出水滴&#xff0c;类似医院打吊针&#xff0c;不管你源头流量多大&#xff0c;我设定匀速流出。 如果流入水滴超出了桶的容量&#xff0c;则流入的水滴将会溢出了(被丢弃)&#xff0c;而漏桶容…

Java性能优化(五)-多线程调优-Lock同步锁的优化

作者主页&#xff1a; &#x1f517;进朱者赤的博客 精选专栏&#xff1a;&#x1f517;经典算法 作者简介&#xff1a;阿里非典型程序员一枚 &#xff0c;记录在大厂的打怪升级之路。 一起学习Java、大数据、数据结构算法&#xff08;公众号同名&#xff09; ❤️觉得文章还…

Git系列:git push (-u) 与 git branch (-u)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

在Python中安装和使用pandas库

在Python中安装和使用pandas库是一个相对简单的过程。以下是具体的步骤&#xff1a; 安装pandas库 你可以使用Python的包管理器pip来安装pandas。打开你的命令行工具&#xff08;在Windows上可能是CMD或PowerShell&#xff0c;在macOS或Linux上可能是Terminal&#xff09;&am…