前言
在剖析完 Spring Boot 返回統一數據格式是怎樣實現的?文章以後,一直以爲有必要說明一下 Spring's Data Binding Mechanism 「Spring 數據綁定機制」。
默認狀況下,Spring 只知道如何轉換簡單數據類型。好比咱們提交的 int、String 或 boolean類型的請求數據,它會自動綁定到與之對應的 Java 類型。但在實際項目中,遠遠不夠,由於咱們可能須要綁定更復雜的對象類型。
咱們須要瞭解 Spring 數據綁定機制,這樣咱們就能夠更靈活的作全局配置或自定義配置,進而讓咱們的 RESTful API 更簡潔,可讀性也更好。本文依舊先經過示例代碼說明實現,而後進行源碼分析,帶領你們瞭解這個機制是如何生效的,知其因此然, Let's go......
Spring 數據綁定
日期綁定web
先來看下面一小段代碼面試
@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
如咱們所料,拋出數據類型轉換異常
spring
由於 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); } }
將轉換器註冊到上下文中:api
@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
app
知道了這個,好比咱們經常使用的枚舉類型也能夠應用這種方式作數據綁定
枚舉類型綁定ide
一樣的套路,自定義轉換器spring-boot
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 跟着步驟查看)
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 統一返回格式的代碼實現
爲了更好的回答小夥伴們的問題,同時與你們更好的交流學習,在公衆號菜單上添加了個人我的微信號二維碼,有須要的小夥伴們能夠加我微信
靈魂追問
tan日拱一兵轉發在看也很贊鐘意做者