代碼整潔之道-Bean Validation【原創(chuàng)】

前言:

本篇文章不是API參考文檔,所以不會(huì)將用到的所有內(nèi)容詳細(xì)列出來肌厨。本文的目的主要是告訴讀者關(guān)于Java的 Bean Validation在Spring的應(yīng)用,并針對常見的場景進(jìn)行說明祈坠,力求讓讀者對Java的Bean Validation有一個(gè)完整的認(rèn)識(shí)和理解净薛。

最后更新日期:2020-02-17

文章關(guān)鍵字:

  • JSR-303
  • Bean Validation 1.0/1.1/2.0
  • MVC Validation
  • Hibernate Validation
  • Spring Validation

為了保證代碼的正常運(yùn)行,經(jīng)常會(huì)對輸入輸出做大量的校驗(yàn)俘闯,以防止非法參數(shù)導(dǎo)致程序運(yùn)行異常潭苞,Java 從2009年開始提出了 Bean Validation 1.0(也就是JSR-303)API,力求將輸入輸入的校驗(yàn)標(biāo)準(zhǔn)化和簡單化备徐,更重要的是將校驗(yàn)通用化萄传。Hibernate Validation 是常用的針對Bean Validation API的實(shí)現(xiàn)之一(還有Apache BVal),并在Bean Validation 的API基礎(chǔ)上蜜猾,進(jìn)行了擴(kuò)展秀菱,以覆蓋更多的場景。Spring Validation 則在整合了Hibernate Validation 的基礎(chǔ)上蹭睡,以Spring的方式衍菱,支持Spring應(yīng)用的輸入輸出校驗(yàn),比如MVC入?yún)⑿r?yàn)肩豁,方法級(jí)校驗(yàn)等等脊串。至此,針對文章關(guān)鍵字已經(jīng)進(jìn)行了大概的說明清钥,下面是他們之間的詳細(xì)關(guān)系:

依賴關(guān)系

到目前為止Java Bean validation一共有三個(gè)版本琼锋。

Java Bean Validation版本關(guān)系

概覽

下面的代碼片段是Controller中常見的代碼,這里出現(xiàn)了@Valid祟昭,@Validated缕坎,@NotEmpty等等和校驗(yàn)相關(guān)的注解,但是其目的卻很簡單:對uuiddtoList兩個(gè)參數(shù)進(jìn)行校驗(yàn)篡悟,并且對list中的元素也進(jìn)行遍歷校驗(yàn)谜叹。

后續(xù)我們在針對此代碼片段進(jìn)行詳細(xì)說明匾寝。

@Validated
@RestController
public class DemoController {

    @PutMapping("bean/validation/tips/{uuid}")
    @Validated({Default.class, Update.class})
    public ResponseEntity<List<ValidationDTO>> doSomething(
            @PathVariable("uuid") @Size(min=32, max=32) String uuid,
            @RequestBody @Valid @NotEmpty List<ValidationDTO> dtoList) {
            // do something
    }
}

@Valid和@Validated

  • @Valid (javax.validation): 是Bean Validation 中的標(biāo)準(zhǔn)注解,表示對需要校驗(yàn)的 【字段/方法/入?yún)ⅰ?進(jìn)行校驗(yàn)標(biāo)記

  • @Validated (org.springframework.validation.annotation):是Spring對@Valid擴(kuò)展后的變體荷腊,支持分組校驗(yàn)艳悔。

MVC中的校驗(yàn)

Spring中的校驗(yàn)有兩種場景,一種是MVC中的controller層校驗(yàn)女仰,一種是添加@Validated的bean的校驗(yàn)猜年,上面提到的例子其實(shí)是兩種場景的共用的情況。

MVC中的校驗(yàn)比較簡單董栽,在Controller的方法入?yún)⒒蛘叱鰠⑻砑?code>@Valid或者@Validated注解码倦,即可對標(biāo)記的對象進(jìn)行校驗(yàn)。

假設(shè)需要校驗(yàn)的目標(biāo)對象為Person锭碳,Person的每個(gè)字段都有一定的業(yè)務(wù)要求:

public class Person {

    @NotBlank //名稱不能為空
    private String name;
    
    @Pattern(regexp = "1[0-9]{10}") // 電話號(hào)碼滿足1開頭袁稽,11位長的數(shù)字
    private String number;

    @NotEmpty //至少有一個(gè)地址
    private List<String> address;

  //getter/setter
  
}

則以下幾種使用方法都是ok的

// test1: 使用Valied對Person進(jìn)行校驗(yàn)
@PostMapping("test1")
public ResponseEntity<?> test1(@RequestBody @Valid Person person) {

    return ResponseEntity.ok("ok");
}
// test2: 使用@Validated對person進(jìn)行校驗(yàn),并將錯(cuò)誤信息綁定到BindingResult中
@PostMapping("test2")
public ResponseEntity<?> test2(@RequestBody @Validated Person person, BindingResult result) {

    if (result.hasErrors()) {
        for (FieldError fieldError : result.getFieldErrors()) {
            //...
        }
        return ResponseEntity.badRequest().body("fail");
    }
    return ResponseEntity.ok("success");
}
// test3: 如果有多個(gè)需要校驗(yàn)的參數(shù)需要給到BindingResult中擒抛,則每個(gè)result需要緊跟著被校驗(yàn)對象
@PostMapping("test3")
public ResponseEntity<?> test3(@Validated Person person, BindingResult result,
                               @Validated Person person2, BindingResult result2) {

    if (result.hasErrors()) {
        for (FieldError fieldError : result.getFieldErrors()) {
            //...
        }
        return ResponseEntity.badRequest().body("fail");
    }
    return ResponseEntity.ok("success");
}

綜上代碼所述:mvc的校驗(yàn)中@Valid@Validated是可以互換的推汽,行為基本一致。test1中沒有將校驗(yàn)的結(jié)果放到BindingResult中歧沪,則controller校驗(yàn)未通過時(shí)歹撒,會(huì)直接扔出異常,如沒有自動(dòng)捕獲诊胞,則請求會(huì)返回BadRequest:400暖夭。

校驗(yàn)對象樹

上述例子中Person是一個(gè)較為簡單的DTO,如果是一個(gè)比較復(fù)雜的嵌套的DTO話撵孤,則校驗(yàn)的目標(biāo)就不應(yīng)該是一個(gè)對象迈着,而是一個(gè)對象樹(可以把每一復(fù)雜的對象屬性看作一個(gè)節(jié)點(diǎn))。這種情況只需要調(diào)整DTO中的校驗(yàn)注解邪码,在需要進(jìn)入到內(nèi)部校驗(yàn)的對象或者數(shù)據(jù)集合添加@Valid注解即可裕菠。Hibernate Validator官方文檔中有較為詳細(xì)的描述【占坑】。

public static class Employee {

    @NotNull(groups = {Update.class})
     private String uuid;

    @NotBlank(message = "員工姓名不能為空")
    private String name;

    @Pattern(regexp = "1[0-9]{10}")
    private String number;

    @NotEmpty
    private List<String> address;

    @Valid // family中每一個(gè)Person對象都進(jìn)行完整校驗(yàn)
    @NotEmpty
    private List<Person> family;

    @Valid // employee對象也會(huì)被作為一個(gè)DTO完整校驗(yàn)
    private Employee superior;
}

自定義錯(cuò)誤信息&分組校驗(yàn)

上述Employeename字段上的@NotEmpty注解提供了message闭专,其作用是當(dāng)校驗(yàn)未通過奴潘,將會(huì)使用message的值作為錯(cuò)誤消息返回。如果缺省的話影钉,校驗(yàn)框架會(huì)自動(dòng)生成消息如:"Employee.name can not be empty"画髓,大多數(shù)情況,校驗(yàn)注解中的message都會(huì)配置為Spring的國際化消息的code進(jìn)行使用平委。

上述Employeeuuid主鍵字段上添加了NotNull注解雀扶,但是提供了groups,其值為Update.class肆汹。其作用是當(dāng)校驗(yàn)組包含Update.class標(biāo)記時(shí)愚墓,此校驗(yàn)注解才會(huì)生效,其他未提供組的校驗(yàn)注解默認(rèn)為Default.class組昂勉,也就是默認(rèn)組浪册。這個(gè)就是按組校驗(yàn),如果要讓Employee中所有的校驗(yàn)注解都生效岗照,則需要使用@Validated({Update.class, Default.class})村象,當(dāng)然如果只需要默認(rèn)組生效,直接用@Validated或者@Validated(Default.class)都可以攒至。

下面是用法舉例:

// 分組校驗(yàn)
@PostMapping("test1")
public ResponseEntity<?> test4(@RequestBody @Validated({Update.class, Default.class}) Employee employee) {
    return ResponseEntity.ok("ok");
}

MVC的入?yún)⑿r?yàn)未生效

ok厚者,到目前都是看起來一切都OK,但是注意下面例子中 test5/test6的情況迫吐。

@PostMapping("test5")
public ResponseEntity<?> test5(@Valid @RequestBody List<Person> personList) {
    return ResponseEntity.ok("ok");
}

@PostMapping("test6")
public ResponseEntity<?> test6(@Validated @RequestBody List<Person> personList) {
    return ResponseEntity.ok("ok");
}

接口的批量操作是很常見的需求库菲,比如批量新建數(shù)據(jù),這個(gè)時(shí)候Controller的入?yún)⒒旧隙际羌系男问街景颉5瞧婀值氖沁@種寫法并不會(huì)生效熙宇,無論是@Valid或者@Validated注解。為什么呢溉浙?

原因分析:

直接對List集合進(jìn)行校驗(yàn)的行為和對自定的DTO校驗(yàn)的行為其實(shí)是有區(qū)別的烫止,區(qū)別在于自定義的DTO是被作為一個(gè)整體對象校驗(yàn)(可以理解為一個(gè)入口),對象里的每一個(gè)字段都會(huì)被按照標(biāo)記的注解進(jìn)行校驗(yàn)。但是將List作為一個(gè)整體對象的時(shí)候戳稽,其內(nèi)部是沒有任何校驗(yàn)注解的馆蠕,因?yàn)閖ava源碼中本身就沒有添加校驗(yàn)相關(guān)的注解。上述的test5test6其本質(zhì)是方法級(jí)別的校驗(yàn)惊奇,與下面這個(gè)例子test7類似互躬。這個(gè)時(shí)候@Valid@NotEmpty都想把personList作為一個(gè)字段來校驗(yàn),但是MVC不支持這種模式赊时,所以未生效吨铸。

@PostMapping("test7")
public ResponseEntity<?> test7(@Valid @NotEmpty @RequestBody List<Person> personList) {
    return ResponseEntity.ok("ok");
}

解決方案:

解決辦法有兩種,一種是封裝祖秒,將接口需要校驗(yàn)的參數(shù)封裝為一個(gè)DTO诞吱,然后再校驗(yàn)。第二個(gè)種是使用Spring的方法級(jí)別的校驗(yàn)竭缝,在Controller的類上添加@Validated注解房维。注意任何Spring的bean都可以添加@Validated注解來進(jìn)行方法級(jí)別的校驗(yàn),并不是只能用在Controller上抬纸,后續(xù)會(huì)進(jìn)行詳細(xì)說明咙俩。

詳解@Validated注解

關(guān)于@Validated注解的功能,官方注釋里面已經(jīng)寫的很清楚了,我這里簡單翻譯下:

  1. JSR-303的變種@Valid阿趁,支持驗(yàn)證組規(guī)范膜蛔。支持基于Spring的JSR-303,但不支持JSR-303的特殊擴(kuò)展脖阵。
  2. 可以用于例如Spring MVC處理程序方法參數(shù)皂股。通過{@linkorg.springframework.validation.SmartValidator}支持組驗(yàn)證。
  3. 支持方法級(jí)的驗(yàn)證命黔。在方法級(jí)別上添加此注解呜呐,會(huì)覆蓋類上的組信息。但是方法上的注釋不會(huì)作為切入點(diǎn)悍募,要想方法上的注解生效蘑辑,類上也必須添加注解。
  4. 支持元注解坠宴,可以添加在自定義注解上洋魂,組裝為新的注解

通過官方的注釋,已經(jīng)能夠明白這個(gè)注解的大部分功能了啄踊。上文也陸陸續(xù)續(xù)的提到的@Validated注解忧设,那么除了在MVC的校驗(yàn)中可以與@Valid的替換外,其他情況如何來使用呢颠通?

@Validated加在類上

@Validated加在類上址晕,Spring會(huì)將標(biāo)注的類包裝為切面,從而讓類中的方法調(diào)用時(shí)顿锰,支持Java的校驗(yàn)谨垃,所以當(dāng)使用@Validated時(shí),不僅可以用于Controller上硼控,其他所有的Spring的bean也都可以使用刘陶。

因?yàn)?code>@Validated支持分組校驗(yàn),當(dāng)加在類上的@Validated提供了分組參數(shù)時(shí)牢撼,默認(rèn)會(huì)應(yīng)用到類中所有的校驗(yàn)中匙隔。比如如下提供的例子,類上的@Validated注解提供了DefaultInsert兩個(gè)分組標(biāo)記參數(shù)熏版,因此這兩個(gè)組會(huì)默認(rèn)應(yīng)用到類中的doSomething方法上纷责。doSomething方法的返回值應(yīng)用了Insert分組,在此類中就會(huì)生效撼短。入?yún)⑸咸砑拥?code>@NotEmpty沒有提供分組參數(shù)再膳,默認(rèn)為Default分組,也會(huì)生效曲横。反之喂柒,如果此例中類上的分組沒有提供Default分組,則下面doSomething方法入?yún)⑸系?code>@NotEmpty就不會(huì)生效。

@Validated({Insert.class, Default.class})
@Component
public class ValidatedTest {
    
    public @NotNull(groups = Insert.class) Object doSomething(@NotEmpty Object[] arg) {
        // do something
        return null;
    }
}

@Validated加在方法上

當(dāng)@Validated注解單獨(dú)加在方法上時(shí)灾杰,并不會(huì)按照預(yù)期的效果工作蚊丐。因此,@Validated注解加在類上是必要條件吭露。方法上的@Validated注解作用一般是覆蓋類上提供的分組吠撮。

比如下例中的代碼,因?yàn)榉椒ㄉ系姆纸M覆蓋了類上的分組信息讲竿,因此doSomething方法上的@NotNull因?yàn)榉纸M不匹配的原因,并不會(huì)生效弄屡。

@Validated({Insert.class, Default.class})
@Component
public class ValidatedTest {
    @Validated({Default.class})
    public Object doSomething(@NotNull(groups = {Insert.class}) Object arg) {
        // do something
        return null;
    }
}

實(shí)戰(zhàn)

實(shí)際使用較為復(fù)雜的情況题禀,會(huì)用到上文中提到的一個(gè)或者多個(gè)特性組合使用。繼續(xù)使用文章開頭的例子進(jìn)行講解膀捷。

@Validated
@RestController
public class DemoController {

    @PutMapping("bean/validation/tips/{uuid}")
    @Validated({Default.class, Update.class})
    public ResponseEntity<List<ValidationDTO>> doSomething(
            @PathVariable("uuid") @Size(min=32, max=32) String uuid,
            @RequestBody @Valid @NotEmpty List<ValidationDTO> dtoList) {
            // do something
    }
}

本例中迈嘹,首先類上添加了@Validated注解,沒有指定分組參數(shù)全庸,因?yàn)槟J(rèn)為Default分組秀仲。然后doSomething方法添加了@Validated注解并覆蓋了類上的默認(rèn)分組信息,額外添加了Update分組壶笼。因此神僵,此方法的校驗(yàn)會(huì)在DefaultUpdate上生效。

uuid參數(shù)上有一個(gè)@Size注解覆劈,指定了字符串的長度只能為32保礼,默認(rèn)分組,因此會(huì)生效责语。

指定長度為32有什么意義炮障,除了對生產(chǎn)環(huán)境的入?yún)?yán)格校驗(yàn)之外,對開發(fā)也是有幫助的坤候。比如我經(jīng)常會(huì)遇到對接的前端的代碼有bug胁赢,傳遞了undefineduuid參數(shù)中,如果此時(shí)添加了長度校驗(yàn)白筹,就可以一眼看出來問題智末,而不用再去debug代碼。

dtoList參數(shù)就有意思了遍蟋,為了遍歷校驗(yàn)到list中的所有元素吹害,需要添加@Valid注解,除此之外虚青,為了保證入?yún)⒌挠行运剑苊鉄o效的請求,添加了@NotEmpty注解,保證集合中至少有一個(gè)元素纵穿。而方法上標(biāo)注的分組信息DefultUpdate會(huì)應(yīng)用于集合中的每一個(gè)元素的校驗(yàn)上下隧。

如果ValidationDTO如下,則在DefaultUpdate分組有效時(shí)只有contentversionNumber字段上的注解會(huì)生效谓媒。

class ValidationDTO {
    
    @NotEmpty(groups = Insert.class)
    private String id;
    
    @NotBlank
    private String content;
    
    @NotNull(groups = Update.class)
    private Long versionNumber;
    
    @Valid
    @NotEmpty(groups = Insert.class)
    private List<ValidationDTO> children;
    
}

分組校驗(yàn)有什么意義:

實(shí)際的業(yè)務(wù)場景往往比較復(fù)雜淆院,單個(gè)DTO可能會(huì)用于新建和更新等多個(gè)方法入?yún)⑸希驗(yàn)楦潞托陆ǖ臅r(shí)候句惯,業(yè)務(wù)需求的參數(shù)不一樣土辩,因此校驗(yàn)的要求也就不一樣,這個(gè)時(shí)候如果沒有分組校驗(yàn)的支持抢野,我們可能需要建立兩個(gè)DTO來分別滿足新建和更新兩種操作場景拷淘。而如果有了分組校驗(yàn),就可以針對業(yè)務(wù)要求指孤,只開啟需要校驗(yàn)的分組启涯,保證的代碼的簡潔和通用。

常見錯(cuò)誤

  1. HV000151問題

javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method XxxxImpl.

翻譯過來就是說恃轩,子類重寫的方法或者實(shí)現(xiàn)類的方法不能重新定義校驗(yàn)注解结洼,如果校驗(yàn)注解不一致,則扔出HV000151問題叉跛。

但是以下情況是允許的:

  • 覆蓋父類或者接口的分組信息

public interface A {

    void doSomething(@Valid Object arg);
}

@Component
@Validated
public class B implement A {

    // 可以通過在子類或者實(shí)現(xiàn)上添加@Validated注解松忍,
    // 覆蓋上層的默認(rèn)分組信息,這樣多個(gè)實(shí)現(xiàn)類就可以客制化校驗(yàn)信息
    @Validated({NewGroup.class})
    public void doSomething(@Valid Object arg) {
        // do something
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末昧互,一起剝皮案震驚了整個(gè)濱河市挽铁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌敞掘,老刑警劉巖叽掘,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異玖雁,居然都是意外死亡更扁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門赫冬,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浓镜,“玉大人,你說我怎么就攤上這事劲厌√叛Γ” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵补鼻,是天一觀的道長哄啄。 經(jīng)常有香客問我雅任,道長,這世上最難降的妖魔是什么咨跌? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任沪么,我火速辦了婚禮,結(jié)果婚禮上锌半,老公的妹妹穿的比我還像新娘禽车。我一直安慰自己,他們只是感情好刊殉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布殉摔。 她就那樣靜靜地躺著,像睡著了一般冗澈。 火紅的嫁衣襯著肌膚如雪钦勘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天亚亲,我揣著相機(jī)與錄音,去河邊找鬼腐缤。 笑死捌归,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的岭粤。 我是一名探鬼主播惜索,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼剃浇!你這毒婦竟也來了巾兆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤虎囚,失蹤者是張志新(化名)和其女友劉穎角塑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淘讥,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡圃伶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蒲列。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窒朋。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蝗岖,靈堂內(nèi)的尸體忽然破棺而出侥猩,到底是詐尸還是另有隱情,我是刑警寧澤抵赢,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布欺劳,位于F島的核電站唧取,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏杰标。R本人自食惡果不足惜兵怯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望腔剂。 院中可真熱鬧媒区,春花似錦、人聲如沸掸犬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽湾碎。三九已至宙攻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間介褥,已是汗流浹背座掘。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柔滔,地道東北人溢陪。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像睛廊,于是被迫代替她去往敵國和親形真。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

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