5 年,只爲了一個更好的校驗框架

天地初開

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

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

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

優秀的程序員徹底不須要思考,就像不須要寫文檔和註釋同樣。github

「真是簡單的需求啊」,小明以爲有些無聊,「毫無挑戰。」web

和無數個 web 開發者同樣,小明今天作的是用戶的註冊功能。編程

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

public class UserRegister {

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

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

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

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

    // getter & setter & toString()
}

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

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

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

(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

小結

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

不過,但願你能喜歡。

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

在這裏插入圖片描述

相關文章
相關標籤/搜索