如何妙用Spring 資料繫結機制?

日拱一兵發表於2019-12-16

前言

在剖析完 「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
複製程式碼

如我們所料,丟擲資料型別轉換異常

如何妙用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);
	}
}
複製程式碼

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

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

如何妙用Spring 資料繫結機制?

傳送請求,檢視控制檯,得到預期結果

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 時已經悄悄的路過了我們本節要說的內容

如何妙用Spring 資料繫結機制?

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

如何妙用Spring 資料繫結機制?

到這裡你應該猛的瞭解這背後的道理了吧

接下來,我們來驗證我們天天用的 @RequestBody 註解是不是這個套路呢? 處理該註解的類是 RequestResponseBodyMethodProcessor,檢視其類圖,發現其依舊實現了 HandlerMethodArgumentResolver 介面

如何妙用Spring 資料繫結機制?

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

如何妙用Spring 資料繫結機制?

整體處理流程如出一轍,只不過在裡面呼叫了 messageConverter 來解析 JSON 資料。

總結

本文說的 Converter 和 ArgumentResolver 以及在 Spring MVC 中常用的 @InitBinder 註解整體過程都如出一轍,大家都可以按照這個思路來檢視具體的實現。另外,在我們完成日常編碼工作時,都可以從 Spring 現有的處理方式中摸索到一些解決方案,但前提是你瞭解 Spring 底層的一些呼叫過程

最後希望小夥伴開啟 IDE 切實檢視相應程式碼,你一定還會有新發現,我們可以一起探討。本文程式碼已上傳,公眾號回覆「demo」,開啟連結檢視 「spring-boot-unified-return」資料夾內容即可,也可以順路回顧以前 Spring Boot 統一返回格式的程式碼實現


靈魂追問

如何妙用Spring 資料繫結機制?

  1. 如上圖所示,在追中原始碼時,發現HandlerMethodArgumentResolverCompositeHandlerMethodArgumentResolver 的實現類之一,其中有一個 Map 型別的成員變數,通常我們使用 Map,key 的型別多數為 String 型別,但看到這個 Map 中有這樣的 key 你馬上想到的是什麼?基礎面試經常會問 equals 和 hashcode 的問題,下一篇文章會藉著這個類來分析說明一下你總困惑的這件小事
  2. 對於 Spring Boot 的整個呼叫過程,你能描述出整體流程嗎?
  3. Spring 內建多少個 Resolver?你可以跟蹤除錯獲取到

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

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

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


如何妙用Spring 資料繫結機制?

相關文章