Bean Validation 2.0 注解
校驗空值
@Null:驗證對象是否為 null
@NotNull:驗證對象是否不為 null
@NotEmpty:驗證對象不為 null,且長度(數組、集合、字符串等)大于 0
@NotBlank:驗證字符串不為 null,且去除兩端空白字符后長度大于 0
校驗大小
@Size(min=, max=):驗證對象(數組、集合、字符串等)長度是否在給定的范圍之內
@Min(value):驗證數值(整數或浮點數)是否大于等于指定的最小值
@Max(value):驗證數值是否小于等于指定的最大值
校驗布爾值
@AssertTrue:驗證 Boolean 對象是否為 true
@AssertFalse:驗證 Boolean 對象是否為 false
校驗日期和時間
@Past:驗證 Date 和 Calendar 對象是否在當前時間之前
@Future:驗證 Date 和 Calendar 對象是否在當前時間之后
@PastOrPresent:驗證日期是否是過去或現在的時間
@FutureOrPresent:驗證日期是否是現在或將來的時間
正則表達式
@Pattern(regexp=, flags=):驗證 String 對象是否符合正則表達式的規則
Hibernate Validation 拓展
@Length(min=, max=):驗證字符串的大小是否在指定的范圍內
@Range(min=, max=):驗證數值是否在合適的范圍內
@UniqueElements:校驗集合中的值是否唯一,依賴于 equals 方法
@ScriptAssert:利用腳本進行校驗
@Valid 和 @Validated
這兩個注解是校驗的入口,作用相似但用法上存在差異。
@Validated
// 用于類/接口/枚舉,方法以及參數
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated { // 校驗時啟動的分組 Class<?>[] value() default {};
}
@Valid
// 用于方法,字段,構造函數,參數,以及泛型類型
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
public @interface Valid { // 未提供其他屬性
}
作用范圍不同:@Validated 無法作用在于字段, @Valid 無法作用于類;
注解中的屬性不同:@Validated 中提供了指定校驗分組的屬性,而 @Valid 沒有這個功能,因為 @Valid 不能進行分組校驗。
字段校驗場景及使用示例
常見的校驗場景有三種: Controller 層的校驗、編程式校驗、 Dubbo 接口校驗。
Controller層 的校驗
使用方式
當方法入參為 @RequestBody 注解的 JavaBean,可在入參前使用 @Validated 或 @Valid 注解開啟校驗。
@PostMapping("/save")
public Response<Boolean> saveNotice(@Validated @RequestBody NoticeDTO noticeDTO) {// noticeDTO中各字段校驗通過,才會執行后續業務邏輯return Response.ok(true);
}
當方法入參為 @PathVariable、 @RequestParam 注解的簡單參數時,需要在 Controller 加上 @Validated 注解開啟校驗。
@RequestMapping("/notice")
@RestController
// 必須加上該注解
@Validated
public class UserController {// 路徑變量@GetMapping("{id}")public Reponse<NoticeDTO> detail(@PathVariable("id") @Min(1L) Long noticeId) {// 參數noticeId校驗通過,執行后續業務邏輯return Reponse.ok();}// 請求參數@GetMapping("getByTitle")public Result getByTitle(@RequestParam("title") @Length(min = 1, max = 20) String title) {// 參數title校驗通過,執行后續業務邏輯return Result.ok();}
}
原理
Spring 框架中的 HandlerMethodArgumentResolver 策略接口,負責將方法參數解析為特定請求中的參數值。
public interface HandlerMethodArgumentResolver { // 判斷當前解析器是否支持給定的方法參數boolean supportsParameter(MethodParameter var1); @Nullable // 實際解析參數的方法Object resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4) throws Exception;
}
上述接口針對 @RequestBody 的實現類 RequestResponseBodyMethodProcessor 中,存在字段校驗邏輯,調用 validateIfApplicable 方法校驗參數。
// RequestResponseBodyMethodProcessor
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 前置處理// 校驗邏輯if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) {//調用校驗函數this.validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } }// 數據綁定邏輯}// 返回處理結果return this.adaptArgumentIfNecessary(arg, parameter);
}
validateIfApplicable 方法中,根據方法參數上的注解,決定是否進行字段校驗:當存在 @Validated 或以 Valid 開頭的注解時,進行校驗。
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {// 獲取參數上的注解Annotation[] annotations = parameter.getParameterAnnotations(); Annotation[] var4 = annotations; int var5 = annotations.length; // 遍歷注解for(int var6 = 0; var6 < var5; ++var6) { Annotation ann = var4[var6]; // 獲取 @Validated 注解Validated validatedAnn = (Validated)AnnotationUtils.getAnnotation(ann, Validated.class); // 或者注解以 Valid 開頭 if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {// 開啟校驗Object hints = validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann); Object[] validationHints = hints instanceof Object[] ? (Object[])((Object[])hints) : new Object[]{hints}; binder.validate(validationHints); break; } }
}
@PathVariable 和 @RequestParam 對應的實現類中,則沒有相應字段校驗邏輯,因此需要在 Controller 上使用 @Validated,開啟字段校驗。
編程式校驗
配置 Validator
@Configuration
public class ValidatorConfiguration { @Bean public Validator validator() { ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() // 設置是否開啟快速失敗模式 //.failFast(true) .buildValidatorFactory(); return validatorFactory.getValidator(); }
}
獲取 validator 并校驗
public class TestValidator {// 注入驗證器@Resourceprivate javax.validation.Validator validator;public String testMethod(TestRequest request) {// 進行校驗,獲取校驗結果Set<ConstraintViolation<TestRequest>> constraintViolations = validator.validate(request);// 組裝校驗信息并返回return res;}
}
Dubbo 接口校驗
可在 @DubboService 注解中,設置 validation 參數為 true,開啟生產者的字段驗證。
@DubboService(version = "1.0.0", validation="true")
public class DubboApiImpl implements DubboApi {....
}
該方式返回的信息對使用者不友好,可通過 Dubbo 的 filter 自定義校驗邏輯和返回信息。需要注意的是,在 Dubbo 中有自己的 IOC 實現來控制容器,因此需提供 setter 方法,供 Dubbo 調用。
@Activate( group = {"provider"}, value = {"customValidationFilter"}, order = 10000
)
@Slf4j
public class CustomValidationFilter implements Filter { private javax.validation.Validator validator; // duubo會調用setter獲取beanpublic void setValidator(javax.validation.Validator validator) { this.validator = validator; } public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException { if (this.validator != null && !invocation.getMethodName().startsWith("$")) { // 補充字段校驗,返回信息的組裝以及異常處理} return invoker.invoke(invocation); }
}
進階使用
分組校驗
對于同一個 DTO, 不同場景下對其校驗規則可能不同, @Validted 支持按照分組分別驗證,示例代碼如下:
校驗注解的 groups 屬性中添加分組
@Data
public class NoticeDTO {@Min(value = 0L, groups = Update.class)private Long id;@NotNull(groups = {Save.class, Update.class})@Length(min = 2, max = 10, groups = {Save.class, Update.class})private String title;// 保存的時候校驗分組public interface Save {}// 更新的時候校驗分組public interface Update {}
}
@Validted 上指定分組
@PostMapping("/save")
public Response<Boolean> saveNotice(@RequestBody @Validated(NoticeDTO.Save.class) NoticeDTO noticeDTO) {// 分組為Save.class的校驗通過,執行后續邏輯return Response.ok();
}@PostMapping("/update")
public Response<Boolean> updateNotice(@RequestBody @Validated(NoticeDTO.Update.class) NoticeDTO noticeDTO) {// 分組為Update.class的校驗通過,執行后續邏輯return Response.ok();
}
自定義校驗注解
如果我們想自定義實現一些驗證邏輯,可以使用自定義注解,主要包括兩部分:實現自定義注解,實現對應的校驗器 validator。下面嘗試實現一個注解,用于校驗集合中的指定屬性是否存在重復,代碼如下:
實現校驗注解,主要需要包含 message()、 filed()、 groups() 三個方法,功能如注釋所示。
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 指定校驗器
@Constraint(validatedBy = UniqueValidator.class)
public @interface Unique { // 用于自定義驗證信息String message() default "字段存在重復"; // 指定集合中的待校驗字段String[] field(); // 指定分組Class<?>[] groups() default {};
}
實現對應的校驗器,主要校驗邏輯在 isValid 方法:獲取集合中指定字段,并組裝為 set,比較 set 和集合的長度,以判斷集合中指定字段是否存在重復。
// 實現ConstraintValidator<T, R>接口,T為注解的類型,R為注解的字段類型
public class UniqueValidator implements ConstraintValidator<Unique, Collection<?>> { private Unique unique; @Override public void initialize(Unique constraintAnnotation) { this.unique = constraintAnnotation; } @Override public boolean isValid(Collection collection, ConstraintValidatorContext constraintValidatorContext) {// 集合為空直接校驗通過if (collection == null || collection.size() == 0) { return Boolean.TRUE; } // 從集合中獲取filed中指定的待校驗字段,看是否存在重復return Arrays.stream(unique.field()) .filter(fieldName -> fieldName != null && !"".equals(fieldName.trim())) .allMatch(fieldName -> {// 收集集合collection中字段為fieldName的值,存入set并計算set的元素個數countint count = (int) collection.stream() .filter(Objects::nonNull) .map(item -> { Class<?> clazz = item.getClass(); Field field; try { field = clazz.getField(fieldName); field.setAccessible(true); return field.get(item); } catch (Exception e) { return null; } }) .collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); // set中元素個數count與集合長度比較,若不相等則說明collection中字段存在重復,校驗不通過if (count != collection.size()) { return false; } return true; }); }
}
總結
通過本文我們得以了解 Spring Validation 的機理及其在實際項目中的應用。無論是標準的校驗注解,還是自定義的校驗邏輯, Spring Validation 都為開發者提供了高效且強大的校驗工具。總的來說, Spring Validation 是任何 Spring 應用不可或缺的一部分,對于追求高質量代碼的 JAVA 開發者而言,掌握其用法和最佳實踐至關重要。