該功能由[`Hibernate-validator`](http://hibernate.org/validator/releases/6.0/)封裝而來
> 在任何時候,當你要處理一個應用程序的業務邏輯,數據校驗是你必須要考慮和面對的事情。應用程序必須通過某種手段來確保輸入進來的數據從語義上來講是正確的,通常我們會有一個驗證數據的過程,待這些驗證過程完畢,結果無誤后,參數才會進入到正式的業務處理中。
> 我們在開發過程中,會有各種各樣的入參,會經常需要寫一些校驗的代碼,比如字段非空,字段長度限制,郵箱格式驗證等等,寫這些與業務邏輯關系不大的代碼個人感覺不夠優雅:
> * 驗證代碼重復繁瑣
> * 方法內代碼冗余
`Hibernate-validator`應運而生,與持久層框架`Hibernate`沒有什么關系,是對`JSR 380(Bean Validation 2.0)`、`JSR 303(Bean Validation 1.0)`規范的實現;部分注解如下:

[TOC]
# 依賴模塊
```
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
```
> 下邊主要從4個方面了解如何使用;
> * 1、簡單校驗
> * 2、分組校驗
> * 3、自定義校驗
> * 4、對象校驗
# 1、簡單校驗
方式1:走的是 Hibernate校驗,失敗則拋出`ConstraintViolationException`異常
TestController.java
```
@RestController
@RequestMapping(value = "/api")
@Validated
public class TestController{
@RequestMapping("/test")
public Result test(@NotNull(message = "參數不能為空") String key) {
return R.succ(key);
}
}
```
方式1:校驗結果
```
[FastBoot][ERROR][08-29 17:19:55]-->[http-nio-9090-exec-3:39648][validatorException(GlobalExceptionAdvice.java:84)] | - validatorException ......
javax.validation.ConstraintViolationException: test.key: 參數不能為空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE]
```
```
{
"code": 3000,
"msg": "效驗錯誤",
"data": [
"test.key 參數不能為空"
],
"success": false
}
```
方式2:走的是 Spring校驗,失敗則拋出`BindException`異常
TestController.java
```
@RestController
@RequestMapping(value = "/api")
public class TestController{
@RequestMapping("/test")
public Result test(@Validated TestVo vo) {
return R.succ(vo.getKey());
}
}
```
TestVo.java
```
@Data
public class TestVo {
@NotNull(message = "參數不能為空")
private String key;
}
```
方式2:校驗結果
```
[FastBoot][ERROR][08-29 17:28:30]-->[http-nio-9090-exec-1:24367][validatorException(GlobalExceptionAdvice.java:84)] | - validatorException ......
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'testVo' on field 'key': rejected value [null]; codes [NotNull.testVo.key,NotNull.key,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testVo.key,key]; arguments []; default message [key]]; default message [參數不能為空]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
```
```
{
"code": 3000,
"msg": "效驗錯誤",
"data": [
"key 參數不能為空"
],
"success": false
}
```
不管是方式1/2都,校驗失敗后會被全局`異常捕獲`處理,再`統一返回`規定格式
# 2、分組校驗
實際開發場景中:
> 查詢用戶接口A,必傳:id
> 修改昵稱接口B,必傳:id,nickname
> 修改手機接口C,必傳:id,code,phone
假設我們每類業務邏輯都有一個VO來接收參數,每個定義N個字段,但每次操作并不需要所有都是必傳,可能只是個別參數必傳,如何優雅的解決?
TestVo.java
```
@Data
public class TestVo {
@NotNull(message = "用戶ID不能為空")
private String id;
@NotNull(message = "昵稱不能為空", groups = B.class)
private String nickname;
@NotNull(message = "驗證碼不能為空", groups = C.class)
private String code;
@NotNull(message = "手機不能為空", groups = C.class)
private String phone;
public interface TestVoValid {
public interface A{}
public interface B{}
public interface C{}
}
}
```
TestController.java
```
@RestController
@RequestMapping(value = "/api")
public class TestController{
// 校驗,默認分組 Default.class (注:沒有使用分組A,默認使用Default)
@RequestMapping("/test")
public Result test(@Validated TestVo vo) {
return R.succ(vo.getId());
}
// 校驗,默認分組 Default.class + B.class
@RequestMapping("/update")
public Result update(@Validated(value = {Default.class, B.class}) TestVo vo) {
return R.succ(vo.getNickname());
}
// 校驗,默認分組 Default.class + C.class
@RequestMapping("/change")
public Result change(@Validated(value = {Default.class, C.class}) TestVo vo) {
return R.succ(vo.getPhone());
}
}
```
校驗結果
> 本框架默認是`快速模式`即只要有一個不通過就返回;
> 原默認是`普通模式`即會返回所有的驗證不通過信息集合
1、當請求A時,id不傳時,校驗錯誤
默認、快速模式均返回
```
{"code":3000,"msg":"校驗錯誤","data":["id 用戶ID不能為空"],"success":false}
```
2、當請求B時,id、nickname有一個不傳時,校驗錯誤
2.1快速模式:
```
{"code":3000,"msg":"校驗錯誤","data":["xxx不能為空"],"success":false}
```
2.2默認模式
```
{"code":3000,"msg":"校驗錯誤","data":["id 用戶ID不能為空","nickname 昵稱不能為空"],"success":false}
```
3、當請求C時,id、code、phone有一個不傳時,校驗錯誤
3.1快速模式:
```
{"code":3000,"msg":"校驗錯誤","data":["xxx不能為空"],"success":false}
```
3.2默認模式
```
{"code":3000,"msg":"校驗錯誤","data":["id 用戶ID不能為空","code 驗證碼不能為空","phone 手機不能為空"],"success":false}
```
# 3、自定義效驗
在上述分組示例中,很明顯手機號是11位數字類型,在內置注解中無法只用單個注解同時滿足3個條件
> * 長度11位
> * 0-9數字組成
> * 手機號段
所以需要自定義處理,這里又可以分2種:
> * 組合驗證:通過內置的注解組合驗證(如:驗證手機號)
VMobile.java
```
@Documented
// 申明注解的作用位置
@Target({ANNOTATION_TYPE, FIELD, METHOD, PARAMETER})
// 運行時機
@Retention(RUNTIME)
// 定義對應的校驗器,自定義注解必須指定
@Constraint(validatedBy = {})
// 不能為空
@NotBlank(message = "{FastBoot.empty}")
// 驗證手機號(第1為:1) + (第2位:可能是3/4/5/7/8等的任意一個) + (第3位:0-9) + d表示數字[0-9]的8位,總共加起來11位
@Pattern(regexp = "(?:0|86|\\+86)?1[3-9]\\d{9}", message = "{fb.mobile}")
public @interface VMobile {
String message() default "{FastBoot.error}";// 錯誤提示信息默認值,可以使用el表達式。
Class<?>[] groups() default {};// 約束注解在驗證時所屬的組別
Class<? extends Payload>[] payload() default {};// 約束注解的有效負載
}
```
> * 自定義校驗器:通過校驗器來驗證(如:驗證JSON格式)
VJson.java
```
@Documented
// 申明注解的作用位置
@Target({ANNOTATION_TYPE, FIELD, METHOD, PARAMETER})
// 運行時機
@Retention(RUNTIME)
// 定義對應的校驗器,自定義注解必須指定
@Constraint(validatedBy = {VJsonRule.class})
// 不能為空
@NotBlank(message = "{FastBoot.empty}")
public @interface VJson {
String message() default "{FastBoot.json}";// 錯誤提示信息默認值,可以使用el表達式。
Class<?>[] groups() default {};// 約束注解在驗證時所屬的組別
Class<? extends Payload>[] payload() default {};// 約束注解的有效負載
}
```
VJsonRule.java
```
public class VJsonRule implements ConstraintValidator<VJson, String> {
@Override
public void initialize(VJson json) {}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
·if (CharSequenceUtil.isBlank(s)) {
return false;
}
return JSONValidator.from(s).validate();
}
}
```
在VO中,則可以使用我們自定義的@VMobile、@VJson注解,無需重復設置message
```
@VMobile
private String phone;
@VJson
private String json;
```
# 4、對象校驗

上述1.2.3都是接收數據時校驗VO,那么在業務對象BO,持久對象PO這類如何去校驗?
```
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = FastBootApplication.class)
public class ValidatorHelperTest {
@Autowired
Validator validator;
@Test
public void verify1() {
TestVo vo = new TestVo();
Set<ConstraintViolation<TestVo>> violations = validator.validate(vo, Default.class);
List<String> message = ValidatorHelper.extractMessage(violations);
log.info(JSON.toJSONString(R.succ(message)));
}
@Test
public void verify2() {
TestVo vo = new TestVo();
try {
ValidatorHelper.validate(vo);
} catch (ConstraintViolationException e) {
// 打印 messgae
List<String> message1 = ValidatorHelper.extractMessage(e);
log.info(JSON.toJSONString(R.succ(message1)));
// 打印property + messgae
Map<String, String> message2 = ValidatorHelper.extractPropertyAndMessage(e);
log.info(JSON.toJSONString(R.succ(message2)));
// 打印property + messgae
List<String> message3 = ValidatorHelper.extractPropertyAndMessageAsList(e);
log.info(JSON.toJSONString(R.succ(message3)));
}
}
}
```
本框架中默認注入Validator,直接使用即可
方式1
```
@Autowired
Validator validator;
```
方式2
```
Validator validator = SpringHelper.getBean(Validator.class);
```
封裝方法
```
public static void validate(Object object) throws ConstraintViolationException
public static void validate(Object object, Class<?>... groups) throws ConstraintViolationException
public static List<String> extractMessage(ConstraintViolationException e)
public static List<String> extractMessage(Set<? extends ConstraintViolation> constraintViolations)
public static Map<String, String> extractPropertyAndMessage(ConstraintViolationException e)
public static Map<String, String> extractPropertyAndMessage(Set<? extends ConstraintViolation> constraintViolations)
public static List<String> extractPropertyAndMessageAsList(ConstraintViolationException e)
public static List<String> extractPropertyAndMessageAsList(Set<? extends ConstraintViolation> constraintViolations)
public static List<String> extractPropertyAndMessageAsList(ConstraintViolationException e, String separator)
public static List<String> extractPropertyAndMessageAsList(Set<? extends ConstraintViolation> constraintViolations, String separator)
```
校驗結果
```
[FastBoot][ INFO][08-29 23:14:28]-->[main: 5648][verify1(ValidatorHelperTest.java:39)] | - {"code":0,"msg":"操作成功","data":["參數不能為空"],"success":true}
[FastBoot][ INFO][08-29 23:14:28]-->[main: 5660][verify2(ValidatorHelperTest.java:50)] | - {"code":0,"msg":"操作成功","data":["參數不能為空"],"success":true}
[FastBoot][ INFO][08-29 23:14:28]-->[main: 5662][verify2(ValidatorHelperTest.java:54)] | - {"code":0,"msg":"操作成功","data":{"id":"參數不能為空"},"success":true}
[FastBoot][ INFO][08-29 23:14:28]-->[main: 5663][verify2(ValidatorHelperTest.java:58)] | - {"code":0,"msg":"操作成功","data":["id 參數不能為空"],"success":true}
```
以上4點是在編寫GOTV服務中最常用的效驗方式。`Hibernate-Validator`遠不僅如此,還有很多如校驗方式... 當實際場景已無法滿足或用起來不是那么優雅的時候,更多見[官方文檔](http://hibernate.org/validator/documentation/)~