以Web專案為例,使用者需要填寫表單資訊儲存提交。
頁面輸入資訊需要進行資料格式校驗,並且返回對應的錯誤提示,以此來達到資料校驗的目的,從而避免無效資料被儲存或者提交。
這些檢查工作包括必填項檢查、數值檢查、長度檢查、身份證號碼、手機號碼檢查等工作。
當請求引數格式不正確的時候,需要程式監測到,對於前後端分離開發過程中,資料校驗還需要返回對應的狀態碼和錯誤提示資訊。
如果將這些欄位校驗和業務邏輯混合一起寫,則會:
接下來將細述在伺服器端,如何對API的資料校驗處理技術。
略,參見
1.0 章節
JSR:Java Specification Requests的縮寫,意思是Java 規範提案。是指向JCP(Java Community Process) 提出新增一個標準化技術規範的正式請求。
任何人都可以提交JSR,以向Java平臺增添新的API和服務。JSR已成為Java界的一個重要標準。
Bean Validation 是一個執行時的資料驗證框架的標準,在驗證之後驗證的錯誤資訊會被馬上返回。
Bean Validation 2.0中包含了22個註解
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${javax.validation-api.version}</version>
</dependency>
重要版本
javax.validation:validation-api.version = 2.0.1.Final
spring-boot-starter-web/validation:2.1.4.RELEASE | hibernate-alidation:6.0.16.Final 中使用的 validation-api 為:
javax.validation:validation-api:2.0.1.Final
hibernate-alidation:6.0.16.Final 中使用的 validation-api 為:
javax.validation:validation-api:2.0.1.Final
javax.validation
包下。jakarta.validation
包下javase
的支援還在jcp(Java Community Process,Java社群標準過程),Java EE 改名 JakartaE E。
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${jakarta.validation-api.version}</version>
</dependency>
重要版本
jakarta.validation:jakarta.validation-api.version = 2.0.2
spring-boot-starter-validation:2.3.12.RELEASE | hibernate-alidation:6.1.7.Final 使用的 validation-api 為:
jakarta.validation:jakarta.validation-api:2.0.2
org.hibernate
)下的專案之一。hibernate-validator
除了提供了JSR 303規範中所有內建constraint 的實現,還有一些附加的constraint。hibernate-validation
已成為【資料校驗框架】實質上的標準框架的標準實現,再無其他框架可望其項背。spring-boot-starter-web/validation:2.1.4.RELEASE | hibernate-alidation:6.0.16.Final
hibernate-alidation:6.0.16.Final
spring-boot-starter-validation:2.3.12.RELEASE | hibernate-alidation:6.1.7.Final
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@NotBlank(message = "使用者名稱不能為空")
private String name;
@NotBlank(message = "郵箱不能為空")
private String email;
}
基於
javax.validation
api 的第三方庫的有:
[1] javax.validation:validation-api : 2.0.1.Final
- [1] hibernate-alidation : 6.0.16.Final
- [2] spring-boot-starter-web/validation : 2.1.4.RELEASE
注:本元件依賴的hibernate-validation
的元件版本為 6.0.16.Final
<!-- | data validation framework | start -->
<!-- javax.validation-api : data validation api design standard & framework -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${javax.validation-api.version}</version>
</dependency>
<!-- hibernate-validator | http://hibernate.org/validator/documentation-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- org.glassfish:javax.el | hibernate-validator 依賴此元件,以防報錯:"javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead" --><dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${glassfish.javax.el.version}</version>
</dependency>
<!-- | data validation framework | end -->
jakarta.validation
api基於
jakarta.validation
api 的第三方庫的有:
[1] jakarta.validation : jakarta.validation-api : 2.0.2
- [1] hibernate-alidation : 6.1.7.Final
- [2] spring-boot-starter-validation : 2.3.12.RELEASE
注:本元件依賴的hibernate-validation
的元件版本為 6.1.7.Final
方式1
<!-- | data validation framework | start -->
<!-- jakarta.validation-api : data validation api design standard & framework -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>${jakarta.validation-api.version}</version>
</dependency>
<!-- hibernate-validator | http://hibernate.org/validator/documentation -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<!-- org.glassfish:javax.el or org.glassfish:jakarta.el [recommend] (2選1即可) | hibernate-validator 依賴此元件,以防報錯:"javax.validation.ValidationException: HV000183: Unable to initialize 'javax.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead" -->
<!--
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>${glassfish.javax.el.version}</version>
</dependency> -->
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>${glassfish.jakarta.el.version}</version>
</dependency>
<!-- | data validation framework | end -->
方式2:直接參照
spring-boot-starter-validation
<!-- | data validation framework | start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- | data validation framework | end -->
版本變數的取值
spring-boot.version := 2.3.12.RELEASE
由上可見:
[1]spring-boot-starter-validation
: 2.3.12.RELEASE
依賴hibernate-validation
: 6.1.7.Final
依賴org.glasssfish : jakarta.el
: 3.0.3
由上可見:
[1]hibernate-validation
: 6.1.7.Final
依賴了org.glasssfish : jakarta.el
(由於scope
是provided
,故具體版本未限制)
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
//import org.hibernate.validator.HibernateValidator;
import java.util.Set;
public class Test {
public static void main(String[] args) {
Student student = new Student("小明", null);
System.out.println(student);
//方式1
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
//方式2
//ValidatorFactory factory = Validation.byProvider( HibernateValidator.class ).configure()
//.addProperty( "hibernate.validator.fail_fast", "true" ) //true 快速失敗返回模式 | false 普通模式
//.buildValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Student>> violations = validator.validate(student);
for (ConstraintViolation<Student> violation : violations) {
System.out.println(violation.getMessage());
}
}
}
output
Student(name=小明, email=null)
十一月 10, 2023 12:56:58 下午 org.hibernate.validator.internal.util.Version <clinit>
INFO: HV000001: Hibernate Validator 6.0.16.Final
郵箱不能為空
在要校驗的POJO上加上以下註解即可:
javax.validation.constraints.Email
註解 | 用途 |
---|---|
Valid (最常用的【標識註解】) | 遞迴的對關聯的物件進行校驗 標記用於驗證級聯的屬性、方法引數或方法返回型別;在驗證屬性、方法引數或方法返回型別時,將驗證在物件及其屬性上定義的約束。此行為是遞迴應用的。 |
AssertFalse | 用於boolean欄位,該欄位的值只能為false |
AssertTrue | 用於boolean欄位,該欄位只能為true |
DecimalMax(value) | 被註釋的元素必須是一個數位,只能大於或等於該值 |
DecimalMin(value) | 被註釋的元素必須是一個數位,只能小於或等於該值 |
Digits(integer,fraction) | 檢查是否是一種數位的(整數,小數)的位數 |
Future | 檢查該欄位的日期是否是屬於將來的日期 |
FutureOrPresent | 判斷日期是否是將來或現在日期 |
Past | 檢查該欄位的日期是在過去 |
PastOrPresent | 判斷日期是否是過去或現在日期 |
Max(value) | 該欄位的值只能小於或等於該值 |
Min(value) | 該欄位的值只能大於或等於該值 |
Negative | 判斷負數 |
NegativeOrZero | 判斷負數或0 |
Positive | 判斷正數 |
PositiveOrZero | 判斷正數或0 |
NotNull | 不能為null |
Null | 必須為 null |
Pattern(value) @Pattern(regexp = ) |
被註釋的元素必須符合指定的正規表示式 |
Size(max, min) | 檢查該欄位的size是否在min和max之間,可以是字串、陣列、集合、Map等 |
Length(max, min) | 判斷字串長度 |
CreditCardNumber | 被註釋的字串必須通過Luhn校驗演演算法,銀行卡,信用卡等號碼一般都用Luhn計算合法性 |
被註釋的元素必須是電子郵箱地址 | |
Length(min=, max=) | 被註釋的字串的大小必須在指定的範圍內 |
NotBlank | 只能用於字串不為null,並且字串trim()以後length要大於0 |
NotEmpty | 集合物件的元素不為0,即集合不為空,也可以用於字串不為null |
Range(min=, max=) | 被註釋的元素必須在合適的範圍內 |
SafeHtml | classpath中要有jsoup包 |
ScriptAssert | 要有Java Scripting API 即JSR 223("Scripting for the JavaTMPlatform")的實現 |
URL(protocol=,host=,port=,regexp=,flags=) | 被註釋的字串必須是一個有效的url |
更多功能,如:自定義校驗規則、分組校驗、關聯引數聯合校驗請檢視官網檔案。
public class ParamTestDTO implements Serializable {
private static final long serialVersionUID = 7123882542534668217L;
@AssertTrue(message = "Error True")
private Boolean testTrue;
@AssertFalse(message = "Error False")
private Boolean testFalse;
@DecimalMax(value = "10", message = "Error StrMax")
private String testStrMax;
@DecimalMin(value = "1", message = "Error StrMin")
private String testStrMin;
@Max(value = 10, message = "Error Max")
private Integer testMax;
@Min(value = 1, message = "Error Min")
private Double testMin;
@Digits(integer = 2, fraction = 3, message = "Error Dig")
private BigDecimal testDig;
@Past(message = "Error Past")
private Date testPast;
@Future(message = "Error Future")
private Date testFuture;
@Null(message = "Error Null")
private String testNull;
@NotNull(message = "Error NonNull")
private String testNonNull;
@Pattern(regexp = "^[0-9]?[0-9]$", message = "Error Pattern")
private String testPattern;
@Size(min = 1, max = 10, message = "Error Size")
private List<String> testSize;
@Length(min = 1, max = 10, message = "Error Length")
private String testLength;
@NotBlank(message = "Error Blank")
private String testBlank;
@NotEmpty(message = "Error NotEmpty")
private String testEmpty;
@Range(min = 1, max = 10, message = "Error Range")
private String testRange;
}
無需util,Dubbo介面設定上的validation為true即可。
<dubbo:reference id="xxxService" interface="xxx.ValidationService" validation="true" />
<dubbo:service interface="xxx.ValidationService" ref="xxxService" validation="true" />
//obj為包含Hibernate Validator註解的POJO
//快速失敗模式
ValidResult validResult = ValidationUtil.fastFailValidate(obj);
//obj為包含Hibernate Validator註解的POJO
//全部校驗模式
ValidResult validResult = ValidationUtil.allCheckValidate(obj);
@RestController
public class StudentController {
// ...
@RequestMapping(value = "/addStudent",method = RequestMethod.POST)
public String addStudent(@Valid @RequestBody Student student){
System.out.println("student = [" + student + "]");
return "ok";
}
// ...
}
@Valid
@Valid
註解:遞迴的對關聯的物件進行校驗import org.springframework.validation.annotation.Validated;
@RestController
@Validated
public class StudentController {
//...
@RequestMapping(value = "/addStudent1",method = RequestMethod.GET)
public String addStudent1(@NotBlank(message = "name不能為空") String name){
System.out.println("name = [" + name + "]");
return "ok addStudent1";
}
//...
}
@Validated
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResultEntity handleBindException(MethodArgumentNotValidException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
// 記錄紀錄檔。。。
return ResultEntity.faill(211,fieldError.getDefaultMessage(),null);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultEntity<T> {
private Integer code;
private String message;
private T data;
public static <T> ResultEntity<T> faill(Integer code,String msg,T t){
return new ResultEntity<T>(code,msg,t);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@NotBlank(message = "使用者名稱不能為空")
private String name;
@Max(150)
@Min(10)
@NotNull(message = "年齡不能為空")
private Integer age;
@Email
private String email;
@NotNull(message = "user不能為空")
@Valid
private User user;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
@NotNull(message = "user物件中的username不能為空")
private String username;
}
如果同一個類,在不同的使用場景下有不同的校驗規則,那麼可以使用分組校驗。實際需求,如未成年人是不能喝酒,如何校驗?
public class Foo {
@Min(value = 18, groups = {Adult.class})
private Integer age;
public interface Adult { }
public interface Minor { }
}
@RequestMapping("/drink")
public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
for (FieldError item : bindingResult.getFieldErrors()) {
}
return "fail";
}
return "success";
}
作為範例,自定義校驗註解@CannotHaveBlank
,實現字串不能包含空格
的校驗限制:
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
// 自定義註解中指定這個註解真正的驗證者類
@Constraint(validatedBy = {CannotHaveBlankValidator.class})
public @interface CannotHaveBlank {
// 預設錯誤訊息
String message() default "不能包含空格";
// 分組
Class<?>[] groups() default {};
// 負載
Class<? extends Payload>[] payload() default {};
// 指定多個時使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CannotHaveBlank[] value();
}
}
public interface ConstraintValidator<A extends Annotation, T> {
void initialize(A constraintAnnotation);// 初始化事件方法
boolean isValid(T value, ConstraintValidatorContext context);// 判斷是否合法
}
// 所有的驗證者都需要實現ConstraintValidator介面
public class CannotHaveBlankValidator implements ConstraintValidator<CannotHaveBlank, String> {
@Override
public void initialize(CannotHaveBlank constraintAnnotation) {
//...
}
@Override
// ConstraintValidatorContext包含認證中所有的資訊,
// 獲取預設錯誤提示資訊,禁用錯誤提示資訊,改寫錯誤提示資訊等操作。
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value != null && value.contains(" ")) {
//獲取預設提示資訊
String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
System.out.println("default message :" + defaultConstraintMessageTemplate);
//禁用預設提示資訊
context.disableDefaultConstraintViolation();
//設定提示語
context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
return false;
}
return true;
}
}
在驗證資料時,常常需要給使用者告知錯誤資訊。通常情況下,錯誤資訊都是非常簡短的。為了更好的告知使用者錯誤資訊,validation-api提供了一種非常好的機制來格式化錯誤資訊。
以一個使用validation-api對錯誤資訊進行格式化為例子:
public static String validateAndFormat(T obj) {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set> constraintViolationSet = validator.validate(obj);
if (constraintViolationSet != null && constraintViolationSet.size() > 0) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation constraintViolation : constraintViolationSet) {
sb.append(constraintViolation.getPropertyPath()).append(":").append(constraintViolation.getMessage()).append(",");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
} else {
return "";
}
}
首先使用validator.validate(obj)方法對資料進行驗證;
如果有錯誤資訊,則用StringBuilder將錯誤資訊格式化後返回。
預設模式
只要有一個失敗就立馬返回
@Configuration
public class HibernateValidatorConfiguration {
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
// true 快速失敗返回模式 false 普通模式
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
測試驗證不通過就會丟擲 ConstraintViolationException 異常,和之前普通模式下丟擲的異常不一樣。
所以,為了格式統一還需要自定義的例外處理。
// 開啟快速失敗返回模式,GET請求校驗不通過會丟擲如下異常,在這對它處理
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public ResultEntity handle(ValidationException exception) {
if (exception instanceof ConstraintViolationException) {
ConstraintViolationException exs = (ConstraintViolationException) exception;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
System.out.println(item.getMessage());
return ResultEntity.faill(212, item.getMessage(), null);
}
}
return ResultEntity.faill(212, "abc", null);
}
@BeanValidation
) + 自定義註解處理方法(Object validateParamByAnnotation(ProceedingJoinPoint ponit, BeanValidation beanValidation)
)的方式進行資料格式驗證。