How Magical Spring data binding mechanism?

Foreword

In the complete analysis, " Spring Boot unified data format is how to achieve? After" article, always felt the need to explain Spring's Data Binding Mechanism "Spring data binding mechanism."

By default, Spring only know how to convert simple data types. For example, int, String or boolean types of data we submitted the request, it will automatically be bound to the corresponding Java type. But in actual projects, not enough, because we may need to bind more complex object types.

We need to understand the Spring data binding mechanism, so that we can do a more flexible global configuration or custom configuration, thereby allowing our RESTful API more concise, readable and better. This article first through sample code that illustrates still achieve, and then source code analysis, and lead us to understand how this mechanism is in effect, know why, Let's go ......

Spring Data Binding

Date Bind

First look at the following piece of code

@RestController
@RequestMapping("/bindings/")
@Slf4j
public class BindingController {


    @GetMapping("/{date}")
    public void getSpecificDateInfo(@PathVariable LocalDateTime date) {
        log.info(date.toString());
    }
}

When we use this API request Postman

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

As we expected, the data type conversion throws an exception

because the default does not support the conversion of Spring String type of request parameters LocalDateTime type, so we need a custom converter "converter" complete the whole conversion process

Custom converter StringToLocalDateTimeConverter, so as to achieve org.springframework.core.convert.converter.Converter<S, T>the interface, we realize custom logic convert conversion method overridden

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

Is converted into the context register:

@Configuration
public class UnifiedReturnConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToLocalDateTimeConverter());
    }
}

Re-visit the link above to view the console, the corresponding conversion results as expected:

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

Know this, such as we commonly enumerated type can also be used in this way do data binding

Enumeration type binding

The same routine, custom converters

public class StringToEnumConverter implements Converter<String, Modes> {
    
    @Override
    public Modes convert(String s) {
        return Modes.valueOf(s);
    }
}

Add it to the context, please try it on their own little friends, know this, we no longer have to do the conversion data within the RESTful API, we did global control, while allowing the entire API looks more clear and concise

Bound object

在某些情况下,我们希望将数据绑定到对象,这时我们可能马上联想起来使用 @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 添加到上下文中

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> 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

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

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

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

Spring 数据绑定源码分析

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

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    List<HandlerMethodArgumentResolver> 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;
}

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 技术栈相关知识,本着将复杂问题简单化,抽象问题具体化和图形化原则逐步分解技术问题,技术持续更新,请持续关注......


Guess you like

Origin www.cnblogs.com/FraserYu/p/12047279.html