如何妙用Spring 數據綁定機制?

前言

在剖析完 「Spring Boot 統一數據格式是怎麼實現的? 」文章以後,一直以爲有必要說明一下 Spring's Data Binding Mechanism 「Spring 數據綁定機制」。html

默認狀況下,Spring 只知道如何轉換簡單數據類型。好比咱們提交的 int、String 或 boolean類型的請求數據,它會自動綁定到與之對應的 Java 類型。但在實際項目中,遠遠不夠,由於咱們可能須要綁定更復雜的對象類型。java

咱們須要瞭解 Spring 數據綁定機制,這樣咱們就能夠更靈活的作全局配置或自定義配置,進而讓咱們的 RESTful API 更簡潔,可讀性也更好。本文依舊先經過示例代碼說明實現,而後進行源碼分析,帶領你們瞭解這個機制是如何生效的,知其因此然, Let's go......web

Spring 數據綁定

日期綁定

先來看下面一小段代碼面試

@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
複製代碼

如咱們所料,拋出數據類型轉換異常 shell

由於 Spring 默認不支持將 String 類型的請求參數轉換爲 LocalDateTime 類型,因此咱們須要自定義 converter 「轉換器」完整整個轉換過程

自定義轉換器 StringToLocalDateTimeConverter,使其實現 org.springframework.core.convert.converter.Converter<S, T> 接口,在重寫的 convert 方法中實現咱們自定義的轉換邏輯api

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);
	}
}
複製代碼

將轉換器註冊到上下文中:app

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

從新訪問上面連接,查看控制檯,按照預期獲得相應轉換結果:ide

c.e.unifiedreturn.api.BindingController  : 2019-12-10T12:00
複製代碼

知道了這個,好比咱們經常使用的枚舉類型也能夠應用這種方式作數據綁定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 統一返回格式的代碼實現


靈魂追問

  1. 如上圖所示,在追中源碼時,發現HandlerMethodArgumentResolverCompositeHandlerMethodArgumentResolver 的實現類之一,其中有一個 Map 類型的成員變量,一般咱們使用 Map,key 的類型多數爲 String 類型,但看到這個 Map 中有這樣的 key 你立刻想到的是什麼?基礎面試常常會問 equals 和 hashcode 的問題,下一篇文章會藉着這個類來分析說明一下你總困惑的這件小事
  2. 對於 Spring Boot 的整個調用過程,你能描述出總體流程嗎?
  3. Spring 內置多少個 Resolver?你能夠跟蹤調試獲取到

歡迎持續關注公衆號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


相關文章
相關標籤/搜索