天地初開
五年前,科技大廈 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 框架,但是有些場景是無法滿足的。
比如:
-
驗證新密碼和確認密碼是否相同。(同一物件下的不同屬性之間關係)
-
當一個屬性值滿足某個條件時,才進行其他值的引數校驗。
-
多個屬性值,至少有一個不能為 null
其實,在對於多個欄位的關聯關係處理時,hibernate-validator 就會比較弱。
本專案結合原有的優點,進行這一點的功能強化。
validation-api 過於複雜
validation-api 提供了豐富的特性定義,也同時帶來了一個問題。
實現起來,特別複雜。
然而我們實際使用中,常常不需要這麼複雜的實現。
valid-api 提供了一套簡化很多的 api,便於使用者自行實現。
自定義缺乏靈活性
hibernate-validator 在使用中,自定義約束實現是基於註解的,針對單個屬性校驗不夠靈活。
本專案中,將屬性校驗約束和註解約束區分開,便於複用和擴充。
程式式程式設計 vs 註解式程式設計
hibernate-validator 核心支援的是註解式程式設計,基於 bean 的校驗。
一個問題是針對屬性校驗不靈活,有時候針對 bean 的校驗,還是要自己寫判斷。
本專案支援 fluent-api 進行程式式程式設計,同時支援註解式程式設計。
儘可能兼顧靈活性與便利性。
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
的註解生效條件,讓註解生效更加靈活。
小明抬頭看了看牆上的鐘,夜已經太深了,百聞不如一見,感興趣的小夥伴可以自己去感受一下:
小結
這個開源工具是日常工作中不想寫太多校驗方法的產物,還處於初期階段,還有很多需要改進的地方。
不過,希望你能喜歡。
我是老馬,期待與你的下次重逢。