如何妙用Spring 數(shù)據(jù)綁定機(jī)制答渔?

前言

在剖析完 「Spring Boot 統(tǒng)一數(shù)據(jù)格式是怎么實(shí)現(xiàn)的? 」文章之后葵硕,一直覺得有必要說明一下 Spring's Data Binding Mechanism 「Spring 數(shù)據(jù)綁定機(jī)制」罐监。

默認(rèn)情況下吴藻,Spring 只知道如何轉(zhuǎn)換簡單數(shù)據(jù)類型。比如我們提交的 int弓柱、String 或 boolean類型的請求數(shù)據(jù)沟堡,它會自動綁定到與之對應(yīng)的 Java 類型。但在實(shí)際項(xiàng)目中矢空,遠(yuǎn)遠(yuǎn)不夠航罗,因?yàn)槲覀兛赡苄枰壎ǜ鼜?fù)雜的對象類型。

我們需要了解 Spring 數(shù)據(jù)綁定機(jī)制屁药,這樣我們就可以更靈活的做全局配置或自定義配置粥血,進(jìn)而讓我們的 RESTful API 更簡潔,可讀性也更好酿箭。本文依舊先通過示例代碼說明實(shí)現(xiàn)复亏,然后進(jìn)行源碼分析,帶領(lǐng)大家了解這個機(jī)制是如何生效的缭嫡,知其所以然蜓耻, 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());
    }
}

當(dāng)我們用 Postman 請求這個 API

http://localhost:8080/rgyb/bindings/2019-12-10 12:00:00

如我們所料,拋出數(shù)據(jù)類型轉(zhuǎn)換異常



因?yàn)?Spring 默認(rèn)不支持將 String 類型的請求參數(shù)轉(zhuǎn)換為 LocalDateTime 類型械巡,所以我們需要自定義 converter 「轉(zhuǎn)換器」完整整個轉(zhuǎn)換過程

自定義轉(zhuǎn)換器 StringToLocalDateTimeConverter,使其實(shí)現(xiàn) org.springframework.core.convert.converter.Converter<S, T> 接口饶氏,在重寫的 convert 方法中實(shí)現(xiàn)我們自定義的轉(zhuǎ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);
    }
}

將轉(zhuǎn)換器注冊到上下文中:

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

重新訪問上面鏈接讥耗,查看控制臺,按照預(yù)期得到相應(yīng)轉(zhuǎn)換結(jié)果:

c.e.unifiedreturn.api.BindingController  : 2019-12-10T12:00

知道了這個疹启,比如我們常用的枚舉類型也可以應(yīng)用這種方式做數(shù)據(jù)綁定

枚舉類型綁定

同樣的套路古程,自定義轉(zhuǎn)換器

public class StringToEnumConverter implements Converter<String, Modes> {
    
    @Override
    public Modes convert(String s) {
        return Modes.valueOf(s);
    }
}

將其添加至上下文,請小伙伴們自行嘗試吧喊崖,知道了這個挣磨,我們再也不用在 RESTful API 內(nèi)部做數(shù)據(jù)轉(zhuǎn)換了雇逞,我們做到了全局控制,同時讓整個 API 看起來更加清晰簡潔

綁定對象

在某些情況下茁裙,我們希望將數(shù)據(jù)綁定到對象塘砸,這時我們可能馬上聯(lián)想起來使用 @RequestBody 注解,該注解通常用于獲取 POST 請求體晤锥,并將其轉(zhuǎn)換相應(yīng)的數(shù)據(jù)對象

在實(shí)際業(yè)務(wù)場景中掉蔬,除了請求體中的數(shù)據(jù),我們同樣需要請求頭中的數(shù)據(jù)矾瘾,比如 token 女轿,token 中包含當(dāng)前登陸用戶的信息,每一次 RESTful 請求我們都需要從 header 中獲取 token 數(shù)據(jù)處理實(shí)際業(yè)務(wù)壕翩,這種場景蛉迹,上文提到的 Converter 以及 @RequestBody 顯然不能滿足我們的需求,此時我們就要換另一種解決方案 : HandlerMethodArgumentResolver

首先我們需要自定義一個注解 LoginUser (運(yùn)行時生效放妈,作用于參數(shù)上)

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface LoginUser {
}

然后自定義 LoginUserArgumentResolver 北救,使其實(shí)現(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 中設(shè)置好相應(yīng)的 K-V挽绩,如下圖

http://localhost:8080/rgyb/bindings/id
image

發(fā)送請求膛壹,查看控制臺,得到預(yù)期結(jié)果

c.e.unifiedreturn.api.BindingController  : LoginUserVo(id=111111, name=rgyb)

相信到這里唉堪,你已經(jīng)了解了基本的使用模聋,接下來我們進(jìn)行源碼分析,透過現(xiàn)象看本質(zhì) (希望可以打開 IDE 跟著步驟查看)

Spring 數(shù)據(jù)綁定源碼分析

首先我們需要了解我們自定義的 LoginUserArgumentResolver 是如何被加載到上下文中的唠亚,在你看過 HttpMessageConverter轉(zhuǎn)換原理解析Springboot返回統(tǒng)一JSON數(shù)據(jù)格式是怎么實(shí)現(xiàn)的链方?后,你也許已經(jīng)有了眉目灶搜,同加載 MessageConverter 如出一轍祟蚀,在 RequestMappingHandlerAdapter 類中,同樣有添加 ArgumentResolver 的方法割卖,該方法會把系統(tǒng)內(nèi)置的 resolver 和用戶自定義的 resolver 都加載到上下文中前酿,關(guān)鍵代碼展示如下:

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轉(zhuǎn)換原理解析 文章中有一段調(diào)用棧跟蹤,我再次粘貼在此處鹏溯,并用紅框做出標(biāo)記罢维,其實(shí)我們在分析 messageConverter 時已經(jīng)悄悄的路過了我們本節(jié)要說的內(nèi)容

我們進(jìn)入相應(yīng)的類中瞧一瞧:


到這里你應(yīng)該猛的了解這背后的道理了吧

接下來,我們來驗(yàn)證我們天天用的 @RequestBody 注解是不是這個套路呢丙挽?
處理該注解的類是 RequestResponseBodyMethodProcessor肺孵,查看其類圖匀借,發(fā)現(xiàn)其依舊實(shí)現(xiàn)了 HandlerMethodArgumentResolver 接口

打開該類,你會看到下圖代碼平窘,重點(diǎn)地方我已標(biāo)記出來


整體處理流程如出一轍吓肋,只不過在里面調(diào)用了 messageConverter 來解析 JSON 數(shù)據(jù)。

總結(jié)

本文說的 Converter 和 ArgumentResolver 以及在 Spring MVC 中常用的 @InitBinder 注解整體過程都如出一轍初婆,大家都可以按照這個思路來查看具體的實(shí)現(xiàn)蓬坡。另外,在我們完成日常編碼工作時磅叛,都可以從 Spring 現(xiàn)有的處理方式中摸索到一些解決方案屑咳,但前提是你了解 Spring 底層的一些調(diào)用過程

最后希望小伙伴打開 IDE 切實(shí)查看相應(yīng)代碼,你一定還會有新發(fā)現(xiàn)弊琴,我們可以一起探討兆龙。本文代碼已上傳,公眾號回復(fù)「demo」敲董,打開鏈接查看 「spring-boot-unified-return」文件夾內(nèi)容即可紫皇,也可以順路回顧以前 Spring Boot 統(tǒng)一返回格式的代碼實(shí)現(xiàn)


靈魂追問

  1. 如上圖所示,在追中源碼時腋寨,發(fā)現(xiàn)HandlerMethodArgumentResolverCompositeHandlerMethodArgumentResolver 的實(shí)現(xiàn)類之一聪铺,其中有一個 Map 類型的成員變量,通常我們使用 Map萄窜,key 的類型多數(shù)為 String 類型铃剔,但看到這個 Map 中有這樣的 key 你馬上想到的是什么?基礎(chǔ)面試經(jīng)常會問 equals 和 hashcode 的問題查刻,下一篇文章會借著這個類來分析說明一下你總困惑的這件小事
  2. 對于 Spring Boot 的整個調(diào)用過程键兜,你能描述出整體流程嗎?
  3. Spring 內(nèi)置多少個 Resolver穗泵?你可以跟蹤調(diào)試獲取到

歡迎持續(xù)關(guān)注公眾號:「日拱一兵」

  • 前沿 Java 技術(shù)干貨分享
  • 高效工具匯總 | 回復(fù)「工具」
  • 面試問題分析與解答
  • 技術(shù)資料領(lǐng)取 | 回復(fù)「資料」

以讀偵探小說思維輕松趣味學(xué)習(xí) Java 技術(shù)棧相關(guān)知識普气,本著將復(fù)雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術(shù)問題佃延,技術(shù)持續(xù)更新现诀,請持續(xù)關(guān)注......


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市履肃,隨后出現(xiàn)的幾起案子赶盔,更是在濱河造成了極大的恐慌,老刑警劉巖榆浓,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撕攒,居然都是意外死亡陡鹃,警方通過查閱死者的電腦和手機(jī)烘浦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萍鲸,“玉大人闷叉,你說我怎么就攤上這事〖挂酰” “怎么了握侧?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長嘿期。 經(jīng)常有香客問我品擎,道長,這世上最難降的妖魔是什么备徐? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任萄传,我火速辦了婚禮,結(jié)果婚禮上蜜猾,老公的妹妹穿的比我還像新娘秀菱。我一直安慰自己,他們只是感情好蹭睡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布衍菱。 她就那樣靜靜地躺著,像睡著了一般肩豁。 火紅的嫁衣襯著肌膚如雪脊串。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天蓖救,我揣著相機(jī)與錄音洪规,去河邊找鬼。 笑死循捺,一個胖子當(dāng)著我的面吹牛斩例,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播从橘,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼念赶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了恰力?” 一聲冷哼從身側(cè)響起叉谜,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎踩萎,沒想到半個月后停局,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年董栽,在試婚紗的時候發(fā)現(xiàn)自己被綠了码倦。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡锭碳,死狀恐怖袁稽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情擒抛,我是刑警寧澤推汽,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站歧沪,受9級特大地震影響歹撒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜槽畔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一栈妆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧厢钧,春花似錦鳞尔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至霞扬,卻和暖如春糕韧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喻圃。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工萤彩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人斧拍。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓雀扶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肆汹。 傳聞我的和親對象是個殘疾皇子愚墓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

推薦閱讀更多精彩內(nèi)容