【SpringBoot】 在SpringBoot中使用Hibernate Validate
前言
在做web相關(guān)的應(yīng)用時业稼,經(jīng)常需要提供接口與用戶交互(獲取數(shù)據(jù)、上傳數(shù)據(jù)等)蚂蕴,由于這個過程需要用戶進(jìn)行相關(guān)的操作低散,為了避免出現(xiàn)一些錯誤的數(shù)據(jù)等,一般需要對數(shù)據(jù)進(jìn)行校驗(yàn)骡楼,隨著接口的增多熔号,校驗(yàn)邏輯的冗余度也越來越大,雖然可以通過抽象出校驗(yàn)的方法來處理鸟整,但還是需要每次手動調(diào)用校驗(yàn)邏輯引镊,相對來說還是不方便。
為了解決這個問題篮条,Java中提供了Bean Validation的標(biāo)準(zhǔn)弟头,該標(biāo)準(zhǔn)規(guī)定了校驗(yàn)的具體內(nèi)容,通過簡單的注解就能完成必要的校驗(yàn)邏輯了兑燥,相對來說就方便了很多,而該規(guī)范其實(shí)只是規(guī)范琴拧,并沒有具體的實(shí)現(xiàn)降瞳,Hibernate提供了具體的實(shí)現(xiàn),也即Hibernate Validator,這個也是目前使用得比較多的驗(yàn)證器了挣饥。
在SpringBoot中使用Hibernate Validate
首先新建一個spring boot項目除师,引入web依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
在web依賴中,已經(jīng)引入了hibernate-validator的支持扔枫,所以只需要引入web依賴即可汛聚。
mvn dependency:tree命令可以查看依賴情況
...
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.1.0.RELEASE:compile
[INFO] | +- org.hibernate.validator:hibernate-validator:jar:6.0.13.Final:compile
[INFO] | | +- javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] | | \- com.fasterxml:classmate:jar:1.4.0:compile
...
如果你所使用的版本沒有支持,或者不是使用SpringBoot項目短荐,具體的請參考文檔解決倚舀。
然后配置一下validator,由于默認(rèn)情況下忍宋,Hibernate-validator使用的校驗(yàn)策略是依次校驗(yàn)痕貌,并且將不通過的結(jié)果保存,最后再統(tǒng)一拋出異常信息糠排,但實(shí)際上舵稠,當(dāng)校驗(yàn)出現(xiàn)第一個不滿足情況的時候,就可以停止了(當(dāng)然入宦,如果選擇全部驗(yàn)證完也是可以的)哺徊,所以我們手動配置一下
ValidatorConfig
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
.configure()
// 將fail_fast設(shè)置為true即可,如果想驗(yàn)證全部乾闰,則設(shè)置為false或者取消配置即可
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
return factory.getValidator();
}
}
接下來編寫需要進(jìn)行驗(yàn)證的Bean
User.java
public class User {
@NotBlank(message = "用戶名不能為空")
private String name;
@Max(value = 120, message = "年齡不能超過120歲")
private int age;
@NotNull
@Size(min = 8, max = 20, message = "密碼必須大于8位并且小于20位")
private String password;
@Email(message = "請輸入符合格式的郵箱")
private String email;
// set落追、get方法
}
上面的注解已經(jīng)很能夠見名知意了,所以這里就先不講解汹忠,后面再補(bǔ)充常用的驗(yàn)證注解及作用總結(jié)淋硝。
定義一個簡單的測試接口
UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public User addUser(@Valid @RequestBody User user) {
// 僅測試驗(yàn)證過程,省略其他的邏輯
return user;
}
}
注意上面所使用的@Valid
注解宽菜,通過該注解能夠使得驗(yàn)證生效谣膳,如果去除的話,可以看到驗(yàn)證邏輯并沒有生效铅乡。
通過上面的一個簡單注解之后继谚,驗(yàn)證的邏輯已經(jīng)能夠生效,然而阵幸,在測試的時候花履,可能會出現(xiàn)下面的情況
{
"timestamp": "2018-11-09T01:47:56.985+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Size.user.password",
"Size.password",
"Size.java.lang.String",
"Size"
],
"arguments": [
{
"codes": [
"user.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
},
20,
8
],
"defaultMessage": "密碼必須大于8位并且小于20位",
"objectName": "user",
"field": "password",
"rejectedValue": "huanfe",
"bindingFailure": false,
"code": "Size"
}
]
# 省略trace信息
}
這是因?yàn)椋J(rèn)情況下挚赊,SpringBoot配置了默認(rèn)異常處理器DefaultHandlerExceptionResolver诡壁,而該處理器僅僅是將異常信息打印出來,顯然荠割,我們并不需要返回如此多的信息妹卿,只需要將對應(yīng)屬性中的message信息給調(diào)用者即可旺矾,解決的方法有兩種。
-
在需要驗(yàn)證的方法中加入
BindingResult
參數(shù)夺克,SpringBoot會自動將異常錯誤信息綁定到該參數(shù)上箕宙,然后處理對應(yīng)的邏輯,如下UserController.java
@PostMapping public User addUser(@Valid @RequestBody User user, BindingResult bindingResult) { if (bindingResult.hasErrors()) { // 具體的處理邏輯铺纽,如封裝錯誤信息等 } return user; }
但是這種方式不是很優(yōu)雅柬帕,因?yàn)閷τ诿恳粋€需要驗(yàn)證的方法,都需要進(jìn)行這樣的邏輯(雖然封裝處理可以解決狡门,但依舊每次需要手動調(diào)用以及加入BindingResult參數(shù))
-
由于在驗(yàn)證失敗的時候陷寝,會拋出異常,所以可以使用全局異常處理器來捕獲該異常融撞,然后進(jìn)行統(tǒng)一處理即可盼铁,具體的異常類型是
MethodArgumentNotValidException
,具體實(shí)現(xiàn)如下所示GlobalExceptionHandler.java
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResultInfo<?> validationErrorHandler(MethodArgumentNotValidException ex) { // 同樣是獲取BindingResult對象尝偎,然后獲取其中的錯誤信息 // 如果前面開啟了fail_fast饶火,事實(shí)上這里只會有一個信息 //如果沒有,則可能又多個 List<String> errorInformation = ex.getBindingResult().getAllErrors() .stream() .map(ObjectError::getDefaultMessage) .collect(Collectors.toList()); return new ResultInfo<>(400, errorInformation.toString(), null); } }
這里的ResultInfo是自定義的一個結(jié)果對象致扯,用于作為統(tǒng)一的返回對象肤寝,內(nèi)容如下
ResultInfo.java
public class ResultInfo<T> { private int code; private String message; private T body; public ResultInfo(int code, String message, T body) { this.code = code; this.message = message; this.body = body; } public ResultInfo(int code, String message) { this(code, message, null); } public ResultInfo(String message) { this(200, message, null); } } // get、set方法
通過上面的處理之后抖僵,現(xiàn)在如果驗(yàn)證不通過鲤看,則可以以比較優(yōu)雅的方式返回給調(diào)用者了。
{
"code": 400,
"message": "[密碼必須大于8位并且小于20位]",
"body": null
}
這里需要注意耍群,如果上面兩種方式都開啟的話义桂,是以第一種方式優(yōu)先的,所以蹈垢,第二種方式不會生效慷吊。
上面的方式能夠解決@RequestBody
標(biāo)注的參數(shù)的驗(yàn)證及錯誤處理,然而曹抬,并不能處理@PathVariable
以及@RequestParam
標(biāo)注的入?yún)?不生效)溉瓶,而事實(shí)上,這兩種類型的操作也是非常常用的(也是需要對這兩種類型進(jìn)行驗(yàn)證谤民,除了手動驗(yàn)證外豆混,還有一種通用的解決方案薪鹦,也是通過注解來實(shí)現(xiàn)),對于這兩種類型同樣可以使用驗(yàn)證注解進(jìn)行標(biāo)注经瓷,如下所示
UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
// .....
@GetMapping("/{name}")
public User getUserByName(
@NotNull
@Size(min = 1, max = 20, message = "用戶名格式有誤")
@PathVariable String name) {
User user = new User();
user.setName(name);
return user;
}
@GetMapping
public User getUserByNameParam(
@NotNull
@Size(min = 1, max = 20, message = "用戶名格式有誤")
@RequestParam("name") String name) {
User user = new User();
user.setName(name);
return user;
}
}
為了讓對應(yīng)的注解生效宋税,可以在類的上方使用@Validated
進(jìn)行標(biāo)注价说,注意是標(biāo)注在類上方馅精,即
@RestController
@RequestMapping("/users")
@Validated
public class UserController {
// ...
}
但此時如果驗(yàn)證失敗,會拋出異常信息顺饮,而且,異常類型不是MethodArgumentNotValidException
凌那,而是ConstraintViolationException
,巨坑R魇拧C钡!块攒,所以還需要捕獲該類型并且進(jìn)行處理励稳,如下所示
GlobalExceptionHandler.java
@ExceptionHandler(ConstraintViolationException.class)
public ResultInfo<?> validationErrorHandler(ConstraintViolationException ex) {
List<String> errorInformation = ex.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toList());
return new ResultInfo<>(400, errorInformation.toString(), null);
}
到此,基本上的參數(shù)驗(yàn)證就能完成了囱井。
此外驹尼,在搜索解決方案的過程中,也發(fā)現(xiàn)了兩個有用的小技巧庞呕,順便記錄在這里(跟上面的內(nèi)容無關(guān)哈)新翎。
對于@PathVariable
還有另外一種解決方案(嚴(yán)格來說作為驗(yàn)證并不完善),通過正則表達(dá)式來進(jìn)行匹配(這種方式不支持長度限制住练,但可以進(jìn)行類型限制地啰,如只包含字符,只包含數(shù)字等)讲逛,如下所示
UserController.java
@GetMapping("/{name:[a-zA-Z]+")
public User getUserByNameRegex(@PathVariable String name) {
User user = new User();
user.setName(name);
return user;
}
對于@RequestParam
來說亏吝,可以使用默認(rèn)值來實(shí)現(xiàn)可變化的參數(shù)列表,如下所示
UserController.java
@GetMapping
public User getUser(
@RequestParam(required = true, value = "name") String name,
@RequestParam(value = "age", defaultValue = "22") int age) {
return new User();
}
更復(fù)雜的如分頁盏混,排序等等蔚鸥,可以通過默認(rèn)參數(shù)的形式,來實(shí)現(xiàn)许赃,而不再需要強(qiáng)制調(diào)用者輸入對應(yīng)的參數(shù)(畢竟這些參數(shù)是可選的嘛)止喷。
常用驗(yàn)證注解
常用的注解主要有以下幾個,作用及內(nèi)容如下所示
-
@Null
图焰,標(biāo)注的屬性值必須為空 -
@NotNull
启盛,標(biāo)注的屬性值不能為空 -
@AssertTrue
,標(biāo)注的屬性值必須為true -
@AssertFalse
技羔,標(biāo)注的屬性值必須為false -
@Min
僵闯,標(biāo)注的屬性值不能小于min中指定的值 -
@Max
,標(biāo)注的屬性值不能大于max中指定的值 -
@DecimalMin
藤滥,小數(shù)值鳖粟,同上 -
@DecimalMax
,小數(shù)值拙绊,同上 -
@Negative
向图,負(fù)數(shù) -
@NegativeOrZero
泳秀,0或者負(fù)數(shù) -
@Positive
,整數(shù) -
@PositiveOrZero
榄攀,0或者整數(shù) -
@Size
嗜傅,指定字符串長度,注意是長度檩赢,有兩個值吕嘀,min以及max,用于指定最小以及最大長度 -
@Digits
贞瞒,內(nèi)容必須是數(shù)字 -
@Past
偶房,時間必須是過去的時間 -
@PastOrPresent
,過去或者現(xiàn)在的時間 -
@Future
军浆,將來的時間 -
@FutureOrPresent
棕洋,將來或者現(xiàn)在的時間 -
@Pattern
,用于指定一個正則表達(dá)式 -
@NotEmpty
乒融,字符串內(nèi)容非空 -
@NotBlank
掰盘,字符串內(nèi)容非空且長度大于0 -
@Email
,郵箱 -
@Range
赞季,用于指定數(shù)字庆杜,注意是數(shù)字的范圍,有兩個值碟摆,min以及max
總結(jié)
本小節(jié)主要學(xué)習(xí)了如何在SpringBoot中使用Hibernate-Validator晃财,驗(yàn)證器的作用在于驗(yàn)證參數(shù)是否符合規(guī)定,通過配置驗(yàn)證器以及對應(yīng)的異常處理器典蜕,可以使我們從繁瑣的驗(yàn)證流程中解脫出來断盛,當(dāng)然,對于復(fù)雜的驗(yàn)證愉舔,其實(shí)還是要手動驗(yàn)證的钢猛,驗(yàn)證器能提供的是一些通用的,常規(guī)的驗(yàn)證操作轩缤,當(dāng)然命迈,大部分情況下已經(jīng)足夠了。