![406b5b002194f1132ec780968b744312.png](https://i-blog.csdnimg.cn/blog_migrate/68a3aa53cfd133cf242735ad4d011b98.jpeg)
前言
在剖析完 「https://dayarch.top/p/spring-boot-global-return.html 」文章之后,一直觉得有必要说明一下 Spring's Data Binding Mechanism 「Spring 数据绑定机制」。
默认情况下,Spring 只知道如何转换简单数据类型。比如我们提交的 int、String 或 boolean类型的请求数据,它会自动绑定到与之对应的 Java 类型。但在实际项目中,远远不够,因为我们可能需要绑定更复杂的对象类型。
我们需要了解 Spring 数据绑定机制,这样我们就可以更灵活的做全局配置或自定义配置,进而让我们的 RESTful API 更简洁,可读性也更好。本文依旧先通过示例代码说明实现,然后进行源码分析,带领大家了解这个机制是如何生效的,知其所以然, Let's go......
Spring 数据绑定
日期绑定
先来看下面一小段代码
@RestController@RequestMapping("/bindings/")@Slf4jpublic class BindingController { @GetMapping("/{date}") public void getSpecificDateInfo(@PathVariable LocalDateTime date) { log.info(date.toString()); }}
当我们用 Postman 请求这个 API
http://localhost:8080/rgyb/bindings/2019-12-10 12:00:00
如我们所料,抛出数据类型转换异常
![d5a1c91412719d2ea7cedb7d9c256686.png](https://i-blog.csdnimg.cn/blog_migrate/84121a6af3b2745e64e080a213d3da23.jpeg)
因为 Spring 默认不支持将 String 类型的请求参数转换为 LocalDateTime 类型,所以我们需要自定义 converter 「转换器」完整整个转换过程
自定义转换器 StringToLocalDateTimeConverter,使其实现 org.springframework.core.convert.converter.Converter 接口,在重写的 convert 方法中实现我们自定义的转换逻辑
public class StringToLocalDateTimeConverter implements Converter { @Override public LocalDateTime convert(String s) { DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.CHINESE); return LocalDateTime.parse(s, formatter); }}
将转换器注册到上下文中:
@Configurationpublic class UnifiedReturnConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToLocalDateTimeConverter()); }}
重新访问上面链接,查看控制台,按照预期得到相应转换结果:
c.e.unifiedreturn.api.BindingController : 2019-12-10T12:00
知道了这个,比如我们常用的枚举类型也可以应用这种方式做数据绑定
枚举类型绑定
同样的套路,自定义转换器
public class StringToEnumConverter implements Converter { @Override public Modes convert(String s) { return Modes.valueOf(s); }}
将其添加至上下文,请小伙伴们自行尝试吧,知道了这个,我们再也不用在 RESTful API 内部做数据转换了,我们做到了全局控制,同时让整个 API 看起来更加清晰简洁
绑定对象
在某些情况下,我们希望将数据绑定到对象,这时我们可能马上联想起来使用 @RequestBody 注解,该注解通常用于获取 POST 请求体,并将其转换相应的数据对象
在实际业务场景中,除了请求体中的数据,我们同样需要请求头中的数据,比如 token ,token 中包含当前登陆用户的信息,每一次 RESTful 请求我们都需要从 header 中获取 token 数据处理实际业务,这种场景,上文提到的 Converter 以及 @RequestBody 显然不能满足我们的需求,此时我们就要换另一种解决方案 : HandlerMethodArgumentResolver
首先我们需要自定义一个注解 LoginUser (运行时生效,作用于参数上)
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.PARAMETER)public @interface LoginUser {}
然后自定义 LoginUserArgumentResolver ,使其实现 HandlerMethodArgumentResolver 接口
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter methodParameter) { //判断参数是否有自定义注解 LoginUser 修饰 return methodParameter.hasParameterAnnotation(LoginUser.class); } @Override public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest(); LoginUserVo loginUserVo = new LoginUserVo(); String token = request.getHeader("token"); if (Strings.isNotBlank(token)){ //通常这里需要编写 token 解析逻辑,并将其放到 LoginUserVo 对象中 //logic } //在此为了快速简洁的做演示说明,省略掉解析 token 部分,直接从 header 指定 key 中获取数据 loginUserVo.setId(Long.valueOf(request.getHeader("userId"))); loginUserVo.setName(request.getHeader("userName")); return loginUserVo; }}
依旧将自定义的 LoginUserArgumentResolver 添加到上下文中
@Overridepublic void addArgumentResolvers(List resolvers) { resolvers.add(new LoginUserArgumentResolver());}
编写 API:
@GetMapping("/id")public void getLoginUserInfo(@LoginUser LoginUserVo loginUserVo) { log.info(loginUserVo.toString());}
通过 Postman 请求,在 header 中设置好相应的 K-V,如下图
http://localhost:8080/rgyb/bindings/id
![95d9c38845cd208823bf2981357033e0.png](https://i-blog.csdnimg.cn/blog_migrate/ba207180be7b36b82d3485bb30631949.jpeg)
发送请求,查看控制台,得到预期结果
c.e.unifiedreturn.api.BindingController : LoginUserVo(id=111111, name=rgyb)
相信到这里,你已经了解了基本的使用,接下来我们进行源码分析,透过现象看本质 (希望可以打开 IDE 跟着步骤查看)
Spring 数据绑定源码分析
首先我们需要了解我们自定义的 LoginUserArgumentResolver 是如何被加载到上下文中的,在你看过 https://dayarch.top/p/spring-boot-messageconverter.html 和 https://dayarch.top/p/spring-boot-global-return.html后,你也许已经有了眉目,同加载 MessageConverter 如出一辙,在 RequestMappingHandlerAdapter 类中,同样有添加 ArgumentResolver 的方法,该方法会把系统内置的 resolver 和用户自定义的 resolver 都加载到上下文中,关键代码展示如下:
private List getDefaultArgumentResolvers() { List resolvers = new ArrayList(); resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), false)); //其他内置 resolver resolvers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice)); ... ... if (this.getCustomArgumentResolvers() != null) { resolvers.addAll(this.getCustomArgumentResolvers()); } ... ... return resolvers;}
在 https://dayarch.top/p/spring-boot-messageconverter.html 文章中有一段调用栈跟踪,我再次粘贴在此处,并用红框做出标记,其实我们在分析 messageConverter 时已经悄悄的路过了我们本节要说的内容
![e2888f0a692fc9b82f7b7ab83dce37ab.png](https://i-blog.csdnimg.cn/blog_migrate/8671a93d58daf6a95492cb53ffa0b2c3.jpeg)
我们进入相应的类中瞧一瞧:
![8ba098dbd377f940b81ade4025b0615e.png](https://i-blog.csdnimg.cn/blog_migrate/23991749ccab94a2336650ada229263c.jpeg)
到这里你应该猛的了解这背后的道理了吧
接下来,我们来验证我们天天用的 @RequestBody 注解是不是这个套路呢?处理该注解的类是 RequestResponseBodyMethodProcessor,查看其类图,发现其依旧实现了 HandlerMethodArgumentResolver 接口
![60dc429cb314635dd349d42dc740f0e9.png](https://i-blog.csdnimg.cn/blog_migrate/f6ae17893691850ad4cef00514b86a97.jpeg)
打开该类,你会看到下图代码,重点地方我已标记出来
![d199a7a2370fedf6b59722f0f935ee2a.png](https://i-blog.csdnimg.cn/blog_migrate/d3acc75d011028704dfbd259fe1ba06c.jpeg)
整体处理流程如出一辙,只不过在里面调用了 messageConverter 来解析 JSON 数据。
总结
本文说的 Converter 和 ArgumentResolver 以及在 Spring MVC 中常用的 @InitBinder 注解整体过程都如出一辙,大家都可以按照这个思路来查看具体的实现。另外,在我们完成日常编码工作时,都可以从 Spring 现有的处理方式中摸索到一些解决方案,但前提是你了解 Spring 底层的一些调用过程
灵魂追问
![5f016b8fe461099f4aa17f2e0b02e2e1.png](https://i-blog.csdnimg.cn/blog_migrate/90ea00d67cd8fb2186e20f3caa87a11d.jpeg)
- 如上图所示,在追中源码时,发现HandlerMethodArgumentResolverComposite 是 HandlerMethodArgumentResolver 的实现类之一,其中有一个 Map 类型的成员变量,通常我们使用 Map,key 的类型多数为 String 类型,但看到这个 Map 中有这样的 key 你马上想到的是什么?基础面试经常会问 equals 和 hashcode 的问题,下一篇文章会借着这个类来分析说明一下你总困惑的这件小事
- 对于 Spring Boot 的整个调用过程,你能描述出整体流程吗?
- Spring 内置多少个 Resolver?你可以跟踪调试获取到
趣味原创解析Java技术栈问题,将复杂问题简单化,将抽象问题图形化落地
如果对我的专题内容感兴趣,或抢先看更多内容,欢迎访问我的博客 https://dayarch.top