如何正確使用 Bean Validation 進(jìn)行數(shù)據(jù)校驗(yàn)
一、背景
在前后端開發(fā)過(guò)程中,數(shù)據(jù)校驗(yàn)是一項(xiàng)必須且常見的事,從展示層、業(yè)務(wù)邏輯層到持久層幾乎每層都需要數(shù)據(jù)校驗(yàn)。如果在每一層中手工實(shí)現(xiàn)驗(yàn)證邏輯,既耗時(shí)又容易出錯(cuò)。
圖片
為了避免重復(fù)這些驗(yàn)證,通常的做法是將驗(yàn)證邏輯直接捆綁到領(lǐng)域模型中,通過(guò)元數(shù)據(jù)(默認(rèn)是注解)去描述模型, 生成校驗(yàn)代碼,從而使校驗(yàn)從業(yè)務(wù)邏輯中剝離,提升開發(fā)效率,使開發(fā)者更專注業(yè)務(wù)邏輯本身。
圖片
在 Spring 中,目前支持兩種不同的驗(yàn)證方法:Spring Validation 和 JSR-303 Bean Validation,即 @Validated(org . springframework.validation.annotation.Validated)和 @Valid(javax.validation.Valid)。兩者都可以通過(guò)定義模型的約束來(lái)進(jìn)行數(shù)據(jù)校驗(yàn),雖然兩者使用類似,在很多場(chǎng)景下也可以相互替換,但實(shí)際上卻完全不同,這些差別長(zhǎng)久以來(lái)對(duì)我們?nèi)粘J褂卯a(chǎn)生了較大疑惑,本文主要梳理其中的差別、介紹 Validation 的使用及其實(shí)現(xiàn)原理,幫助大家在實(shí)踐過(guò)程中更好使用 Validation 功能。
二、Bean Validation簡(jiǎn)介
什么是JSR?
JSR 是 Java Specification Requests 的縮寫,意思是 Java 規(guī)范提案。是指向 JCP(Java Community Process) 提出新增一個(gè)標(biāo)準(zhǔn)化技術(shù)規(guī)范的正式請(qǐng)求,以向 Java 平臺(tái)增添新的 API 和服務(wù)。JSR 已成為 Java 界的一個(gè)重要標(biāo)準(zhǔn)。
JSR-303定義的是什么標(biāo)準(zhǔn)?
JSR-303 是用于 Bean Validation 的 Java API 規(guī)范,該規(guī)范是 Jakarta EE and JavaSE 的一部分,Hibernate Validator 是 Bean Validation 的參考實(shí)現(xiàn)。Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 Constraint 的實(shí)現(xiàn),除此之外還有一些附加的 Constraint。(最新的為 JSR-380 為 Bean Validation 3.0)
圖片
常用的校驗(yàn)注解補(bǔ)充:
@NotBlank 檢查約束字符串是不是 Null 還有被 Trim 的長(zhǎng)度是否大于,只對(duì)字符串,且會(huì)去掉前后空格。
@NotEmpty 檢查約束元素是否為 Null 或者是 Empty。
@Length 被檢查的字符串長(zhǎng)度是否在指定的范圍內(nèi)。
@Email 驗(yàn)證是否是郵件地址,如果為 Null,不進(jìn)行驗(yàn)證,算通過(guò)驗(yàn)證。
@Range 數(shù)值返回校驗(yàn)。
@IdentityCardNumber 校驗(yàn)身份證信息。
@UniqueElements 集合唯一性校驗(yàn)。
@URL 驗(yàn)證是否是一個(gè) URL 地址。
Spring Validation的產(chǎn)生背景
上文提到 Spring 支持兩種不同的驗(yàn)證方法:Spring Validation 和 JSR-303 Bean Validation(下文使用@Validated和@Valid替代)。為什么會(huì)同時(shí)存在兩種方式?
Spring 增加 @Validated 是為了支持分組校驗(yàn),即同一個(gè)對(duì)象在不同的場(chǎng)景下使用不同的校驗(yàn)形式。比如有兩個(gè)步驟用于提交用戶資料,后端復(fù)用的是同一個(gè)對(duì)象,第一步驗(yàn)證姓名,電子郵件等字段,然后在后續(xù)步驟中的其他字段中。這時(shí)候分組校驗(yàn)就會(huì)發(fā)揮作用。
- 為什么不合入到 JSR-303 中?
之所以沒(méi)有將它添加到 @Valid 注釋中,是因?yàn)樗鞘褂?Java 社區(qū)過(guò)程(JSR-303)標(biāo)準(zhǔn)化的,這需要時(shí)間,而 Spring 開發(fā)者想讓人們更快地使用這個(gè)功能。
- @Validated 的內(nèi)置自動(dòng)化校驗(yàn)
Spring 增加 @Validated 還有另一層原因,Bean Validation 的標(biāo)準(zhǔn)做法是在程序中手工調(diào)用 Validator 或者 ExecutableValidator 進(jìn)行校驗(yàn),為了實(shí)現(xiàn)自動(dòng)化,通常通過(guò) AOP、代理等方法攔截技術(shù)來(lái)調(diào)用。而 @Validated 注解就是為了配合 Spring 進(jìn)行 AOP 攔截,從而實(shí)現(xiàn) Bean Validation 的自動(dòng)化執(zhí)行。
- @Validated 和 @Valid 的區(qū)別
@Valid 是 JSR 標(biāo)準(zhǔn) API,@Validated 擴(kuò)展了 @Valid 支持分組校驗(yàn)且能作為 SpringBean 的 AOP 注解,在 SpringBean 初始化時(shí)實(shí)現(xiàn)方法層面的自動(dòng)校驗(yàn)。最終還是使用了 JSR API 進(jìn)行約束校驗(yàn)。
三、Bean Validation的使用
引入POM
// 正常應(yīng)該引入hibernate-validator,是JSR的參考實(shí)現(xiàn)
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
// Spring在stark中集成了,所以hibernate-validator可以不用引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Bean層面校驗(yàn)
- 變量層面約束
public class EntryApplicationInfoCmd {
/**
* 用戶ID
*/
@NotNull(message = "用戶ID不為空")
private Long userId;
/**
* 證件類型
*/
@NotEmpty(message = "證件類型不為空")
private String certType;
}
- 屬性層面約束
主要為了限制 Setter 方法的只讀屬性。屬性的 Getter 方法打注釋,而不是 Setter。
public class EntryApplicationInfoCmd {
public EntryApplicationInfoCmd(Long userId, String certType) {
this.userId = userId;
this.certType = certType;
}
/**
* 用戶ID
*/
private Long userId;
/**
* 證件類型
*/
private String certType;
@NotNull
public String getUserId() {
return userId;
}
@NotEmpty
public String getCertType() {
return userId;
}
}
- 容器元素約束
public class EntryApplicationInfoCmd {
...
List<@NotEmpty Long> categoryList;
}
- 類層面約束
@CategoryBrandNotEmptyRecord 是自定義類層面的約束,也可以約束在構(gòu)造函數(shù)上。
@CategoryBrandNotEmptyRecord
public class EntryApplicationInfoCmd {
/**
* 用戶ID
*/
@NotNull(message = "用戶ID不為空")
private Long userId;
List<@NotEmpty Long> categoryList;
}
- 嵌套約束
嵌套對(duì)象需要額外使用 @Valid 進(jìn)行標(biāo)注(@Validate 不支持,為什么?請(qǐng)看產(chǎn)生的背景)。
public class EntryApplicationInfoCmd {
/**
* 主營(yíng)品牌
*/
@Valid
@NotNull
private MainBrandImagesCmd mainBrandImage;
}
public class MainBrandImagesCmd {
/**
* 品牌名稱
*/
@NotEmpty
private String brandName;;
}
- 手工驗(yàn)證Bean約束
// 獲取校驗(yàn)器
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// 進(jìn)行bean層面校驗(yàn)
Set<ConstraintViolation<User>> violations = validator.validate(EntryApplicationInfoCmd);
// 打印校驗(yàn)信息
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
方法層面校驗(yàn)
- 函數(shù)參數(shù)約束
public class MerchantMainApplyQueryService {
MainApplyDetailResp detail(@NotNull(message = "申請(qǐng)單號(hào)不能為空") Long id) {
...
}
}
- 函數(shù)返回值約束
public class MerchantMainApplyQueryService {
@NotNull
@Size(min = 1)
public List<@NotNull MainApplyStandDepositResp> getStanderNewDeposit(Long id) {
//...
}
}
- 嵌套約束
嵌套對(duì)象需要額外使用 @Valid 進(jìn)行標(biāo)注(@Validate 不支持)。
public class MerchantMainApplyQueryService {
public NewEntryBrandRuleCheckApiResp brandRuleCheck(@Valid @NotNull NewEntryBrandRuleCheckRequest request) {
...
}
}
public class NewEntryBrandRuleCheckRequest {
@NotNull(message = "一級(jí)類目不能為空")
private Long level1CategoryId;
}
- 在繼承中方法約束
Validation 的設(shè)計(jì)需要遵循里氏替換原則,無(wú)論何時(shí)使用類型 T,也可以使用 T 的子類型 S,而不改變程序的行為。即子類不能增加約束也不能減弱約束。
子類方法參數(shù)的約束與父類行為不一致(錯(cuò)誤例子):
// 繼承的方法參數(shù)約束不能改變,否則會(huì)導(dǎo)致父類子類行為不一致
public interface Vehicle {
void drive(@Max(75) int speedInMph);
}
public class Car implements Vehicle {
@Override
public void drive(@Max(55) int speedInMph) {
//...
}
}
方法的返回值可以增加約束(正確例子):
// 繼承的方法返回值可以增加約束
public interface Vehicle {
@NotNull
List<Person> getPassengers();
}
public class Car implements Vehicle {
@Override
@Size(min = 1)
public List<Person> getPassengers() {
//...
return null;
}
}
- 手工驗(yàn)證方法約束
方法層面校驗(yàn)使用的是 ExecutableValidator。
// 獲取校驗(yàn)器
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator executableValidator = factory.getValidator().forExecutables();
// 進(jìn)行方法層面校驗(yàn)
MerchantMainApplyQueryService service = getService();
Method method = MerchantMainApplyQueryService.class.getMethod( "getStanderNewDeposit", int.class );
Object[] parameterValues = { 80 };
Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters(
service,
method,
parameterValues
);
// 打印校驗(yàn)信息
for (ConstraintViolation<User> violation : violations) {
log.error(violation.getMessage());
}
分組校驗(yàn)
不同場(chǎng)景復(fù)用一個(gè) Model,采用不一樣的校驗(yàn)方式。
public class NewEntryMainApplyRequest {
@NotNull(message = "一級(jí)類目不能為空")
private Long level1CategoryId;
@NotNull(message = "申請(qǐng)單ID不能為空", group = UpdateMerchantMainApplyCmd.class)
private Long applyId;
@NotEmpty(message = "審批人不能為空", group = AddMerchantMainApplyCmd.class)
private String operator;
}
// 校驗(yàn)分組UpdateMerchantMainApplyCmd.class
NewEntryMainApplyRequest request1 = new NewEntryMainApplyRequest( 29, null, "aaa");
Set<ConstraintViolation<NewEntryMainApplyRequest>> constraintViolations = validator.validate( request1, UpdateMerchantMainApplyCmd.class );
assertEquals("申請(qǐng)單ID不能為空", constraintViolations.iterator().next().getMessage());
// 校驗(yàn)分組AddMerchantMainApplyCmd.class
NewEntryMainApplyRequest request2 = new NewEntryMainApplyRequest( 29, "12345", "");
Set<ConstraintViolation<NewEntryMainApplyRequest>> constraintViolations = validator.validate( request2, AddMerchantMainApplyCmd.class );
assertEquals("審批人不能為空", constraintViolations.iterator().next().getMessage());
自定義校驗(yàn)
自定義注解:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyConstraintValidator.class)
public @interface MyConstraint {
String message();
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
自定義校驗(yàn)器:
public class MyConstraintValidator implements ConstraintValidator<MyConstraint, Object> {
@Override
public void initialize(MyConstraint constraintAnnotation) {
}
@Override
public isValid isValid(Object value, ConstraintValidatorContext context) {
String name = (String)value;
if("xxxx".equals(name)) {
return true;
}
return false;
}
}
使用自定義約束:
public class Test {
@MyConstraint(message = "test")
String name;
}
四、Bean Validation自動(dòng)執(zhí)行以及原理
上述 2.6 和 3.5 分別實(shí)現(xiàn)了 Bean 和 Method 層面的約束校驗(yàn),但是每次都主動(dòng)調(diào)用比較繁瑣,因此 Spring 在 @RestController 的 @RequestBody 注解中內(nèi)置了一些自動(dòng)化校驗(yàn)以及在 Bean 初始化中集成了 AOP 來(lái)簡(jiǎn)化編碼。
Validation的常見誤解
最常見的應(yīng)該就是在 RestController 中,校驗(yàn) @RequestBody 指定參數(shù)的約束,使用 @Validated 或者 @Valid(該場(chǎng)景下兩者等價(jià))進(jìn)行約束校驗(yàn),以至于大部分人理解的 Validation 只要打個(gè)注解就可以生效,實(shí)際上這只是一種特例。很多人在使用過(guò)程中經(jīng)常遇到約束校驗(yàn)不生效。約束校驗(yàn)生效
Spring-mvc 中在 @RequestBody 后面加 @Validated、@Valid 約束即可生效。
@RestController
@RequestMapping("/biz/merchant/enter")
public class MerchantEnterController {
@PostMapping("/application")
// 使用@Validated
public HttpMessageResult addOrUpdateV1(@RequestBody @Validated MerchantEnterApplicationReq req){
...
}
// 使用@Valid
@PostMapping("/application2")
public HttpMessageResult addOrUpdate2(@RequestBody @Valid MerchantEnterApplicationReq req){
...
}
}
- 約束校驗(yàn)不生效
然而下面這個(gè)約束其實(shí)是不生效的,想要生效得在 MerchantEntryServiceImpl 類目加上 @Validated 注解。
// @Validated 不加不生效
@Service
public class MerchantEntryService {
public Boolean applicationAddOrUpdate(@Validated MerchantEnterApplicationReq req) {
...
}
public Boolean applicationAddOrUpdate2(@Valid MerchantEnterApplicationReq req) {
...
}
}
那么究竟為什么會(huì)出現(xiàn)這種情況呢,這就需要對(duì) Spring Validation 的注解執(zhí)行原理有一定的了解。
Controller自動(dòng)執(zhí)行約束校驗(yàn)原理
在 Spring-mvc 中,有很多攔截器對(duì) Http 請(qǐng)求的出入?yún)⑦M(jìn)行解析和轉(zhuǎn)換,Validation 解析和執(zhí)行也是類似,其中 RequestResponseBodyMethodProcessor 是用于解析 @RequestBody 標(biāo)注的參數(shù)以及處理 @ResponseBody 標(biāo)注方法的返回值的。
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
// 類上或者方法上標(biāo)注了@ResponseBody注解都行
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
}
// 這是處理入?yún)⒎庋b校驗(yàn)的入口
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional();
// 獲取請(qǐng)求的參數(shù)對(duì)象
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
// 獲取參數(shù)名稱
String name = Conventions.getVariableNameForParameter(parameter);
// 只有存在binderFactory才會(huì)去完成自動(dòng)的綁定、校驗(yàn)~
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
// 這里完成數(shù)據(jù)綁定+數(shù)據(jù)校驗(yàn)~~~~~(綁定的錯(cuò)誤和校驗(yàn)的錯(cuò)誤都會(huì)放進(jìn)Errors里)
validateIfApplicable(binder, parameter);
// 若有錯(cuò)誤消息hasErrors(),并且僅跟著的一個(gè)參數(shù)不是Errors類型,Spring MVC會(huì)主動(dòng)給你拋出MethodArgumentNotValidException異常
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
// 把錯(cuò)誤消息放進(jìn)去 證明已經(jīng)校驗(yàn)出錯(cuò)誤了~~~
// 后續(xù)邏輯會(huì)判斷MODEL_KEY_PREFIX這個(gè)key的~~~~
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}
...
}
約束的校驗(yàn)邏輯是在 RequestResponseBodyMethodProcessor.validateIfApplicable 實(shí)現(xiàn)的,這里同時(shí)兼容了 @Validated 和 @Valid,所以該場(chǎng)景下兩者是等價(jià)的。
// 校驗(yàn),如果合適的話。使用WebDataBinder,失敗信息最終也都是放在它身上~
// 入?yún)ⅲ篗ethodParameter parameter
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 拿到標(biāo)注在此參數(shù)上的所有注解們(比如此處有@Valid和@RequestBody兩個(gè)注解)
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
// 先看看有木有@Validated
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 這個(gè)里的判斷是關(guān)鍵:可以看到標(biāo)注了@Validated注解 或者注解名是以Valid打頭的 都會(huì)有效哦
//注意:這里可沒(méi)說(shuō)必須是@Valid注解。實(shí)際上你自定義注解,名稱只要一Valid開頭都成~~~~~
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
// 拿到分組group后,調(diào)用binder的validate()進(jìn)行校驗(yàn)~~~~
// 可以看到:拿到一個(gè)合適的注解后,立馬就break了~~~
// 所以若你兩個(gè)主機(jī)都標(biāo)注@Validated和@Valid,效果是一樣滴~
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
binder.validate(validationHints);
break;
}
}
binder.validate() 的實(shí)現(xiàn)中使用的 org.springframework.validation.Validator 的接口,該接口的實(shí)現(xiàn)為 SpringValidatorAdapter。
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
for (Validator validator : getValidators()) {
// 使用的org.springframework.validation.Validator,調(diào)用SpringValidatorAdapter.validate
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(target, bindingResult);
}
}
}
在 ValidatorAdapter.validate 實(shí)現(xiàn)中,最終調(diào)用了 javax.validation.Validator.validate,也就是說(shuō)最終是調(diào)用 JSR 實(shí)現(xiàn),@Validate 只是外層的包裝,在這個(gè)包裝中擴(kuò)展的分組功能。
public class SpringValidatorAdapter {
...
private javax.validation.Validator targetValidator;
@Override
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(
// 最終是調(diào)用JSR實(shí)現(xiàn)
this.targetValidator.validate(target), errors));
}
}
}
targetValidator.validate 就是 javax.validation.Validator.validate 上述 2.6 Bean 層面手工驗(yàn)證一致。
Service自動(dòng)執(zhí)行約束校驗(yàn)原理
非Controller的@RequestBody注解,自動(dòng)執(zhí)行約束校驗(yàn),是通過(guò) MethodValidationPostProcessor 實(shí)現(xiàn)的,該類繼承。
BeanPostProcessor, 在 Spring Bean 初始化過(guò)程中讀取 @Validated 注解創(chuàng)建 AOP 代理(實(shí)現(xiàn)方式與 @Async 基本一致)。該類開頭文檔注解(JSR 生效必須類層面上打上 @Spring Validated 注解)。
/**
* <p>Target classes with such annotated methods need to be annotated with Spring's
* {@link Validated} annotation at the type level, for their methods to be searched for
* inline constraint annotations. Validation groups can be specified through {@code @Validated}
* as well. By default, JSR-303 will validate against its default group only.
*/
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
.....
/**
* 設(shè)置Validator
* Set the JSR-303 Validator to delegate to for validating methods.
* <p>Default is the default ValidatorFactory's default Validator.
*/
public void setValidator(Validator validator) {
// Unwrap to the native Validator with forExecutables support
if (validator instanceof LocalValidatorFactoryBean) {
this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
}
else if (validator instanceof SpringValidatorAdapter) {
this.validator = validator.unwrap(Validator.class);
}
else {
this.validator = validator;
}
}
/**
* Create AOP advice for method validation purposes, to be applied
* with a pointcut for the specified 'validated' annotation.
* @param validator the JSR-303 Validator to delegate to
* @return the interceptor to use (typically, but not necessarily,
* a {@link MethodValidationInterceptor} or subclass thereof)
* @since 4.2
*/
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
// 創(chuàng)建了方法調(diào)用時(shí)的攔截器
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
真正執(zhí)行方法調(diào)用時(shí),會(huì)走到 MethodValidationInterceptor.invoke,進(jìn)行約束校驗(yàn)。
public class MethodValidationInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
...
try {
// 執(zhí)行約束校驗(yàn)
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
...
return returnValue;
}
}
execVal.validateParameters 就是 javax.validation.executable.ExecutableValidator.validateParameters 與上述 3.5 方法層面手工驗(yàn)證一致。
五、總結(jié)
圖片
參考文章:https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single