1. 前言
數(shù)據(jù)字段一般都要遵循業(yè)務要求和數(shù)據(jù)庫設計肝集,所以后端的參數(shù)校驗是必須的瞻坝,應用程序必須通過某種手段來確保輸入進來的數(shù)據(jù)從語義上來講是正確的。
2. 數(shù)據(jù)校驗的痛點
為了保證數(shù)據(jù)語義的正確杏瞻,我們需要進行大量的判斷來處理驗證邏輯所刀。而且項目的分層也會造成一些重復的校驗衙荐,產生大量與業(yè)務無關的代碼。不利于代碼的維護浮创,增加了開發(fā)人員的工作量忧吟。
3. JSR 303校驗規(guī)范及其實現(xiàn)
為了解決上面的痛點,將驗證邏輯與相應的領域模型進行綁定是十分有必要的斩披。為此產生了JSR 303 – Bean Validation 規(guī)范溜族。Hibernate Validator 是JSR-303的參考實現(xiàn),它提供了JSR 303規(guī)范中所有的約束(constraint)的實現(xiàn)雏掠,同時也增加了一些擴展斩祭。
Hibernate Validator 提供的常用的約束注解
約束注解 | 詳細信息 |
---|---|
@Null |
被注釋的元素必須為 null
|
@NotNull |
被注釋的元素必須不為 null
|
@AssertTrue |
被注釋的元素必須為 true
|
@AssertFalse |
被注釋的元素必須為 false
|
@Min(value) |
被注釋的元素必須是一個數(shù)字,其值必須大于等于指定的最小值 |
@Max(value) |
被注釋的元素必須是一個數(shù)字乡话,其值必須小于等于指定的最大值 |
@DecimalMin(value) |
被注釋的元素必須是一個數(shù)字摧玫,其值必須大于等于指定的最小值 |
@DecimalMax(value) |
被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值 |
@Size(max, min) |
被注釋的元素的大小必須在指定的范圍內 |
@Digits (integer, fraction) |
被注釋的元素必須是一個數(shù)字绑青,其值必須在可接受的范圍內 |
@Past |
被注釋的元素必須是一個過去的日期 |
@Future |
被注釋的元素必須是一個將來的日期 |
@Pattern(value) |
被注釋的元素必須符合指定的正則表達式 |
@Email |
被注釋的元素必須是電子郵箱地址 |
@Length |
被注釋的字符串的大小必須在指定的范圍內 |
@NotEmpty |
被注釋的字符串的必須非空 |
@Range |
被注釋的元素必須在合適的范圍內 |
4. 驗證注解的使用
在Spring Boot開發(fā)中使用Hibernate Validator是非常容易的诬像,引入下面的starter就可以了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
一種可以實現(xiàn)接口來定制Validator
,一種是使用約束注解闸婴。胖哥覺得注解可以滿足絕大部分的需求坏挠,所以建議使用注解來進行數(shù)據(jù)校驗。而且注解更加靈活邪乍,控制的粒度也更加細降狠。接下來我們來學習如何使用注解進行數(shù)據(jù)校驗。
4.1 約束注解的基本使用
我們對需要校驗的方法入?yún)⑦M行注解約束標記庇楞,例子如下:
@Data
public class Student {
@NotBlank(message = "姓名必須填")
private String name;
@NotNull(message = "年齡必須填寫")
@Range(min = 1,max =50, message = "年齡取值范圍1-50")
private Integer age;
@NotEmpty(message = "成績必填")
private List<Double> scores;
}
POST請求
然后定義一個POST請求的Spring MVC接口:
@RestController
@RequestMapping("/student")
public class StudentController {
@PostMapping("/add")
public Rest<?> addStudent(@Valid @RequestBody Student student) {
return RestBody.okData(student);
}
}
通過對addStudent
方法入?yún)⑻砑?code>@Valid來啟用參數(shù)校驗榜配。當使用下面數(shù)據(jù)進行請求將會拋出MethodArgumentNotValidException
異常,提示age
范圍超出1-50
吕晌。
POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json
{
"name": "felord.cn",
"age": 77,
"scores": [
55
]
}
GET請求
如法炮制蛋褥,我們定義一個GET請求的接口:
@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {
return RestBody.okData(student);
}
使用下面的請求可以正確對學生分數(shù)scores
進行了校驗,但是拋出的并不是MethodArgumentNotValidException
異常睛驳,而是BindException
異常烙心。這和使用@RequestBody
注解有關系,這對我們后面的統(tǒng)一處理非常十分重要乏沸。
GET /student/get?name=felord.cn&age=12 HTTP/1.1
Host: localhost:8888
自定義注解
可能有些同學注意到上面的年齡我進行了這樣的標記:
@NotNull(message = "年齡必須填寫")
@Range(min = 1,max =50, message = "年齡取值范圍1-50")
private Integer age;
這是因為@Range
不會去校驗為空的情況淫茵,它只處理非空的時候是否符合范圍約束。所以要用多個注解來約束屎蜓。如果我們某些場景需要重復的捆綁多個注解來使用時痘昌,可以使用自定義注解將它們封裝起來組合使用,下面這個注解就是將@NotNull
和@Range
進行了組合炬转,你可以仿一個出來用用看辆苔。
import org.hibernate.validator.constraints.Range;
import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;
/**
* @author a
* @since 17:31
**/
@Constraint(
validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD,
ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {
// message 必須有
String message() default "年齡必須填寫,且范圍為 1-50 ";
// 可選
Class<?>[] groups() default {};
// 可選
Class<? extends Payload>[] payload() default {};
}
還有一種情況扼劈,我們在后臺定義了枚舉值來進行狀態(tài)的流轉驻啤,也是需要校驗的,比如我們定義了顏色枚舉:
public enum Colors {
RED, YELLOW, BLUE
}
我們希望入?yún)⒉荒艹?code>Colors的范圍["RED", "YELLOW", "BLUE"]
荐吵,這就需要實現(xiàn)ConstraintValidator<A extends Annotation, T>
接口來定義一個顏色約束了骑冗,其中泛型A
為自定義的約束注解,泛型T
為入?yún)⒌念愋拖燃澹@里使用字符串,然后我們的實現(xiàn)如下:
/**
* @author felord.cn
* @since 17:57
**/
public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();
@Override
public void initialize(Color constraintAnnotation) {
Colors[] value = constraintAnnotation.value();
List<String> list = Arrays.stream(value)
.map(Enum::name)
.collect(Collectors.toList());
COLOR_CONSTRAINTS.addAll(list);
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return COLOR_CONSTRAINTS.contains(value);
}
}
然后聲明對應的約束注解Color
贼涩,需要在元注解@Constraint
中指明使用上面定義好的處理類ColorConstraintValidator
進行校驗。
/**
* @author felord.cn
* @since 17:55
**/
@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,
ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
// 錯誤提示信息
String message() default "顏色不符合規(guī)格";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 約束的類型
Colors[] value();
}
然后我們來試一下薯蝎,先對參數(shù)進行約束:
@Data
public class Param {
@Color({Colors.BLUE,Colors.YELLOW})
private String color;
}
接口跟上面幾個一樣遥倦,調用下面的接口將拋出BindException
異常:
GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888
當我們把參數(shù)color
賦值為BLUE
或者YELLOW
后,能夠成功得到響應占锯。
4.2 常見問題
在實際使用起來我們會遇到一些問題袒哥,這里總結了一些常見的問題和處理方式。
檢驗基礎類型不生效的問題
上面為了校驗顏色我們聲明了一個Param
對象來包裝唯一的字符串參數(shù)color
消略,為什么直接使用下面的方式定義呢堡称?
@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {
return RestBody.okData(color);
}
或者使用路徑變量:
@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {
return RestBody.okData(color);
}
上面兩種方式是不會生效的。不信你可以試一試艺演,起碼在Spring Boot 2.3.1.RELEASE是不會直接生效的却紧。
使以上兩種生效的方法是在類上添加@Validated
注解。注意一定要添加到方法所在的類上才行胎撤。這時候會拋出ConstraintViolationException
異常晓殊。
集合類型參數(shù)中的元素不生效的問題
就像下面的寫法,方法的參數(shù)為集合時哩照,如何檢驗元素的約束呢挺物?
/**
* 集合類型參數(shù)元素.
*
* @param student the student
* @return the rest
*/
@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
return RestBody.okData(student);
}
同樣是在類上添加@Validated
注解。注意一定要添加到方法所在的類上才行飘弧。這時候會拋出ConstraintViolationException
異常识藤。
嵌套校驗不生效
嵌套的結構如何校驗呢?打個比方次伶,如果我們在學生類Student
中添加了其所屬的學校信息School
并希望對School
的屬性進行校驗痴昧。
@Data
public class Student {
@NotBlank(message = "姓名必須填")
private String name;
@Age
private Integer age;
@NotEmpty(message = "成績必填")
private List<Double> scores;
@NotNull(message = "學校不能為空")
private School school;
}
@Data
public class School {
@NotBlank(message = "學校名稱不能為空")
private String name;
@Min(value = 0,message ="校齡大于0" )
private Integer age;
}
當 GET請求時正常校驗了School
的屬性,但是POST請求卻無法對School
的屬性進行校驗冠王。這時我們只需要在該屬性上加上@Valid
注解即可赶撰。
@Data
public class Student {
@NotBlank(message = "姓名必須填")
private String name;
@Age
private Integer age;
@NotEmpty(message = "成績必填")
private List<Double> scores;
@Valid
@NotNull(message = "學校不能為空")
private School school;
}
每加一層嵌套都需要加一層
@Valid
注解。通常在校驗對象屬性時,@NotNull
豪娜、@NotEmpty
和@Valid
配合才能起到校驗效果餐胀。
如果你有其它問題可以通過felord.cn聯(lián)系到我探討。
5. 總結
通過校驗框架我們可以專心于業(yè)務開發(fā)瘤载,本文對Hibernate Validator的使用和一些常見問題進行了梳理否灾。我們可以通過Spring Boot統(tǒng)一異常處理來解決參數(shù)校驗的異常信息的提示問題。具體可以通過關注:碼農小胖哥 回復 valid獲取相關DEMO鸣奔。
關注公眾號:碼農小胖哥墨技,獲取更多資訊