如何妙用Spring 数据绑定机制?

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 如何妙用Spring 数据绑定机制?

前言


微信图片_20220510132217.gif


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


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


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


Spring 数据绑定


日期绑定


先来看下面一小段代码


@RestController
@RequestMapping("/bindings/")
@Slf4j
public 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


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


微信图片_20220510132344.jpg


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

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


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


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


@Configuration
public 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<String, Modes> {
    @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 添加到上下文中


@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


微信图片_20220510132838.jpg


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


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 时已经悄悄的路过了我们本节要说的内容


微信图片_20220510133453.jpg


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


微信图片_20220510133521.jpg


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

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

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


微信图片_20220510133540.jpg


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


微信图片_20220510133602.jpg


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


总结


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


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


灵魂追问


微信图片_20220510133624.jpg


  1. 如上图所示,在追中源码时,发现HandlerMethodArgumentResolverCompositeHandlerMethodArgumentResolver 的实现类之一,其中有一个 Map 类型的成员变量,通常我们使用 Map,key 的类型多数为 String 类型,但看到这个 Map 中有这样的 key 你马上想到的是什么?基础面试经常会问 equals 和 hashcode 的问题,下一篇文章会借着这个类来分析说明一下你总困惑的这件小事


  1. 对于 Spring Boot 的整个调用过程,你能描述出整体流程吗?


  1. Spring 内置多少个 Resolver?你可以跟踪调试获取到
相关文章
|
2月前
|
存储 运维 Java
使用 @Audited 增强Spring Boot 应用程序的数据审计能力
【7月更文挑战第19天】在Spring Boot应用中使用`@Audited`增强数据审计能力涉及在相关实体或方法上添加该注解以标记需审计的操作。例如,在`User`类的`updateUser`方法上使用`@Audited`可记录更新操作的详情。此外,还需配置审计日志存储方式(如数据库)及事件过滤规则等。这有助于满足合规性需求、故障排查及数据分析,对数据安全和完整至关重要。
|
14天前
|
存储 Java API
如何使用 Java 记录简化 Spring Data 中的数据实体
如何使用 Java 记录简化 Spring Data 中的数据实体
29 9
|
16天前
|
JSON 前端开发 Java
【Spring】“请求“ 之传递 JSON 数据
【Spring】“请求“ 之传递 JSON 数据
52 2
|
2月前
|
JSON Java API
哇塞!Spring Boot 中的 @DateTimeFormat 和 @JsonFormat,竟能引发数据时间大变革!
【8月更文挑战第29天】在Spring Boot开发中,正确处理日期时间至关重要。
48 1
|
2月前
|
Java 开发工具 Spring
Spring的Factories机制介绍
Spring的Factories机制介绍
55 1
|
2月前
|
XML JSON Java
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
本文介绍了如何使用IntelliJ IDEA和Maven搭建一个整合了Struts2、Spring4、Hibernate4的J2EE项目,并配置了项目目录结构、web.xml、welcome.jsp以及多个JSP页面,用于刷新和学习传统的SSH框架。
55 0
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
|
2月前
|
Java Spring 开发者
掌握Spring事务管理,打造无缝数据交互——实用技巧大公开!
【8月更文挑战第31天】在企业应用开发中,确保数据一致性和完整性至关重要。Spring框架提供了强大的事务管理机制,包括`@Transactional`注解和编程式事务管理,简化了事务处理。本文深入探讨Spring事务管理的基础知识与高级技巧,涵盖隔离级别、传播行为、超时时间等设置,并介绍如何使用`TransactionTemplate`和`PlatformTransactionManager`进行编程式事务管理。通过合理设计事务范围和选择合适的隔离级别,可以显著提高应用的稳定性和性能。掌握这些技巧,有助于开发者更好地应对复杂业务需求,提升应用质量和可靠性。
39 0
|
2月前
|
JSON 前端开发 Java
Spring MVC返回JSON数据
综上所述,Spring MVC提供了灵活、强大的方式来支持返回JSON数据,从直接使用 `@ResponseBody`及 `@RestController`注解,到通过配置消息转换器和异常处理器,开发人员可以根据具体需求选择合适的实现方式。
130 4
|
2月前
|
存储 Java 数据库
使用 @Audited 增强Spring Boot 应用程序的数据审计能力
【8月更文挑战第3天】在Spring Boot应用中,`@Audited`注解能显著提升数据审计能力。它可用于标记需审计的方法或类,记录操作用户、时间和类型等信息。此注解支持与Logback或Log4j等日志框架集成,亦可将审计信息存入数据库,便于后续分析。此外,还支持自定义审计处理器以满足特定需求。
120 0
|
3月前
|
安全 Java API
构建基于Spring Boot的REST API安全机制
构建基于Spring Boot的REST API安全机制