前言:
本篇文章不是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)系:
到目前為止Java Bean validation一共有三個(gè)版本琼锋。
概覽
下面的代碼片段是Controller中常見的代碼,這里出現(xiàn)了@Valid
祟昭,@Validated
缕坎,@NotEmpty
等等和校驗(yàn)相關(guān)的注解,但是其目的卻很簡單:對uuid
和dtoList
兩個(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)
上述Employee
中name
字段上的@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)行使用平委。
上述Employee
的uuid
主鍵字段上添加了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)的注解。上述的
test5
和test6
其本質(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)寫的很清楚了,我這里簡單翻譯下:
- JSR-303的變種
@Valid
阿趁,支持驗(yàn)證組規(guī)范膜蛔。支持基于Spring的JSR-303,但不支持JSR-303的特殊擴(kuò)展脖阵。- 可以用于例如Spring MVC處理程序方法參數(shù)皂股。通過{
@linkorg.springframework.validation.SmartValidator
}支持組驗(yàn)證。- 支持方法級(jí)的驗(yàn)證命黔。在方法級(jí)別上添加此注解呜呐,會(huì)覆蓋類上的組信息。但是方法上的注釋不會(huì)作為切入點(diǎn)悍募,要想方法上的注解生效蘑辑,類上也必須添加注解。
- 支持元注解坠宴,可以添加在自定義注解上洋魂,組裝為新的注解
通過官方的注釋,已經(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
注解提供了Default
和Insert
兩個(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ì)在Default
和Update
上生效。
uuid
參數(shù)上有一個(gè)@Size
注解覆劈,指定了字符串的長度只能為32保礼,默認(rèn)分組,因此會(huì)生效责语。
指定長度為32有什么意義炮障,除了對生產(chǎn)環(huán)境的入?yún)?yán)格校驗(yàn)之外,對開發(fā)也是有幫助的坤候。比如我經(jīng)常會(huì)遇到對接的前端的代碼有bug胁赢,傳遞了
undefined
到uuid
參數(shù)中,如果此時(shí)添加了長度校驗(yàn)白筹,就可以一眼看出來問題智末,而不用再去debug代碼。
dtoList
參數(shù)就有意思了遍蟋,為了遍歷校驗(yàn)到list
中的所有元素吹害,需要添加@Valid
注解,除此之外虚青,為了保證入?yún)⒌挠行运剑苊鉄o效的請求,添加了@NotEmpty
注解,保證集合中至少有一個(gè)元素纵穿。而方法上標(biāo)注的分組信息Defult
和Update
會(huì)應(yīng)用于集合中的每一個(gè)元素的校驗(yàn)上下隧。
如果ValidationDTO
如下,則在Default
和Update
分組有效時(shí)只有content
和versionNumber
字段上的注解會(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ò)誤
- 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
}
}