前言

在剖析完 「Spring Boot 统一数据格式是怎么实现的? 」文章之后,一直觉得有必要说明一下 Spring's Data Binding Mechanism 「Spring 数据绑定机制」。

默认情况下,Spring 只知道如何转换简单数据类型。比如我们提交的 int、String 或 boolean类型的请求数据,它会自动绑定到与之对应的 Java 类型。但在实际项目中,远远不够,因为我们可能需要绑定更复杂的对象类型。

我们需要了解 Spring 数据绑定机制,这样我们就可以更灵活的做全局配置或自定义配置,进而让我们的 RESTful API 更简洁,可读性也更好。本文依旧先通过示例代码说明实现,然后进行源码分析,带领大家了解这个机制是如何生效的,知其所以然, Let's go......

Spring 数据绑定

日期绑定

先来看下面一小段代码

  1. @RestController
  2. @RequestMapping("/bindings/")
  3. @Slf4j
  4. public class BindingController {
  5. @GetMapping("/{date}")
  6. public void getSpecificDateInfo(@PathVariable LocalDateTime date) {
  7. log.info(date.toString());
  8. }
  9. }

当我们用 Postman 请求这个 API

  1. http://localhost:8080/rgyb/bindings/2019-12-10 12:00:00

如我们所料,抛出数据类型转换异常



因为 Spring 默认不支持将 String 类型的请求参数转换为 LocalDateTime 类型,所以我们需要自定义 converter 「转换器」完整整个转换过程

自定义转换器 StringToLocalDateTimeConverter,使其实现 org.springframework.core.convert.converter.Converter<S, T> 接口,在重写的 convert 方法中实现我们自定义的转换逻辑

  1. public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
  2. @Override
  3. public LocalDateTime convert(String s) {
  4. DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINESE);
  5. return LocalDateTime.parse(s, formatter);
  6. }
  7. }

将转换器注册到上下文中:

  1. @Configuration
  2. public class UnifiedReturnConfig implements WebMvcConfigurer {
  3. @Override
  4. public void addFormatters(FormatterRegistry registry) {
  5. registry.addConverter(new StringToLocalDateTimeConverter());
  6. }
  7. }

重新访问上面链接,查看控制台,按照预期得到相应转换结果:

  1. c.e.unifiedreturn.api.BindingController : 2019-12-10T12:00

知道了这个,比如我们常用的枚举类型也可以应用这种方式做数据绑定

枚举类型绑定

同样的套路,自定义转换器

  1. public class StringToEnumConverter implements Converter<String, Modes> {
  2. @Override
  3. public Modes convert(String s) {
  4. return Modes.valueOf(s);
  5. }
  6. }

将其添加至上下文,请小伙伴们自行尝试吧,知道了这个,我们再也不用在 RESTful API 内部做数据转换了,我们做到了全局控制,同时让整个 API 看起来更加清晰简洁

绑定对象

在某些情况下,我们希望将数据绑定到对象,这时我们可能马上联想起来使用 @RequestBody 注解,该注解通常用于获取 POST 请求体,并将其转换相应的数据对象

在实际业务场景中,除了请求体中的数据,我们同样需要请求头中的数据,比如 token ,token 中包含当前登陆用户的信息,每一次 RESTful 请求我们都需要从 header 中获取 token 数据处理实际业务,这种场景,上文提到的 Converter 以及 @RequestBody 显然不能满足我们的需求,此时我们就要换另一种解决方案 : HandlerMethodArgumentResolver

首先我们需要自定义一个注解 LoginUser (运行时生效,作用于参数上)

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.PARAMETER)
  3. public @interface LoginUser {
  4. }

然后自定义 LoginUserArgumentResolver ,使其实现 HandlerMethodArgumentResolver 接口

  1. public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
  2. @Override
  3. public boolean supportsParameter(MethodParameter methodParameter) {
  4. //判断参数是否有自定义注解 LoginUser 修饰
  5. return methodParameter.hasParameterAnnotation(LoginUser.class);
  6. }
  7. @Override
  8. public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
  9. HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();
  10. LoginUserVo loginUserVo = new LoginUserVo();
  11. String token = request.getHeader("token");
  12. if (Strings.isNotBlank(token)){
  13. //通常这里需要编写 token 解析逻辑,并将其放到 LoginUserVo 对象中
  14. //logic
  15. }
  16. //在此为了快速简洁的做演示说明,省略掉解析 token 部分,直接从 header 指定 key 中获取数据
  17. loginUserVo.setId(Long.valueOf(request.getHeader("userId")));
  18. loginUserVo.setName(request.getHeader("userName"));
  19. return loginUserVo;
  20. }
  21. }

依旧将自定义的 LoginUserArgumentResolver 添加到上下文中

  1. @Override
  2. public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
  3. resolvers.add(new LoginUserArgumentResolver());
  4. }

编写 API:

  1. @GetMapping("/id")
  2. public void getLoginUserInfo(@LoginUser LoginUserVo loginUserVo) {
  3. log.info(loginUserVo.toString());
  4. }

通过 Postman 请求,在 header 中设置好相应的 K-V,如下图

  1. http://localhost:8080/rgyb/bindings/id

发送请求,查看控制台,得到预期结果

  1. c.e.unifiedreturn.api.BindingController : LoginUserVo(id=111111, name=rgyb)

相信到这里,你已经了解了基本的使用,接下来我们进行源码分析,透过现象看本质 (希望可以打开 IDE 跟着步骤查看)

Spring 数据绑定源码分析

首先我们需要了解我们自定义的 LoginUserArgumentResolver 是如何被加载到上下文中的,在你看过 HttpMessageConverter转换原理解析Springboot返回统一JSON数据格式是怎么实现的?后,你也许已经有了眉目,同加载 MessageConverter 如出一辙,在 RequestMappingHandlerAdapter 类中,同样有添加 ArgumentResolver 的方法,该方法会把系统内置的 resolver 和用户自定义的 resolver 都加载到上下文中,关键代码展示如下:

  1. private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
  2. List<HandlerMethodArgumentResolver> resolvers = new ArrayList();
  3. resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), false));
  4. //其他内置 resolver
  5. resolvers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice));
  6. ...
  7. ...
  8. if (this.getCustomArgumentResolvers() != null) {
  9. resolvers.addAll(this.getCustomArgumentResolvers());
  10. }
  11. ...
  12. ...
  13. return resolvers;
  14. }

HttpMessageConverter转换原理解析 文章中有一段调用栈跟踪,我再次粘贴在此处,并用红框做出标记,其实我们在分析 messageConverter 时已经悄悄的路过了我们本节要说的内容

我们进入相应的类中瞧一瞧:

到这里你应该猛的了解这背后的道理了吧

接下来,我们来验证我们天天用的 @RequestBody 注解是不是这个套路呢?

处理该注解的类是 RequestResponseBodyMethodProcessor,查看其类图,发现其依旧实现了 HandlerMethodArgumentResolver 接口

打开该类,你会看到下图代码,重点地方我已标记出来

整体处理流程如出一辙,只不过在里面调用了 messageConverter 来解析 JSON 数据。

总结

本文说的 Converter 和 ArgumentResolver 以及在 Spring MVC 中常用的 @InitBinder 注解整体过程都如出一辙,大家都可以按照这个思路来查看具体的实现。另外,在我们完成日常编码工作时,都可以从 Spring 现有的处理方式中摸索到一些解决方案,但前提是你了解 Spring 底层的一些调用过程

最后希望小伙伴打开 IDE 切实查看相应代码,你一定还会有新发现,我们可以一起探讨。本文代码已上传,公众号回复「demo」,打开链接查看 「spring-boot-unified-return」文件夹内容即可,也可以顺路回顾以前 Spring Boot 统一返回格式的代码实现


灵魂追问

  1. 如上图所示,在追中源码时,发现HandlerMethodArgumentResolverCompositeHandlerMethodArgumentResolver 的实现类之一,其中有一个 Map 类型的成员变量,通常我们使用 Map,key 的类型多数为 String 类型,但看到这个 Map 中有这样的 key 你马上想到的是什么?基础面试经常会问 equals 和 hashcode 的问题,下一篇文章会借着这个类来分析说明一下你总困惑的这件小事
  2. 对于 Spring Boot 的整个调用过程,你能描述出整体流程吗?
  3. Spring 内置多少个 Resolver?你可以跟踪调试获取到

欢迎持续关注公众号:「日拱一兵」

  • 前沿 Java 技术干货分享
  • 高效工具汇总 | 回复「工具」
  • 面试问题分析与解答
  • 技术资料领取 | 回复「资料」

以读侦探小说思维轻松趣味学习 Java 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......


如何妙用Spring 数据绑定机制?的更多相关文章

  1. Spring MVC—数据绑定机制,数据转换,数据格式化配置,数据校验

    Spring MVC数据绑定机制 数据转换 Spring MVC处理JSON 数据格式化配置使用 数据校验 数据校验 Spring MVC数据绑定机制 Spring MVC解析JSON格式的数据: 步 ...

  2. Spring 事务机制详解

    原文出处: 陶邦仁 Spring事务机制主要包括声明式事务和编程式事务,此处侧重讲解声明式事务,编程式事务在实际开发中得不到广泛使用,仅供学习参考. Spring声明式事务让我们从复杂的事务处理中得到 ...

  3. spring工作机制

    Hibernate.struts,还差一个spring 就一起发出去.. spring工作机制及为什么要用? 1.springmvc请所有的请求都提交给DispatcherServlet,它会委托应用 ...

  4. 剖析WPF数据绑定机制

    引言 WPF框架采取的是MVVM模式,也就是数据驱动UI,UI控件(Controls)被严格地限制在表示层内,不会参与业务逻辑的处理,只是通过数据绑定(Data Binding)简单忠实地表达与之绑定 ...

  5. 再谈angularJS数据绑定机制及背后原理—angularJS常见问题总结

    这篇是对angularJS的一些疑点回顾,是对目前angularJS开发的各种常见问题的整理汇总.如果对文中的题目全部了然于胸,觉得对整个angular框架应该掌握的七七八八了.希望志同道合的通知补充 ...

  6. Hibernate工作原理及为什么要用?. Struts工作机制?为什么要使用Struts? spring工作机制及为什么要用?

    三大框架是用来开发web应用程序中使用的.Struts:基于MVC的充当了其中的试图层和控制器Hibernate:做持久化的,对JDBC轻量级的封装,使得我们能过面向对象的操作数据库Spring: 采 ...

  7. Spring 事务机制详解(事务的隔离性和传播性)

    原文出处: 陶邦仁 Spring事务机制主要包括声明式事务和编程式事务,此处侧重讲解声明式事务,编程式事务在实际开发中得不到广泛使用,仅供学习参考. Spring声明式事务让我们从复杂的事务处理中得到 ...

  8. AngularJS 作用域与数据绑定机制

    AngularJS 简介 AngularJS 是由 Google 发起的一款开源的前端 MVC 脚本框架,既适合做普通 WEB 应用也可以做 SPA(单页面应用,所有的用户操作都在一个页面中完成).与 ...

  9. Spring事件机制详解

    一.前言 说来惭愧,对应Spring事件机制之前只知道实现 ApplicationListener 接口,就可以基于Spring自带的事件做一些事情(如ContextRefreshedEvent),但 ...

随机推荐

  1. mui 底部导航栏的实现

    mui 底部导航栏的实现 <nav class="mui-bar mui-bar-tab"> <a id="display" class=&q ...

  2. hdu 1863 畅通工程 (并查集 、 kruskal)

    畅通工程Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submiss ...

  3. 插入订单并且输出订单号的sql存储过程

    --插入订单-- create proc InsertOrders ( @OrderNumber varchar(300), @OrderState varchar(30), @OrderType v ...

  4. 022.掌握Pod-Pod升级和回滚

    一 deploymentPod升级和回滚 1.1 deployment升级 若Pod是通过Deployment创建的,可以在运行时修改Deployment的Pod定义(spec.template)或镜 ...

  5. Pashmak and Buses(构造)

    题目链接:http://codeforces.com/problemset/problem/459/C 题意:n个人, k辆车, d天,每天将所有 任意人安排到k辆车, 问怎样安排, 可时不存在 2人 ...

  6. [学习笔记] 在Windows 10上安装 WebLogic 12.2.1.3

    本文适用于学习目的而撰写.截止目前WebLogic已经有12.2.1.4.0了. 在安装WebLogic 12.2.1.3.0 首先要在Windows10之上Oracle JDK 1.8.  当前认证 ...

  7. python描述:链表

    单链表结构: 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点 ...

  8. Nginx 跨域代理

    安装环境: win10 nginx-1.17.2 安装: 在写前端调用后台接口时,报了跨域的错误. 调试地址:http://localhost:5500/demo/encAjax.html 接口地址: ...

  9. MySQL锁会不会,你就差看一看

    数据库锁知识 不少人在开发的时候,应该很少会注意到这些锁的问题,也很少会给程序加锁(除了库存这些对数量准确性要求极高的情况下),即使我们不会这些锁知识,我们的程序在一般情况下还是可以跑得好好的.因为这 ...

  10. Hash Map 在java中的解释及示例

    目录 HashMap在java中的应用及示例 HashMap的内部结构 HashMap的性能 同步HashMap HashMap的构造函数 HashMap的时间复杂度 HashMap的方法 1. vo ...