在剖析完 「Spring Boot 統一數據格式是怎麼實現的? 」文章以後,一直以爲有必要說明一下 Spring's Data Binding Mechanism 「Spring 數據綁定機制」。html
默認狀況下,Spring 只知道如何轉換簡單數據類型。好比咱們提交的 int、String 或 boolean類型的請求數據,它會自動綁定到與之對應的 Java 類型。但在實際項目中,遠遠不夠,由於咱們可能須要綁定更復雜的對象類型。java
咱們須要瞭解 Spring 數據綁定機制,這樣咱們就能夠更靈活的作全局配置或自定義配置,進而讓咱們的 RESTful API 更簡潔,可讀性也更好。本文依舊先經過示例代碼說明實現,而後進行源碼分析,帶領你們瞭解這個機制是如何生效的,知其因此然, Let's go......web
先來看下面一小段代碼面試
@RestController @RequestMapping("/bindings/") @Slf4j public class BindingController { @GetMapping("/{date}") public void getSpecificDateInfo(@PathVariable LocalDateTime date) { log.info(date.toString()); } }
當咱們用 Postman 請求這個 APIspring
http://localhost:8080/rgyb/bindings/2019-12-10 12:00:00
如咱們所料,拋出數據類型轉換異常
由於 Spring 默認不支持將 String 類型的請求參數轉換爲 LocalDateTime 類型,因此咱們須要自定義 converter 「轉換器」完整整個轉換過程shell
自定義轉換器 StringToLocalDateTimeConverter
,使其實現 org.springframework.core.convert.converter.Converter<S, T>
接口,在重寫的 convert 方法中實現咱們自定義的轉換邏輯segmentfault
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); } }
將轉換器註冊到上下文中:api
@Configuration public class UnifiedReturnConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToLocalDateTimeConverter()); } }
從新訪問上面連接,查看控制檯,按照預期獲得相應轉換結果:app
c.e.unifiedreturn.api.BindingController : 2019-12-10T12:00
知道了這個,好比咱們經常使用的枚舉類型也能夠應用這種方式作數據綁定ide
一樣的套路,自定義轉換器
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
發送請求,查看控制檯,獲得預期結果
c.e.unifiedreturn.api.BindingController : LoginUserVo(id=111111, name=rgyb)
相信到這裏,你已經瞭解了基本的使用,接下來咱們進行源碼分析,透過現象看本質 (但願能夠打開 IDE 跟着步驟查看)
首先咱們須要瞭解咱們自定義的 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 統一返回格式的代碼實現
HandlerMethodArgumentResolverComposite
是 HandlerMethodArgumentResolver
的實現類之一,其中有一個 Map 類型的成員變量,一般咱們使用 Map,key 的類型多數爲 String 類型,但看到這個 Map 中有這樣的 key 你立刻想到的是什麼?基礎面試常常會問 equals 和 hashcode 的問題,下一篇文章會藉着這個類來分析說明一下你總困惑的這件小事歡迎關注個人公衆號 「日拱一兵」,趣味原創解析Java技術棧問題,將複雜問題簡單化,將抽象問題圖形化落地
若是對個人專題內容感興趣,或搶先看更多內容,歡迎訪問個人博客 dayarch.top