簡(jiǎn)介
Spring Validation是在Spring Context包下的,在Spring Boot項(xiàng)目中,我們引入spring-boot-starter-web便會(huì)引入進(jìn)來(lái),Spring Validation是對(duì)Hibernate Validator的二次封裝,使我們可以更方便的在Spring MVC中完成自動(dòng)校驗(yàn)。
Hibernate Validator是對(duì)JSR-303(Bean Validation)的參考實(shí)現(xiàn)。Hibernate Validator 提供了JSR-303規(guī)范中所有內(nèi)置constraint的實(shí)現(xiàn),除此之外還有一些附加的constraint。
JSR-303定義的constraint:
| Constraint | Description |
|---|---|
| @Null | 被注解的元素必須為null
|
| @NotNull | 被注解的元素必須不為null
|
| @AssertTure | 被注解的元素必須為ture
|
| @AssertFalse | 被注解的元素必須為false
|
| @Min(value) | 被注解的元素必須是數(shù)字且必須大于等于指定值 |
| @Max(value) | 被注解的元素必須是數(shù)字且必須小于等于指定值 |
| @DecimalMin(value) | 被注解的元素必須是數(shù)字且必須大于等于指定值 |
| @DecimalMax(value) | 被注解的元素必須是數(shù)字且必須小于等于指定值 |
| @Size(max, min) | 被注解的元素必須在指定的范圍內(nèi) |
| @Digits(integer, fraction) | 被注解的元素必須是數(shù)字且其值必須在給定的范圍內(nèi) |
| @Past | 被注解的元素必須是一個(gè)過(guò)去的日期 |
| @Future | 被注解的元素必須是一個(gè)將來(lái)的日期 |
| @Pattern(value) | 被注解的元素必須符合給定正則表達(dá)式 |
Hibernate Validator附加實(shí)現(xiàn)的constraint
| Constraint | Description |
|---|---|
被注解的元素必須是Email地址 |
|
| @Length(min, max) | 被注解的元素長(zhǎng)度必須在指定的范圍內(nèi) |
| @NotEmpty | 被注解的元素必須非空 |
| @Range | 被注解的元素(可以是數(shù)字或者表示數(shù)字的字符串)必須在給定的范圍內(nèi) |
| @URL | 被注解的元素必須是URL
|
當(dāng)然,我們也可以自定義實(shí)現(xiàn),自定義實(shí)現(xiàn)在下面使用中在講吧。
使用
首先是引入依賴(lài),在Spring Boot項(xiàng)目中,我們引入web就已經(jīng)可以使用了,這里就不再具體贅述了。?
使用@Validated注解攔截校驗(yàn)
先定義下要校驗(yàn)的實(shí)體吧:
public class User {
@Length(min = 1, max = 22, message = "name字段不合法")
private String name;
@Min(value = 1, message = "age字段不合法")
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
在Controller中,我們需要校驗(yàn)前端傳遞過(guò)來(lái)的參數(shù),我們可以這么寫(xiě)
@RestController
public class TestController {
@PostMapping("/test")
public Object test(@RequestBody @Validated User user, BindingResult result) {
if (result.hasErrors()) {
return result.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
}
return user;
}
}
只需要在需要校驗(yàn)的實(shí)體前面打上@Validated注解就可以了,這時(shí)候,如果我們傳遞的參數(shù)符合要求,則會(huì)正常返回。否則返回:
[
"age字段不合法",
"name字段不合法"
]
它會(huì)將我們所有不合法信息一次性全部返回,在日常開(kāi)發(fā)中,我們可以吧校驗(yàn)BindingResult是否有錯(cuò)誤信息的校驗(yàn)統(tǒng)一抽出到一個(gè)工具類(lèi)中去做處理,使用項(xiàng)目中統(tǒng)一格式返回錯(cuò)誤信息就好。這就是一個(gè)最簡(jiǎn)單的校驗(yàn)示例了,其他注解也都是類(lèi)似的,就不多舉例了,可以自己嘗試著玩玩。
在日常開(kāi)發(fā)中想必都曾遇到過(guò)這樣的需求,比如這個(gè)age這個(gè)字段,我想要這個(gè)字段只在PC端校驗(yàn),在App端不做限制,這就需要用到分組校驗(yàn)了,每個(gè)注解都提供了一個(gè)group屬性,利用這個(gè)屬性就可以輕易做到以上需求。比如在User上的注解中加入group屬性,指定其被校驗(yàn)的group:
public class User {
@Length(min = 1, max = 22, message = "name字段不合法", groups = {App.class, PC.class})
private String name;
@Min(value = 1, message = "age字段不合法", groups = PC.class)
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
在Controller中的@Validated中指定當(dāng)前group:
@RestController
public class TestController {
@PostMapping("/test")
public Object test(@RequestBody @Validated(App.class) User user, BindingResult result) {
if (result.hasErrors()) {
return result.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
}
return user;
}
}
這時(shí)候我再使用兩個(gè)不合法字段訪(fǎng)問(wèn)返回:
[
"name字段不合法"
]
可以看到,它并沒(méi)有對(duì)age字段進(jìn)行校驗(yàn)。這就是它的分組校驗(yàn)。
在方法實(shí)現(xiàn)中攔截校驗(yàn)
它不只是在Controller校驗(yàn)前端傳遞過(guò)來(lái)的參數(shù)的時(shí)候可以用,它在方法中同樣可以用,我們可以這樣來(lái)使用:
@RestController
public class TestController {
@Autowired
ObjectMapper objectMapper;
@Autowired
SmartValidator smartValidator;
@GetMapping("/test")
public Object test() {
String context = "{\"name\": \"felixu\",\"age\": 0}";
User user = null;
try {
user = objectMapper.readValue(context, User.class);
} catch (IOException e) {
e.printStackTrace();
}
BeanPropertyBindingResult result = new BeanPropertyBindingResult(user, "user");
smartValidator.validate(user, result);
if (result.hasErrors()) {
return result.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.toList());
}
return user;
}
}
使用需要被校驗(yàn)的實(shí)體構(gòu)造BeanPropertyBindingResult對(duì)象,然后將傳遞給SmartValidator的validate方法來(lái)完成跟上面相同的校驗(yàn)。validate有個(gè)重載方法,也接收分組,所以這種方式同樣可以實(shí)現(xiàn)分組校驗(yàn)。
自定義實(shí)現(xiàn)
需求總是多變的,有時(shí)候,可能上面的校驗(yàn)方式并不能滿(mǎn)足我們的要求,這時(shí)候就需要我們自定義一下校驗(yàn)了,要做到自定義注解來(lái)校驗(yàn),我們需要做以下兩步,首先實(shí)現(xiàn)ConstraintValidator<A extends Annotation, T>(ps:原諒我的自戀。。。不要管我干了啥,關(guān)鍵是要知道可以用來(lái)干啥對(duì)不對(duì),哈哈哈哈):
public class IsFelixuValidator implements ConstraintValidator<IsFelixu, String> {
@Override
public void initialize(IsFelixu annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if ("felixu".equals(value)) {
return true;
}
return false;
}
}
isValid便是我們的校驗(yàn)邏輯,true為通過(guò)校驗(yàn)。
然后我們實(shí)現(xiàn)注解:
@Documented
@Constraint(
// 指定對(duì)應(yīng)的校驗(yàn)類(lèi)
validatedBy = {IsFelixuValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface IsFelixu {
String message() default "this value is not felixu";
// 這兩個(gè)屬性必須要存在
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
這樣就ok了,我們繼續(xù)使用之前的來(lái)做測(cè)試,在User的name屬性上加上@IsFelixu注解,此時(shí)測(cè)試,如果不傳遞name為felixu的值,則會(huì)提示如下信息:
[
"this value is not felixu",
"age字段不合法"
]
這個(gè)多多少少看著有點(diǎn)沙雕,我決定拿個(gè)別的舉例了,先看注解
@Documented
@Constraint(
validatedBy = {PrecisionValidator.class}
)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Precision {
String message() default "精度不符合要求";
// 最少小數(shù)位
int min() default 0;
// 最多小數(shù)位
int max() default Integer.MAX_VALUE;
// 是否固定小數(shù)位,固定多少位,-1 為不固定
int fixed() default -1;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
emmmm,這是個(gè)校驗(yàn)精度的,我們可以看到它有好幾個(gè)屬性,比如min啊、max的之類(lèi)的,就是為了標(biāo)注當(dāng)前值要最小幾位小數(shù),最多多少位小數(shù)。
下面我們?cè)賮?lái)看看是怎么實(shí)現(xiàn)校驗(yàn)的:
public class PrecisionValidator implements ConstraintValidator<Precision, Object> {
// Hibernate validator 自帶的一些錯(cuò)誤提示
private static final Log LOG = LoggerFactory.make(MethodHandles.lookup());
private int min;
private int max;
private int fixed;
// 項(xiàng)目啟動(dòng)時(shí),此方法便會(huì)被調(diào)用,可以拿到注解中各屬性的值,用以做合法性校驗(yàn)
// 比如這里就檢查了這三個(gè)屬性的合法性
@Override
public void initialize(Precision precision) {
min = precision.min();
max = precision.max();
fixed = precision.fixed();
validateParameters();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// 數(shù)字轉(zhuǎn)字符串
if (value instanceof Number)
value = String.valueOf(value);
if (value instanceof CharSequence) {
// 這是自己的一個(gè)工具類(lèi),可以不用管,就是把 value 以 . 切割成數(shù)組
List<String> vals = Splitters.DOT.splitToList((String) value);
// 如果只有 1 位,說(shuō)明沒(méi)有小數(shù)位,直接放行
if (vals.size() == 1)
return true;
// 不是 2 位說(shuō)明參數(shù)是瞎特么傳的
if (vals.size() != 2)
return false;
// 獲取小數(shù)位的長(zhǎng)度
int length = vals.get(1).length();
// 判斷有沒(méi)有指定固定小數(shù)位, -1 認(rèn)為是沒(méi)有
if (fixed != -1)
return length == fixed;
// 判斷長(zhǎng)度是否在范圍內(nèi)
return length >= min && length <= max;
}
return false;
}
// 注解屬性值校驗(yàn)方法
private void validateParameters() {
if (min < 0)
throw LOG.getMinCannotBeNegativeException();
if (max < 0)
throw LOG.getMaxCannotBeNegativeException();
if (max < min)
throw LOG.getLengthCannotBeNegativeException();
if (fixed < 0 && fixed != -1)
throw new IllegalArgumentException("The fixed cannot be negative.");
}
}
之前實(shí)現(xiàn)那個(gè)沙雕注解的時(shí)候有些方法沒(méi)有詳細(xì)介紹,可以看一下上面這個(gè)實(shí)現(xiàn)中的注釋。當(dāng)時(shí)這個(gè)是為了滿(mǎn)足產(chǎn)品的一個(gè)沙雕要求,正好拿出來(lái)舉個(gè)??。
補(bǔ)充說(shuō)明
在上面舉例中,我在controller中注入了BindingResult來(lái)獲取錯(cuò)誤信息,可以將錯(cuò)誤信息封裝到一個(gè)工具類(lèi)中來(lái)統(tǒng)一返回,例如下面代碼中的onValidFail方法,將錯(cuò)誤信息封裝到統(tǒng)一返回中
public class RespDTO<T> implements Serializable{
public int code;
public String error;
public T data;
public static <T> RespDTO<T> onSuc() {
return onSuc(null);
}
public static <T> RespDTO<T> onSuc(T data) {
return build(ErrorCode.OK.getCode(), ErrorCode.OK.getMsg(), data);
}
public static <T> RespDTO<T> onValidFail(BindingResult result) {
String errorMsg = result.getAllErrors()
.stream()
.map(objectError -> {
FieldError error = (FieldError) objectError;
return error.getField() + ", " + error.getDefaultMessage();
})
.collect(Collectors.joining("\n"));
return build(ErrorCode.PARAM_ERROR.getCode(), errorMsg, null);
}
public static <T> RespDTO<T> onFail(ErrorCode errorCode) {
return onFail(errorCode.getCode(), errorCode.getMsg());
}
public static <T> RespDTO<T> onFail(int code, String msg) {
return build(code, msg, null);
}
private static <T> RespDTO<T> build(int ret, String msg, T data) {
return new RespDTO<T>(ret, msg, data);
}
}
當(dāng)然我們也可以不去注入BindingResult,而直接使用注解,這樣校驗(yàn)失敗就會(huì)拋出異常,再由我們自己的統(tǒng)一異常攔截去攔截,之后再處理成統(tǒng)一返回,這樣也是可以的,比如下面這樣寫(xiě)controller
public class DemoController {
@PostMapping
public RespDTO<Boolean> create(@Validated({Create.class, Default.class}) @RequestBody RoutineInfo routineInfo) {
Account account = accountService.getDefaultAccount(routineInfo.getUserId());
routineInfo.setAccountId(account.getId());
return RespDTO.onSuc(routineInfoService.save(routineInfo));
}
}
然后定義異常攔截去攔截:
@RestControllerAdvice
public class DemoExceptionHandler {
@ExceptionHandler(BindException.class)
public ResponseEntity<RespDTO<Object>> handleBindException(BindException e) {
return new ResponseEntity<>(RespDTO.onValidFail(e), HttpStatus.OK);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<RespDTO<Object>> methodArgumentNotValidHandler(MethodArgumentNotValidException e) {
return new ResponseEntity<>(RespDTO.onValidFail(e.getBindingResult()), HttpStatus.OK);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<RespDTO<Object>> validationExceptionHandler(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
Map<String, String> result = new HashMap<>(violations.size());
for (ConstraintViolation<?> violation : violations) {
String fieldName = ((PathImpl) violation.getPropertyPath()).getLeafNode().getName();
result.put(fieldName, violation.getMessage());
}
BindException exception = new BindException(e, "exception");
result.forEach((key, value) -> exception.addError(new FieldError(exception.getObjectName(), key, value)));
return new ResponseEntity<>(RespDTO.onValidFail(exception.getBindingResult()), HttpStatus.OK);
}
}
這樣校驗(yàn)失敗拋出的異常便會(huì)被統(tǒng)一的異常攔截器攔截到,然后被處理成統(tǒng)一返回,返回給前端了。
結(jié)語(yǔ)
JSR-303 的發(fā)布使得在數(shù)據(jù)自動(dòng)綁定和驗(yàn)證變得簡(jiǎn)單,使開(kāi)發(fā)人員在定義數(shù)據(jù)模型時(shí)不必考慮實(shí)現(xiàn)框架的限制。當(dāng)然Bean Validation還只是提供了一些最基本的constraint。
上面只是相對(duì)簡(jiǎn)單的用法,在實(shí)際的開(kāi)發(fā)過(guò)程中,用戶(hù)可以根據(jù)自己的需要組合或開(kāi)發(fā)出更加復(fù)雜的constraint。這就需要想象力了,從上面的用法中應(yīng)該可以想到很多地方可以去使用,但是設(shè)計(jì)和實(shí)現(xiàn)時(shí),往往需要考慮諸多因素,比如易用性和封裝的復(fù)雜度,等等方面,還需要自己去考量了。