前言:
本篇文章不是API參考文檔,所以不會(huì)將用到的所有內(nèi)容詳細(xì)列出來(lái)。本文的目的主要是告訴讀者關(guān)于Java的 Bean Validation在Spring的應(yīng)用,并針對(duì)常見(jiàn)的場(chǎng)景進(jìn)行說(shuō)明,力求讓讀者對(duì)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ì)對(duì)輸入輸出做大量的校驗(yàn),以防止非法參數(shù)導(dǎo)致程序運(yùn)行異常,Java 從2009年開(kāi)始提出了 Bean Validation 1.0(也就是JSR-303)API,力求將輸入輸入的校驗(yàn)標(biāo)準(zhǔn)化和簡(jiǎn)單化,更重要的是將校驗(yàn)通用化。Hibernate Validation 是常用的針對(duì)Bean Validation API的實(shí)現(xiàn)之一(還有Apache BVal),并在Bean Validation 的API基礎(chǔ)上,進(jìn)行了擴(kuò)展,以覆蓋更多的場(chǎng)景。Spring Validation 則在整合了Hibernate Validation 的基礎(chǔ)上,以Spring的方式,支持Spring應(yīng)用的輸入輸出校驗(yàn),比如MVC入?yún)⑿r?yàn),方法級(jí)校驗(yàn)等等。至此,針對(duì)文章關(guān)鍵字已經(jīng)進(jìn)行了大概的說(shuō)明,下面是他們之間的詳細(xì)關(guān)系:
到目前為止Java Bean validation一共有三個(gè)版本。
概覽
下面的代碼片段是Controller中常見(jiàn)的代碼,這里出現(xiàn)了@Valid
,@Validated
,@NotEmpty
等等和校驗(yàn)相關(guān)的注解,但是其目的卻很簡(jiǎn)單:對(duì)uuid
和dtoList
兩個(gè)參數(shù)進(jìn)行校驗(yàn),并且對(duì)list
中的元素也進(jìn)行遍歷校驗(yàn)。
后續(xù)我們?cè)卺槍?duì)此代碼片段進(jìn)行詳細(xì)說(shuō)明。
@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)注解,表示對(duì)需要校驗(yàn)的 【字段/方法/入?yún)ⅰ?進(jìn)行校驗(yàn)標(biāo)記@
Validated
(org.springframework.validation.annotation
):是Spring對(duì)@Valid
擴(kuò)展后的變體,支持分組校驗(yàn)。
MVC中的校驗(yàn)
Spring中的校驗(yàn)有兩種場(chǎng)景,一種是MVC中的controller
層校驗(yàn),一種是添加@Validated
的bean的校驗(yàn),上面提到的例子其實(shí)是兩種場(chǎng)景的共用的情況。
MVC中的校驗(yàn)比較簡(jiǎn)單,在Controller的方法入?yún)⒒蛘叱鰠⑻砑?code>@Valid或者@Validated
注解,即可對(duì)標(biāo)記的對(duì)象進(jìn)行校驗(yàn)。
假設(shè)需要校驗(yàn)的目標(biāo)對(duì)象為Person
,Person
的每個(gè)字段都有一定的業(yè)務(wù)要求:
public class Person {
@NotBlank //名稱(chēng)不能為空
private String name;
@Pattern(regexp = "1[0-9]{10}") // 電話號(hào)碼滿足1開(kāi)頭,11位長(zhǎng)的數(shù)字
private String number;
@NotEmpty //至少有一個(gè)地址
private List<String> address;
//getter/setter
}
則以下幾種使用方法都是ok的
// test1: 使用Valied對(duì)Person進(jìn)行校驗(yàn)
@PostMapping("test1")
public ResponseEntity<?> test1(@RequestBody @Valid Person person) {
return ResponseEntity.ok("ok");
}
// test2: 使用@Validated對(duì)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)對(duì)象
@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
中沒(méi)有將校驗(yàn)的結(jié)果放到BindingResult
中,則controller
校驗(yàn)未通過(guò)時(shí),會(huì)直接扔出異常,如沒(méi)有自動(dòng)捕獲,則請(qǐng)求會(huì)返回BadRequest:400
。
校驗(yàn)對(duì)象樹(shù)
上述例子中Person
是一個(gè)較為簡(jiǎn)單的DTO,如果是一個(gè)比較復(fù)雜的嵌套的DTO話,則校驗(yàn)的目標(biāo)就不應(yīng)該是一個(gè)對(duì)象,而是一個(gè)對(duì)象樹(shù)(可以把每一復(fù)雜的對(duì)象屬性看作一個(gè)節(jié)點(diǎn))。這種情況只需要調(diào)整DTO中的校驗(yàn)注解,在需要進(jìn)入到內(nèi)部校驗(yàn)的對(duì)象或者數(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對(duì)象都進(jìn)行完整校驗(yàn)
@NotEmpty
private List<Person> family;
@Valid // employee對(duì)象也會(huì)被作為一個(gè)DTO完整校驗(yàn)
private Employee superior;
}
自定義錯(cuò)誤信息&分組校驗(yàn)
上述Employee
中name
字段上的@NotEmpty
注解提供了message
,其作用是當(dāng)校驗(yàn)未通過(guò),將會(huì)使用message
的值作為錯(cuò)誤消息返回。如果缺省的話,校驗(yàn)框架會(huì)自動(dòng)生成消息如:"Employee.name can not be empty",大多數(shù)情況,校驗(yàn)注解中的message
都會(huì)配置為Spring的國(guó)際化消息的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,到目前都是看起來(lái)一切都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");
}
接口的批量操作是很常見(jiàn)的需求,比如批量新建數(shù)據(jù),這個(gè)時(shí)候Controller的入?yún)⒒旧隙际羌系男问健5瞧婀值氖沁@種寫(xiě)法并不會(huì)生效,無(wú)論是@Valid
或者@Validated
注解。為什么呢?
原因分析:
直接對(duì)List集合進(jìn)行校驗(yàn)的行為和對(duì)自定的DTO校驗(yàn)的行為其實(shí)是有區(qū)別的,區(qū)別在于自定義的DTO是被作為一個(gè)整體對(duì)象校驗(yàn)(可以理解為一個(gè)入口),對(duì)象里的每一個(gè)字段都會(huì)被按照標(biāo)記的注解進(jìn)行校驗(yàn)。但是將List作為一個(gè)整體對(duì)象的時(shí)候,其內(nèi)部是沒(méi)有任何校驗(yàn)注解的,因?yàn)閖ava源碼中本身就沒(méi)有添加校驗(yàn)相關(guān)的注解。上述的
test5
和test6
其本質(zhì)是方法級(jí)別的校驗(yàn),與下面這個(gè)例子test7
類(lèi)似。這個(gè)時(shí)候@Valid
和@NotEmpty
都想把personList
作為一個(gè)字段來(lái)校驗(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的類(lèi)上添加
@Validated
注解。注意任何Spring的bean都可以添加@Validated
注解來(lái)進(jìn)行方法級(jí)別的校驗(yàn),并不是只能用在Controller上,后續(xù)會(huì)進(jìn)行詳細(xì)說(shuō)明。
詳解@Validated
注解
關(guān)于@Validated
注解的功能,官方注釋里面已經(jīng)寫(xiě)的很清楚了,我這里簡(jiǎn)單翻譯下:
- JSR-303的變種
@Valid
,支持驗(yàn)證組規(guī)范。支持基于Spring的JSR-303,但不支持JSR-303的特殊擴(kuò)展。- 可以用于例如Spring MVC處理程序方法參數(shù)。通過(guò){
@linkorg.springframework.validation.SmartValidator
}支持組驗(yàn)證。- 支持方法級(jí)的驗(yàn)證。在方法級(jí)別上添加此注解,會(huì)覆蓋類(lèi)上的組信息。但是方法上的注釋不會(huì)作為切入點(diǎn),要想方法上的注解生效,類(lèi)上也必須添加注解。
- 支持元注解,可以添加在自定義注解上,組裝為新的注解
通過(guò)官方的注釋?zhuān)呀?jīng)能夠明白這個(gè)注解的大部分功能了。上文也陸陸續(xù)續(xù)的提到的@Validated
注解,那么除了在MVC的校驗(yàn)中可以與@Valid
的替換外,其他情況如何來(lái)使用呢?
@Validated
加在類(lèi)上
將@Validated
加在類(lèi)上,Spring會(huì)將標(biāo)注的類(lèi)包裝為切面,從而讓類(lèi)中的方法調(diào)用時(shí),支持Java的校驗(yàn),所以當(dāng)使用@Validated
時(shí),不僅可以用于Controller上,其他所有的Spring的bean也都可以使用。
因?yàn)?code>@Validated支持分組校驗(yàn),當(dāng)加在類(lèi)上的@Validated
提供了分組參數(shù)時(shí),默認(rèn)會(huì)應(yīng)用到類(lèi)中所有的校驗(yàn)中。比如如下提供的例子,類(lèi)上的@Validated
注解提供了Default
和Insert
兩個(gè)分組標(biāo)記參數(shù),因此這兩個(gè)組會(huì)默認(rèn)應(yīng)用到類(lèi)中的doSomething
方法上。doSomething
方法的返回值應(yīng)用了Insert
分組,在此類(lèi)中就會(huì)生效。入?yún)⑸咸砑拥?code>@NotEmpty沒(méi)有提供分組參數(shù),默認(rèn)為Default
分組,也會(huì)生效。反之,如果此例中類(lèi)上的分組沒(méi)有提供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
注解加在類(lèi)上是必要條件。方法上的@Validated
注解作用一般是覆蓋類(lèi)上提供的分組。
比如下例中的代碼,因?yàn)榉椒ㄉ系姆纸M覆蓋了類(lèi)上的分組信息,因此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ù)使用文章開(kāi)頭的例子進(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
}
}
本例中,首先類(lèi)上添加了@Validated
注解,沒(méi)有指定分組參數(shù),因?yàn)槟J(rèn)為Default
分組。然后doSomething
方法添加了@Validated
注解并覆蓋了類(lèi)上的默認(rèn)分組信息,額外添加了Update
分組。因此,此方法的校驗(yàn)會(huì)在Default
和Update
上生效。
uuid
參數(shù)上有一個(gè)@Size
注解,指定了字符串的長(zhǎng)度只能為32,默認(rèn)分組,因此會(huì)生效。
指定長(zhǎng)度為32有什么意義,除了對(duì)生產(chǎn)環(huán)境的入?yún)?yán)格校驗(yàn)之外,對(duì)開(kāi)發(fā)也是有幫助的。比如我經(jīng)常會(huì)遇到對(duì)接的前端的代碼有bug,傳遞了
undefined
到uuid
參數(shù)中,如果此時(shí)添加了長(zhǎng)度校驗(yàn),就可以一眼看出來(lái)問(wèn)題,而不用再去debug代碼。
dtoList
參數(shù)就有意思了,為了遍歷校驗(yàn)到list
中的所有元素,需要添加@Valid
注解,除此之外,為了保證入?yún)⒌挠行裕苊鉄o(wú)效的請(qǐng)求,添加了@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ù)場(chǎng)景往往比較復(fù)雜,單個(gè)DTO可能會(huì)用于新建和更新等多個(gè)方法入?yún)⑸希驗(yàn)楦潞托陆ǖ臅r(shí)候,業(yè)務(wù)需求的參數(shù)不一樣,因此校驗(yàn)的要求也就不一樣,這個(gè)時(shí)候如果沒(méi)有分組校驗(yàn)的支持,我們可能需要建立兩個(gè)DTO來(lái)分別滿足新建和更新兩種操作場(chǎng)景。而如果有了分組校驗(yàn),就可以針對(duì)業(yè)務(wù)要求,只開(kāi)啟需要校驗(yàn)的分組,保證的代碼的簡(jiǎn)潔和通用。
常見(jiàn)錯(cuò)誤
- HV000151問(wèn)題
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method XxxxImpl.
翻譯過(guò)來(lái)就是說(shuō),子類(lèi)重寫(xiě)的方法或者實(shí)現(xiàn)類(lèi)的方法不能重新定義校驗(yàn)注解,如果校驗(yàn)注解不一致,則扔出HV000151問(wèn)題。
但是以下情況是允許的:
- 覆蓋父類(lèi)或者接口的分組信息
public interface A {
void doSomething(@Valid Object arg);
}
@Component
@Validated
public class B implement A {
// 可以通過(guò)在子類(lèi)或者實(shí)現(xiàn)上添加@Validated注解,
// 覆蓋上層的默認(rèn)分組信息,這樣多個(gè)實(shí)現(xiàn)類(lèi)就可以客制化校驗(yàn)信息
@Validated({NewGroup.class})
public void doSomething(@Valid Object arg) {
// do something
}
}