5 年,只爲了一個更好的校驗框架 天地初開 初見 Hibernate-Validator 框架不足之處 valid 工具的誕生 小結

天地初開

五年前,科技大廈 1 層 B 座。

小明的眼睛直勾勾地盯着屏幕,雙手噼裏啪啦的敲着鍵盤。

思考是不存在的,思考只會讓小明的速度降下來。

優秀的程序員完全不需要思考,就像不需要寫文檔和註釋一樣。

“真是簡單的需求啊”,小明覺得有些無聊,“毫無挑戰。”

和無數個 web 開發者一樣,小明今天做的是用戶的註冊功能。

首先定義一下對應的用戶註冊對象:

public class UserRegister {

    /**
     * 名稱
     */
    private String name;

    /**
     * 原始密碼
     */
    private String password;

    /**
     * 確認密碼
     */
    private String password2;

    /**
     * 性別
     */
    private String sex;

    // getter & setter & toString()
}

註冊時格式要求文檔也做了簡單的限制:

(1)name 名稱必須介於 1-32 位之間

(2)password 密碼必須介於 6-32 位之間

(3)password2 確認密碼必須和 password 保持一致

(4)sex 性別必須爲 BOY/GIRL 兩者中的一個。

“這也不難”,無情的編碼機器開始瘋狂的敲打着鍵盤,不一會兒基本的校驗方法就寫好了:

private void paramCheck(UserRegister userRegister) {
    //1. 名稱
    String name = userRegister.getName();
    if(name == null) {
        throw new IllegalArgumentException("名稱不可爲空");
    }
    if(name.length() < 1 || name.length() > 32) {
        throw new IllegalArgumentException("名稱長度必須介於 1-32 之間");
    }

    //2. 密碼
    String password = userRegister.getPassword();
    if(password == null) {
        throw new IllegalArgumentException("密碼不可爲空");
    }
    if(password.length() < 6 || password.length() > 32) {
        throw new IllegalArgumentException("密碼長度必須介於 6-32 之間");
    }
    //2.2 確認密碼
    String password2 = userRegister.getPassword2();
    if(!password.equals(password2)) {
        throw new IllegalArgumentException("確認密碼必須和密碼保持一致");
    }

    //3. 性別
    String sex = userRegister.getSex();
    if(!SexEnum.BOY.getCode().equals(sex) && !SexEnum.GIRL.getCode().equals(sex)) {
        throw new IllegalArgumentException("性別必須指定爲 GIRL/BOY");
    }
}

打完收工,小明把代碼提交完畢,就早早地下班跑路了。

初見 Hibernate-Validator

“小明啊,我今天簡單地看了一下你的代碼。”,項目經理看似隨意地提了一句。

小明停下了手中的工作,看向項目經理,意思是讓他繼續說下去。

“整體還是比較嚴謹的,就是寫了太多的校驗代碼。”

“太多的校驗代碼?不校驗數據用戶亂填怎麼辦?”,小明有些不太明白。

“校驗代碼的話,有時間可以瞭解一下 hibernate-validator 校驗框架。”

“可以,我有時間看下。”

嘴上說着,小明心裏一萬個不願意。

什麼休眠框架,影響我搬磚的速度。

後來小明還是勉爲其難的搜索了一下 hibernate-validator,看了看感覺還不錯。

這個框架提供了很多內置的註解,便於日常校驗的開發,大大提升了校驗方法的可複用性。

於是,小明把自己的校驗方法改良了一下:

public class UserRegister {

    /**
     * 名稱
     */
    @NotNull(message = "名稱不可爲空")
    @Length(min = 1, max = 32, message = "名稱長度必須介於 1-32 之間")
    private String name;

    /**
     * 原始密碼
     */
    @NotNull(message = "密碼不可爲空不可爲空")
    @Length(min = 1, max = 32, message = "密碼長度必須介於 6-32 之間")
    private String password;

    /**
     * 確認密碼
     */
    @NotNull(message = "確認密碼不可爲空不可爲空")
    @Length(min = 1, max = 32, message = "確認密碼必須介於 6-32 之間")
    private String password2;

    /**
     * 性別
     */
    private String sex;

}

校驗方法調整如下:

private void paramCheck2(UserRegister userRegister) {
    //1. 名稱
    ValidateUtil.validate(userRegister);

    //2.2 確認密碼
    String password2 = userRegister.getPassword2();
    if(!userRegister.getPassword().equals(password2)) {
        throw new IllegalArgumentException("確認密碼必須和密碼保持一致");
    }

    //3. 性別
    String sex = userRegister.getSex();
    if(!SexEnum.BOY.getCode().equals(sex) && !SexEnum.GIRL.getCode().equals(sex)) {
        throw new IllegalArgumentException("性別必須指定爲 GIRL/BOY");
    }
}

確實清爽了很多,ValidateUtil 是基於一個簡單的工具類:

public class ValidateUtil {

    /**
     * 使用hibernate的註解來進行驗證
     */
    private  static Validator validator = Validation
            .byProvider(HibernateValidator.class)
            .configure().failFast(true)
            .buildValidatorFactory()
            .getValidator();

    public static <T> void validate(T t) {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);
        // 拋出檢驗異常
        if (constraintViolations.size() > 0) {
            final String msg = constraintViolations.iterator().next().getMessage();
            throw new IllegalArgumentException(msg);
        }
    }

}

但是小明依然覺得不滿意,sex 的校驗可以進一步優化嗎?

答案是肯定的,小明發現 hibernate-validator 支持自定義註解。

這是一個很強大的功能,優秀的框架就應該爲使用者提供更多的可能性

於是小明實現了一個自定義註解:

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MyEnumRangesValidator.class)
public @interface MyEnumRanges {

    Class<? extends Enum> value();

    String message() default "";

}

MyEnumRangesValidator 的實現如下:

public class MyEnumRangesValidator implements
        ConstraintValidator<MyEnumRanges, String> {

    private MyEnumRanges myEnumRanges;

    @Override
    public void initialize(MyEnumRanges constraintAnnotation) {
        this.myEnumRanges = constraintAnnotation;
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return getEnumValues(myEnumRanges.value()).contains(value);
    }

    /**
     * 獲取枚舉值對應的信息
     *
     * @param enumClass 枚舉類
     * @return 枚舉說明
     * @since 0.0.9
     */
    private List<String> getEnumValues(Class<? extends Enum> enumClass) {
        Enum[] enums = enumClass.getEnumConstants();

        return ArrayUtil.toList(enums, new IHandler<Enum, String>() {
            @Override
            public String handle(Enum anEnum) {
                return anEnum.toString();
            }
        });
    }

}

限制當前的字段值必須在指定的枚舉範圍內,以後所有涉及到枚舉範圍的,使用這個註解即可搞定。

然後把 @MyEnumRanges 加在 sex 字段上:

@NotNull(message = "性別不可爲空")
@MyEnumRanges(message = "性別必須在 BOY/GIRL 範圍內", value = SexEnum.class)
private String sex;

這樣校驗方法可以簡化如下:

private void paramCheck3(UserRegister userRegister) {
    //1. 名稱
    ValidateUtil.validate(userRegister);
    //2.2 確認密碼
    String password2 = userRegister.getPassword2();
    if(!userRegister.getPassword().equals(password2)) {
        throw new IllegalArgumentException("確認密碼必須和密碼保持一致");
    }
}

小明滿意的笑了笑。

但是他的笑容只是持續了一會兒,因爲他發現了一個不令人滿意的地方。

確認密碼這一段代碼可以去掉嗎?

好像直接使用 hibernate-validator 框架是做不到的。

框架不足之處

這一切令小明很痛苦,他發現框架本身確實有很多不足之處。

hibernate-validator 無法滿足的場景

如今 java 最流行的 hibernate-validator 框架,但是有些場景是無法滿足的。

比如:

  1. 驗證新密碼和確認密碼是否相同。(同一對象下的不同屬性之間關係)

  2. 當一個屬性值滿足某個條件時,才進行其他值的參數校驗。

  3. 多個屬性值,至少有一個不能爲 null

其實,在對於多個字段的關聯關係處理時,hibernate-validator 就會比較弱。

本項目結合原有的優點,進行這一點的功能強化。

validation-api 過於複雜

validation-api 提供了豐富的特性定義,也同時帶來了一個問題。

實現起來,特別複雜。

然而我們實際使用中,常常不需要這麼複雜的實現。

valid-api 提供了一套簡化很多的 api,便於用戶自行實現。

自定義缺乏靈活性

hibernate-validator 在使用中,自定義約束實現是基於註解的,針對單個屬性校驗不夠靈活。

本項目中,將屬性校驗約束和註解約束區分開,便於複用和拓展。

過程式編程 vs 註解式編程

hibernate-validator 核心支持的是註解式編程,基於 bean 的校驗。

一個問題是針對屬性校驗不靈活,有時候針對 bean 的校驗,還是要自己寫判斷。

本項目支持 fluent-api 進行過程式編程,同時支持註解式編程。

儘可能兼顧靈活性與便利性。

valid 工具的誕生

於是小明花了很長時間,寫了一個校驗工具,希望可以彌補上述工具的不足。

開源地址:https://github.com/houbb/valid

特性

  • 支持 fluent-validation

  • 支持 jsr-303 註解,支持所有 hibenrate-validator 常用註解

  • 支持 i18n

  • 支持用戶自定義策略

  • 支持用戶自定義註解

  • 支持針對屬性的校驗

  • 支持過程式編程與註解式編程

  • 支持指定校驗生效的條件

快速開始

maven 引入

<dependency>
    <groupId>com.github.houbb</groupId>
    <artifactId>valid-jsr</artifactId>
    <version>0.2.2</version>
</dependency>

編碼

工具類使用:

User user = new User();
user.sex("what").password("old").password2("new");

ValidHelper.failOverThrow(user);

報錯如下:

會拋出 ValidRuntimeException 異常,異常的信息如下:

name: 值 <null> 不是預期值,password: 值 <old> 不是預期值,sex: 值 <what> 不是預期值

其中 User 的定義如下:

public class User {

    /**
     * 名稱
     */
    @HasNotNull({"nickName"})
    private String name;

    /**
     * 暱稱
     */
    private String nickName;

    /**
     * 原始密碼
     */
    @AllEquals("password2")
    private String password;

    /**
     * 新密碼
     */
    private String password2;

    /**
     * 性別
     */
    @Ranges({"boy", "girl"})
    private String sex;

    /**
     * 失敗類型枚舉
     */
    @EnumRanges(FailTypeEnum.class)
    private String failType;

    //Getter and Setter
}

內置註解簡介如下:

註解 說明
@AllEquals 當前字段及指定字段值必須全部相等
@HasNotNull 當前字段及指定字段值至少有一個不爲 null
@EnumRanges 當前字段值必須在枚舉屬性範圍內
@Ranges 當前字段值必須在指定屬性範圍內

小明在設計驗證工具的時候,針對 hibernater 的不足都做了一點小小的改進。

可以讓字段之間產生聯繫,以提供更加強大的功能。

每一個註解都有對應的過程式方法,讓你可以在註解式和過程式中切換自如。

內置了 @Condition 的註解生效條件,讓註解生效更加靈活。

小明擡頭看了看牆上的鐘,夜已經太深了,百聞不如一見,感興趣的小夥伴可以自己去感受一下:

開源地址:https://github.com/houbb/valid

小結

這個開源工具是日常工作中不想寫太多校驗方法的產物,還處於初期階段,還有很多需要改進的地方。

不過,希望你能喜歡。

我是老馬,期待與你的下次重逢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章