前言
在剖析完 「Spring Boot 統(tǒng)一數(shù)據(jù)格式是怎么實現(xiàn)的? 」文章之后,一直覺得有必要說明一下 Spring's Data Binding Mechanism 「Spring 數(shù)據(jù)綁定機制」。
默認情況下,Spring 只知道如何轉換簡單數(shù)據(jù)類型。比如我們提交的 int、String 或 boolean類型的請求數(shù)據(jù),它會自動綁定到與之對應的 Java 類型。但在實際項目中,遠遠不夠,因為我們可能需要綁定更復雜的對象類型。
我們需要了解 Spring 數(shù)據(jù)綁定機制,這樣我們就可以更靈活的做全局配置或自定義配置,進而讓我們的 RESTful API 更簡潔,可讀性也更好。本文依舊先通過示例代碼說明實現(xiàn),然后進行源碼分析,帶領大家了解這個機制是如何生效的,知其所以然, Let's go......
Spring 數(shù)據(jù)綁定
日期綁定
先來看下面一小段代碼
@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
如我們所料,拋出數(shù)據(jù)類型轉換異常

因為 Spring 默認不支持將 String 類型的請求參數(shù)轉換為 LocalDateTime 類型,所以我們需要自定義 converter 「轉換器」完整整個轉換過程
自定義轉換器 StringToLocalDateTimeConverter,使其實現(xiàn) org.springframework.core.convert.converter.Converter<S, T> 接口,在重寫的 convert 方法中實現(xiàn)我們自定義的轉換邏輯
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
知道了這個,比如我們常用的枚舉類型也可以應用這種方式做數(shù)據(jù)綁定
枚舉類型綁定
同樣的套路,自定義轉換器
public class StringToEnumConverter implements Converter<String, Modes> {
@Override
public Modes convert(String s) {
return Modes.valueOf(s);
}
}
將其添加至上下文,請小伙伴們自行嘗試吧,知道了這個,我們再也不用在 RESTful API 內(nèi)部做數(shù)據(jù)轉換了,我們做到了全局控制,同時讓整個 API 看起來更加清晰簡潔
綁定對象
在某些情況下,我們希望將數(shù)據(jù)綁定到對象,這時我們可能馬上聯(lián)想起來使用 @RequestBody 注解,該注解通常用于獲取 POST 請求體,并將其轉換相應的數(shù)據(jù)對象
在實際業(yè)務場景中,除了請求體中的數(shù)據(jù),我們同樣需要請求頭中的數(shù)據(jù),比如 token ,token 中包含當前登陸用戶的信息,每一次 RESTful 請求我們都需要從 header 中獲取 token 數(shù)據(jù)處理實際業(yè)務,這種場景,上文提到的 Converter 以及 @RequestBody 顯然不能滿足我們的需求,此時我們就要換另一種解決方案 : HandlerMethodArgumentResolver
首先我們需要自定義一個注解 LoginUser (運行時生效,作用于參數(shù)上)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface LoginUser {
}
然后自定義 LoginUserArgumentResolver ,使其實現(xiàn) HandlerMethodArgumentResolver 接口
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//判斷參數(shù)是否有自定義注解 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 中獲取數(shù)據(jù)
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

發(fā)送請求,查看控制臺,得到預期結果
c.e.unifiedreturn.api.BindingController : LoginUserVo(id=111111, name=rgyb)
相信到這里,你已經(jīng)了解了基本的使用,接下來我們進行源碼分析,透過現(xiàn)象看本質(zhì) (希望可以打開 IDE 跟著步驟查看)
Spring 數(shù)據(jù)綁定源碼分析
首先我們需要了解我們自定義的 LoginUserArgumentResolver 是如何被加載到上下文中的,在你看過 HttpMessageConverter轉換原理解析 和 Springboot返回統(tǒng)一JSON數(shù)據(jù)格式是怎么實現(xiàn)的?后,你也許已經(jīng)有了眉目,同加載 MessageConverter 如出一轍,在 RequestMappingHandlerAdapter 類中,同樣有添加 ArgumentResolver 的方法,該方法會把系統(tǒng)內(nèi)置的 resolver 和用戶自定義的 resolver 都加載到上下文中,關鍵代碼展示如下:
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList();
resolvers.add(new RequestParamMethodArgumentResolver(this.getBeanFactory(), false));
//其他內(nèi)置 resolver
resolvers.add(new RequestResponseBodyMethodProcessor(this.getMessageConverters(), this.requestResponseBodyAdvice));
...
...
if (this.getCustomArgumentResolvers() != null) {
resolvers.addAll(this.getCustomArgumentResolvers());
}
...
...
return resolvers;
}
在 HttpMessageConverter轉換原理解析 文章中有一段調(diào)用棧跟蹤,我再次粘貼在此處,并用紅框做出標記,其實我們在分析 messageConverter 時已經(jīng)悄悄的路過了我們本節(jié)要說的內(nèi)容

我們進入相應的類中瞧一瞧:

到這里你應該猛的了解這背后的道理了吧
接下來,我們來驗證我們天天用的 @RequestBody 注解是不是這個套路呢?
處理該注解的類是 RequestResponseBodyMethodProcessor,查看其類圖,發(fā)現(xiàn)其依舊實現(xiàn)了 HandlerMethodArgumentResolver 接口

打開該類,你會看到下圖代碼,重點地方我已標記出來

整體處理流程如出一轍,只不過在里面調(diào)用了 messageConverter 來解析 JSON 數(shù)據(jù)。
總結
本文說的 Converter 和 ArgumentResolver 以及在 Spring MVC 中常用的 @InitBinder 注解整體過程都如出一轍,大家都可以按照這個思路來查看具體的實現(xiàn)。另外,在我們完成日常編碼工作時,都可以從 Spring 現(xiàn)有的處理方式中摸索到一些解決方案,但前提是你了解 Spring 底層的一些調(diào)用過程
最后希望小伙伴打開 IDE 切實查看相應代碼,你一定還會有新發(fā)現(xiàn),我們可以一起探討。本文代碼已上傳,公眾號回復「demo」,打開鏈接查看 「spring-boot-unified-return」文件夾內(nèi)容即可,也可以順路回顧以前 Spring Boot 統(tǒng)一返回格式的代碼實現(xiàn)
靈魂追問

- 如上圖所示,在追中源碼時,發(fā)現(xiàn)
HandlerMethodArgumentResolverComposite是HandlerMethodArgumentResolver的實現(xiàn)類之一,其中有一個 Map 類型的成員變量,通常我們使用 Map,key 的類型多數(shù)為 String 類型,但看到這個 Map 中有這樣的 key 你馬上想到的是什么?基礎面試經(jīng)常會問 equals 和 hashcode 的問題,下一篇文章會借著這個類來分析說明一下你總困惑的這件小事 - 對于 Spring Boot 的整個調(diào)用過程,你能描述出整體流程嗎?
- Spring 內(nèi)置多少個 Resolver?你可以跟蹤調(diào)試獲取到
歡迎持續(xù)關注公眾號:「日拱一兵」
- 前沿 Java 技術干貨分享
- 高效工具匯總 | 回復「工具」
- 面試問題分析與解答
- 技術資料領取 | 回復「資料」
以讀偵探小說思維輕松趣味學習 Java 技術棧相關知識,本著將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續(xù)更新,請持續(xù)關注......
