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

老馬嘯西風發表於2021-08-10

天地初開

五年前,科技大廈 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 工具的誕生

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

開源地址: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 的註解生效條件,讓註解生效更加靈活。

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

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

小結

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

不過,希望你能喜歡。

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

在這裡插入圖片描述

相關文章