Bean 上驗證

語言: CN / TW / HK

一、Validation 驗證

系統開發中,我們通過一系列的規則校驗,來確保輸入數據的正確性、一致性和安全性。

驗證不僅僅是確保正確的用户擁有正確的權限,關注 「你是誰」「你能做什麼」,而且要關注「做的對不對」,數據質量的準確性同樣可能導致驗證的安全問題。

缺失校驗會造成缺陷,過度的校驗則會使代碼顯得繁瑣。

場景:用户註冊

用户: 填寫表單數據,提交

系統:throw new RestException("手機號必填")

用户:填寫手機號

系統:throw new RestException("郵箱格式不正確")

用户:修改郵箱格式

系統:throw new BusinessException("郵箱已經註冊過了")

用户:換一個郵箱

系統:throw new DAOException("用户名過長,插入失敗")

用户:...

二、驗證放哪裏合適

對於簡單的必填或長度校驗,前端需要做,但後端同樣需要做一次驗證。

驗證應該放在 controller 層,還是放在 service 層?

1、在 controller 做,service 不做,理由是如果調用了兩個 service 方法 A.method()、B.method() ,兩個 service 要做重複校驗。

2、在 service 做,controller 不做,理由是簡單校驗已經由前端攔住,業務相關的校驗應該由具體的 service 做,業務相關的東西放到 controller 不合適。

3、controller、service 各做各的,controller 做參數簡單驗證,service 做業務相關驗證,和上述註冊的例子是一致的。

4、各層都做,比如在 DAO 層做最終一道攔截,確保不會出現數據問題。

三、Bean 上驗證

在 Java 中有專門的驗證標準,Bean Validation 1.0 (JSR 303)、Bean Validation 1.1 (JSR 349)、Bean Validation 2.0 (JSR 380)。

JSR,Java Specification Requests 的縮寫,意思是 Java 規範提案。是指向 JCP (Java Community Process) 提出新增一個標準化技術規範的正式請求。JSR 已成為 Java 界的一個重要標準。

Jakarta Bean Validation,2.0 版本發佈與 2019 年。

2009 年,Oracle 收購 SUN ,並將開源部分移交給 Eclipse 基金會,但是有商業要求,如不允許再使用 Java EE 等名稱,於是基金會改名 Jakarta EE。本質上 Jakarta Bean Validation = Java Bean Validation。

我們可以從分層中抽離出來,針對於 bean 單獨做驗證。

常規的驗證方式可以藉助 Bean Validation 提供的一些註解來操作:

@NotBlank
@NotEmpty
@Range
@Length

比如:

@NotBlank(message = "用户賬號不能為空")
private String userCode;

四、驗證的封裝

如果系統接口較多,需要做校驗的入參量級比較大,@NotBlank(message = "用户賬號不能為空"),這個註解就需要重複 N 多次。

通過進行一層封裝可以使之變得更簡單。

自定義一個簡單的註解

定義一個註解 @UserCodeNotBlank ,代表用户系統號不能為空,其驗證邏輯在 ParamValidation.UserCodeNotBlankValidate.class 中。

/**
* @author lyqiang
*/
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = ParamValidation.UserCodeNotBlankValidate.class)
public @interface UserCodeNotBlank {

String message() default "系統號不能為空";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

然後定義一個校驗規則

/**
* @author lyqiang
* <p>
* 一些常用 (比如賬號)
* 或需要做邏輯判斷的校驗 (比如 必須是經理)
* 統一放到此處
*/
public class ParamValidation<T extends Annotation> implements ConstraintValidator<T, Object> {

protected Predicate<Object> predicate = c -> true;

@Inject
protected EhrApiAdapter ehrApiAdapter;

@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
return predicate.test(value);
}

public static class UserCodeNotBlankValidate extends ParamValidation<UserCodeNotBlank> {

@Override
public void initialize(UserCodeNotBlank constraintAnnotation) {
predicate = c -> Objects.nonNull(c) && StringUtils.isNotBlank((String) c);
}
}
}

使用方的 Bean 上或者 controller 方法參數上直接加註解。

@UserCodeNotBlank
private String userCode;

稍微複雜的註解

我們也可以支持其它一些通用的業務校驗操作,比如驗證用户角色必須是經理。

public static class GroupChiefCheckValidate extends ParamValidation<GroupChiefCheck> {
@Override
public void initialize(GroupChiefCheck groupChiefCheck) {
predicate = c -> Objects.nonNull(c) && RoleEnum.GROUP_CHIEF == ehrApiAdapter.getUserInfo((String) c).getRole();
}
}

其它場景應用這種方式校驗也非常合適,比如:

  • 一些合法性的校驗,入參字段必須屬於某個枚舉中的值,state in [1,2,3,4,5]  等等;

  • 一些操作類的如新增某表記錄的接口,需要校驗庫存當前不存在此記錄,否則不允許進行 insert 。