Jakarta Bean Validation 規範介紹及其API使用以及與Spring Validator之間的關係

爱吃麦辣鸡翅發表於2024-05-28

Jakarta Bean Validation 規範

1.Bean Validation的前世今生

Bean Validation 規範最早在 Oracle Java EE 下維護。2017 年 11 月,Oracle 將 Java EE 移交給 Eclipse 基金會。 2018 年 3 月 5 日,Eclipse 基金會宣佈 Java EE (Enterprise Edition) 被更名為 Jakarta EE。隨著JSR-303JSR-349JSR-380提案的相繼問世(分別對應Bean Validation 1.0、Bean Validation 1.1和Bean Validation 2.0)。

2020年7月釋出的 Jakarta Bean Validation 3.0 在 Jakarta Bean Validation 2.0 的基礎上,徹底將包名稱空間遷移到 jakarta.validation,而不再是 javax.validation。當前版本為Jakarta Bean Validation 3.1,Maven座標為:

<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>3.1.0</version>
</dependency>

2.Bean Validation的介紹

Bean Validation 是一套Java的規範,它可以

  1. 透過使用註解的方式在物件模型上表達約束;
  2. 以擴充套件的方式編寫自定義約束;
  3. 提供了用於驗證物件和物件圖的API;
  4. 提供了用於驗證方法和構造方法的引數和返回值的API;
  5. 報告違反約定的集合;

在 Jakarta Bean Validation 規範中,有一些核心 API 如下:

  • Validator,用於校驗常規 Java Bean,同時支援分組校驗;
  • ExecutableValidator,用於校驗方法引數與方法返回值,同樣支援分組校驗。方法引數和方法返回值往往並不是一個常規 Java Bean,可能是一種容器,比如:List、Map 和 Optional 等;Java 8 針對ElementType新增了一個 TYPE_USE 列舉例項,這讓容器元素的校驗變得簡單,Jakarta Bean Validation API 中內建的註解式約束的頭上均有 TYPE_USE 的身影。
  • ConstraintValidator,如果 Jakarta Bean Validation API 中內建的註解式約束不能滿足實際的需求,則需要自定義註解式約束,同時還需要為自定義約束指定校驗器,這個校驗器需要實現 ConstraintValidator 介面。
  • ValueExtractor,容器並不僅僅指的是 JDK 類庫中的 List、Map 和 Set 等,也可以是一些包裝類,比如ResponseEntity;如果要想校驗 ResponseEntity 容器中的 body,那麼就需要透過實現 ValueExtractor 介面來自定義一個容器元素抽取器,然後透過ConfigurationaddValueExtractor()方法註冊自定義 ValueExtractor。

其內建 Jakarta Bean Validation 3.0的內建約束有:

Constraint 描述 支援型別
@Null 被註釋的元素必須為 null 任意型別
@NotNull 被註釋的元素必須不為 null 任意型別
@AssertTrue 被註釋的元素必須為 true booleanBoolean
@AssertFalse 被註釋的元素必須為 false booleanBoolean
@Min(value) 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 BigDecimalBigIntegerbyteshortintlong 以及各自的包裝類
@Max(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 BigDecimalBigIntegerbyteshortintlong 以及各自的包裝類
@DecimalMin(value) 被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包裝類
@DecimalMax(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包裝類
@Size(max, min) 被註釋的元素的大小必須在指定的範圍內 CharSequenceCollectionMapArray
@Negative 被標註元素必須為是一個嚴格意義上的負數(即0被認為是無效的) BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包裝類
@NegativeOrZero 被標註元素必須為是負數或者0 BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包裝類
@Positive 被標註元素必須為是一個嚴格意義上的正數(即0被認為是無效的) BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包裝類
@Positive OrZero 被標註元素必須為是正數或者0 BigDecimalBigIntegerbyteshortintlongfloatdouble以及各自的包裝類
@Digits 被標註元素必須是在可接受範圍內的數字 BigDecimalBigIntegerCharSequencebyteshortintlong 以及各自的包裝類
@Pattern 被標註的CharSequence必須匹配指定的正規表示式,該正規表示式遵循Java的正規表示式規定 CharSequence
@NotEmpty 被標註元素必須不為null或者空 CharSequenceCollectionMapArray
@NotBlank 被標註元素必須不為null,並且必須包含至少一個非空格的字元 CharSequence
@Email 字串必須是符合正確格式的電子郵件地址 CharSequence

詳見: Jakarta Bean Validation 3.0

Jakarta Bean Validation 規範的唯一實現為 Hibernate Validator(官網:Hibernate Validator),會附加一些第三方的的約束,詳見:2.3.2. Additional constraints

3.Bean Validation的違約處理

上述註解作用於欄位或方法上時,對於違反約定的請求引數,Bean Validation會丟擲異常:MethodArgumentNotValidException

image-20240528104058574 image-20240528104229638

重點看一下AbstractBindingResultBindingResult的抽象類

image-20240528104500920

其中objectName是校驗Bean的名稱,errors就是丟擲的異常,進一步看ObjectError的實現類FieldError

image-20240528105035668

field是違約的欄位,rejectedValue是該欄位顯式定義的拒絕的值,bindingFailure表示是否為繫結失敗,如果不是的話則說明是校驗失敗,進一步跟進ObjectError可以看到:

image-20240528110829904

其中objectName為繫結校驗的物件名,source為違反約束的例項物件,繼續看其父類訊息處理類:

image-20240528111232635

其中arguments是處理訊息的引數,defaultMessage則是預設訊息,會被賦予註解中的message欄位值。

4.Bean Validation的使用

4.1 Spring MVC中如何使用

實體類:

@Data
public class Entity {
  
    @NotBlank(message = "名稱不能為空")
    private String name;

    @NotBlank(message = "描述不能為空")
    private String description;

    @NotNull(message = "型別不能為null")
    private Byte type;
}

controller層:

@RestController
@RequestMapping("/demo")
@Validated
public class DemoController {

    @PostMapping("create")
    public String create(@Valid @RequestBody Entity entity) {
        return "ok";
    }
}

上述程式碼涉及到兩個註解@Validated和@Valid,其中@Valid是JSR-303規範中的註解,@Validated是由Spring提供的註解,具體區別為:

  1. 註解使用的位置:@Validated可以用在型別、方法和方法引數上,但是不能用在成員屬性上;而@Valid可以用在方法、建構函式、方法引數和成員屬性上;
  2. 分組校驗:@Validated提供了一個分組功能,可以在入參驗證時,根據不同的分組採用不同的驗證機制;
  3. 巢狀校驗:二者均無法單獨提供巢狀校驗的功能,但是可以透過在引用屬性上加@Valid註解實現對引用屬性中的欄位校驗;

其中分組校驗:

// 分組
public interface Group1{
}
public interface Group2{
}

// 實體類
public class Entity {
	@NotNull(message = "id不能為null", groups = { Group1.class })
	private int id;
 
	@NotBlank(message = "使用者名稱不能為空", groups = { Group2.class })
	private String username;
}

// controller層
public String create(@Validated( { Group1.class }) Entity entity, BindingResult result) {
	if (result.hasErrors()) {
		return "validate error";
	}
	return "redirect:/success";
}

其中巢狀校驗:

@RestController
public class DemoController {

    @RequestMapping("/create")
    public void create(@Validated Outer outer, BindingResult bindingResult) {
        doSomething();
    }
}

// 實體類
public class Outer {

    @NotNull(message = "id不能為空")
    @Min(value = 1, message = "id必須為正整數")
    private Long id;

    @Valid
    @NotNull(message = "inner不能為空")
    private Inner inner;
}

// 引用屬性
public class Inner {

    @NotNull(message = "id不能為空")
    @Min(value = 1, message = "id必須為正整數")
    private Long id;

    @NotBlank(message = "name不能為空")
    private String name;
}

4.2 Dubbo中如何使用

利用dubbo的攔截器擴充套件點,判斷請求引數是否為自定義的Request型別,如果是的話呼叫validator.validate方法校驗引數,並將結果對映出一個屬性路徑拼接錯誤資訊的list,最終將校驗異常資訊封裝為失敗響應返回。

@Activate(group = Constants.PROVIDER, before = {"DubboExceptionFilter"}, order = -20)
public class ValidatorDubboProviderFilter implements Filter {

    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        Object[] arguments = invocation.getArguments();
        if (arguments != null && arguments.length > 0 && arguments[0] instanceof Request<?> request) {
            Object requestParams = request.getRequestParams();
            Set<ConstraintViolation<Object>> errors = validator.validate(requestParams);
            List<String> collect = errors.stream().map(error -> error.getPropertyPath() + "," + error.getMessage()).collect(Collectors.toList());
            if (!CollectionUtils.isEmpty(collect)) {
                OpenApiResponse openApiResponse = new OpenApiResponse();
                openApiResponse.setSuccess(false);
                openApiResponse.setCode(DddCons.ValidateError);
                openApiResponse.setMessage(String.join("||", collect));
                if (request.getRequestId() != null) {
                    openApiResponse.setRequestId(request.getRequestId());
                }
                return  AsyncRpcResult.newDefaultAsyncResult(openApiResponse ,invocation);
            }
        }
        return invoker.invoke(invocation);
    }
}

最後別忘了新增META-INF/dubbo/org.apache.dubbo.rpc.Filter

ValidatorDubboProviderFilter=com.xxx.infrastructure.tech.validator.dubbo.ValidatorDubboProviderFilter

5.Spring Validator 與 Bean Validation的關係

5.1 Spring的Validator介面

在Spring Framework中有自己的Validator介面,但是其API 設計的比較簡陋,而且需要編寫大量 Validator 實現類,與javax bean validation的註解式校驗相比簡直不堪一擊,於是在Spring 3.0版本開始,Spring Validator將其所有校驗請求轉發至Jakarta Bean Validation介面的實現中。

image-20240528151130219

其中一個非常重要的類就是SpringValidatorAdapter,不僅實現了 SmartValidator 介面,同時也實現了jakarta.validation.Validator介面。(SmartValidator介面繼承自spring的Validator介面)顧名思義,xxxAdapter就是適配層,所以SpringValidatorAdapter的核心作用就是將校驗請求轉發給jakarta.validation.Validator

public class SpringValidatorAdapter implements SmartValidator, Validator {
    @Nullable
    private Validator targetValidator;

    public void validate(Object target, Errors errors) {
        if (this.targetValidator != null) {
            this.processConstraintViolations(this.targetValidator.validate(target, new Class[0]), errors);
        }
    }

    public void validate(Object target, Errors errors, Object... validationHints) {
        if (this.targetValidator != null) {
            this.processConstraintViolations(this.targetValidator.validate(target, this.asValidationGroups(validationHints)), errors);
        }
    }
}
//---------------------------------------------------------------------
// Implementation of JSR-303 Validator interface
//---------------------------------------------------------------------
public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
    Assert.state(this.targetValidator != null, "No target Validator set");
    return this.targetValidator.validate(object, groups);
}

5.2 Spring MVC 是如何進行 Bean 校驗的

LocalValidatorFactoryBean 繼承自 SpringValidatorAdapter,負責構建與配置 jakarta.validation.Validator 例項,並且實現了InitializingBean介面,在後者 afterPropertiesSet() 方法內進行構建與配置 jakarta.validation.Validator 例項,然後透過 setTargetValidator() 方法為 SpringValidatorAdapter 注入 Bean Validation 例項。

image-20240528161416816

在 Spring MVC 中,HandlerMethodArgumentResolver一般會委派HttpMessageConverter從 HTTP 請求中解析出HandlerMethod所需要的方法引數值 (有了引數才能反射呼叫由@RestController註解標記的方法),然後進行 Bean Validation 操作。RequestResponseBodyMethodProcessor是極為重要的一個 HandlerMethodArgumentResolver 實現類,因為由@RequestBody標記的引數就由它解析。

image-20240528155809865

validateIfApplicable方法首先透過determineValidationHints方法判斷註解是@Valid還是@Validated,然後呼叫dataBinder.validate方法校驗引數。

image-20240528160057907 image-20240528161631280

最終還是透過SpringValidatorAdapter傳送到 jakarta.validation.Validator<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups)方法,(BindingResult繼承自Errors)

image-20240528161838294

參考文獻:

[1] Spring:全面擁抱 Jakarta Bean Validation 規範.https://cloud.tencent.com/developer/article/2322140


本部落格內容僅供個人學習使用,禁止用於商業用途。轉載需註明出處並連結至原文。

相關文章