java 開發中,參數校驗是很是常見的需求。java
可是 hibernate-validator 在使用過程當中,依然會存在一些問題。git
valid
現在 java 最流行的 hibernate-validator 框架,可是有些場景是沒法知足的。github
好比:正則表達式
其實,在對於多個字段的關聯關係處理時,hibernate-validator 就會比較弱。spring
本項目結合原有的優勢,進行這一點的功能強化。apache
validation-api 提供了豐富的特性定義,也同時帶來了一個問題。編程
實現起來,特別複雜。api
然而咱們實際使用中,經常不須要這麼複雜的實現。安全
valid-api 提供了一套簡化不少的 api,便於用戶自行實現。框架
hibernate-validator 在使用中,自定義約束實現是基於註解的,針對單個屬性校驗不夠靈活。
本項目中,將屬性校驗約束和註解約束區分開,便於複用和拓展。
hibernate-validator 核心支持的是註解式編程,基於 bean 的校驗。
一個問題是針對屬性校驗不靈活,有時候針對 bean 的校驗,仍是要本身寫判斷。
本項目支持 fluent-api 進行過程式編程,同時支持註解式編程。
儘量兼顧靈活性與便利性。
模塊名稱 | 說明 |
---|---|
valid-api | 核心 api 及註解定義 |
valid-core | 針對 valid-api 的核心實現 |
valid-jsr | 針對 JSR-303 標準註解的實現 |
valid-test | 測試代碼模塊 |
valid-core 默認引入 valid-api
valid-jsr 默認引入 valid-core
JDK1.7+
Maven 3.X+
<dependency> <groupId>com.github.houbb</groupId> <artifactId>valid-jsr</artifactId> <version>0.1.2</version> </dependency>
咱們直接利用 jsr 內置的約束類:
public void helloValidTest() { IResult result = ValidBs.on(null, JsrConstraints.notNullConstraint()) .result() .print(); Assert.assertFalse(result.pass()); }
對應日誌輸出爲:
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='預期值爲 <not null>,實際值爲 <null>', value=null, constraint='NotNullConstraint', expectValue='not null'}], allList=null}
ValidBs 用來進行驗證的引導類,上述的寫法等價於以下:
public void helloValidAllConfigTest() { IResult result = ValidBs.on(null, JsrConstraints.notNullConstraint()) .fail(Fails.failFast()) .group() .valid(DefaultValidator.getInstance()) .result() .print(); Assert.assertFalse(result.pass()); }
Object 能夠是對象,也能夠是普通的值。
constraints 爲對應的約束列表,爲默認的約束驗證提供便利性。
IConstraint 相關建立工具類 Constraints
、JsrConstraints
能夠指定失敗時的處理策略,支持用戶自定義失敗策略。
實現 | 說明 |
---|---|
failOver | 失敗後繼續驗證,直到驗證完全部屬性 |
failFast | 失敗後快速返回 |
有時候咱們但願,只驗證指定某一分組的約束。
能夠經過 group() 屬性指定,與 IConstraint 中的 group() 屬性匹配的約束纔會被執行。
默認爲 DefaultValidator,爲 valid-api 的實現驗證。
若是你但願使用 jsr-303 註解,可使用 JsrValidator
。
支持自定義驗證策略。
默認爲 simple() 的簡單結果處理。
能夠指定爲 detail() 進行詳細結果處理查看。
支持用戶自定義結果處理策略。
simple()/detail() 處理的結果爲 IResult 實現類。
IResult 支持以下方法:
對結果進行打印,主要便於調試。
對於參數的校驗,通常都是基於異常結合 spring aop來處理的。
throwsEx 會在驗證不經過時,拋出 ValidRuntimeException 異常,對應 message 爲提示消息。
@Test(expected = ValidRuntimeException.class) public void resultThrowsExTest() { ValidBs.on(null, notNullValidatorEntry()) .valid() .result() .throwsEx(); }
上面咱們對 ValidBs 有了一個總體的瞭解,下面來看一看系統內置的屬性約束有哪些。
每一個屬性約束都有對應註解。
針對單個屬性,直接使用屬性約束便可,靈活快捷。
針對 bean 校驗,能夠結合註解實現,相似於 hibernate-validator。
核心內置屬性約束實現。
枚舉類指定範圍約束
參見工具類 Constraints#enumRangesConstraint
/** * 枚舉範圍內約束 * (1)當前值必須在枚舉類對應枚舉的 toString() 列表中。 * @param enumClass 枚舉類,不可爲空 * @return 約束類 * @since 0.1.1 * @see com.github.houbb.valid.core.annotation.constraint.EnumRanges 枚舉類指定範圍註解 */ public static IConstraint enumRangesConstraint(final Class<? extends Enum> enumClass)
參見測試類 EnumsRangesConstraintTest
IResult result = ValidBs.on("DEFINE", Constraints.enumRangesConstraint(FailTypeEnum.class)) .result(); Assert.assertFalse(result.pass());
FailTypeEnum 是 valid-api 內置的枚舉類,枚舉值爲 FAIL_FAST/FAIL_OVER。
只有屬性值在枚舉值範圍內,驗證纔會經過。
指定屬性範圍內約束
參見工具類 Constraints#rangesConstraint
* 值在指定範圍內約束 * (1)這裏爲了和註解保持一致性,暫時只支持 String * @param strings 對象範圍 * @return 約束類 * @since 0.1.1 * @see com.github.houbb.valid.core.annotation.constraint.Ranges String 指定範圍內註解 */ public static IConstraint rangesConstraint(String ... strings)
參見測試類 RangesConstraintTest
IResult result = ValidBs.on("DEFINE", Constraints.rangesConstraint("FAIL_OVER", "FAIL_FAST")) .result(); Assert.assertFalse(result.pass());
這個相對於枚舉值,更加靈活一些。
能夠根據本身的須要,指定屬性的範圍。
valid-jsr 中內置註解,和 jsr-303 標準一一對應,此處再也不贅述。
建立方式見工具類 JsrConstraints
,測試代碼見 xxxConstraintTest。
對應列表以下:
屬性約束 | 註解 | 簡介 |
---|---|---|
AssertFalseConstraint | @AssertFalse | 指定值必須爲 false |
AssertTrueConstraint | @AssertTrue | 指定值必須爲 true |
MinConstraint | @Min | 指定值必須大於等於最小值 |
MaxConstraint | @Max | 指定值必須小於等於最大值 |
DecimalMinConstraint | @DecimalMin | 指定金額必須大於等於最小值 |
DecimalMaxConstraint | @DecimalMax | 指定金額必須小於等於最大值 |
DigitsConstraint | @Digits | 指定值位數必須符合要求 |
FutureConstraint | @Future | 指定日期必須在將來 |
PastConstraint | @Past | 指定日期必須在過去 |
PatternConstraint | @Pattern | 指定值必須知足正則表達式 |
SizeConstraint | @Size | 指定值必須在指定大小內 |
實際業務需求的是不斷變化的,內置的屬性約束經常沒法知足咱們的實際需求。
咱們能夠經過自定義屬性,來實現本身的需求。
參見類 DefineConstraintTest
notNullConstraint 對於 null 值是嚴格的。
因此繼承自 AbstractStrictConstraint
,以下:
IResult result = ValidBs.on(null, new AbstractStrictConstraint() { @Override protected boolean pass(IConstraintContext context, Object value) { return value != null; } }).result(); Assert.assertFalse(result.pass());
在 jsr-303 標準中,除卻 @NotNull
對於 null 值都是非嚴格校驗的。
繼承自 AbstractConstraint
便可,以下:
IConstraint assertTrueConstraint = new AbstractConstraint<Boolean>() { @Override protected boolean pass(IConstraintContext context, Boolean value) { return false; } }; IResult nullValid = ValidBs.on(null, assertTrueConstraint) .result(); Assert.assertTrue(nullValid.pass()); IResult falseValid = ValidBs.on(false, assertTrueConstraint) .result(); Assert.assertFalse(falseValid.pass());
註解 | 說明 |
---|---|
@AllEquals | 當前字段及指定字段值必須所有相等 |
@HasNotNull | 當前字段及指定字段值至少有一個不爲 null |
@EnumRanges | 當前字段值必須在枚舉屬性範圍內 |
@Ranges | 當前字段值必須在指定屬性範圍內 |
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; //fluent getter & setter }
咱們限制 name/nickName 至少有一個不爲空,password/password2 值要一致。
以及限定了 sex 的範圍值和 failType 的枚舉值。
User user = new User(); user.sex("what").password("old").password2("new") .failType("DEFINE"); IResult result = ValidBs.on(user) .fail(Fails.failOver()) .result() .print(); Assert.assertFalse(result.pass());
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='值 <null> 不是預期值', value=null, constraint='HasNotNullConstraint', expectValue=''}, DefaultConstraintResult{pass=false, message='值 <old> 不是預期值', value=old, constraint='AllEqualsConstraint', expectValue=''}, DefaultConstraintResult{pass=false, message='值 <what> 不是預期值', value=what, constraint='RangesConstraint', expectValue=''}, DefaultConstraintResult{pass=false, message='值 <DEFINE> 不是預期值', value=DEFINE, constraint='EnumRangesConstraint', expectValue=''}], allList=null}
與 jsr-303 註解標準保持一致。
爲了演示,簡單定義以下:
public class JsrUser { @Null private Object nullVal; @NotNull private String notNullVal; @AssertFalse private boolean assertFalse; @AssertTrue private boolean assertTrue; @Pattern(regexp = "[123456]{2}") private String pattern; @Size(min = 2, max = 5) private String size; @DecimalMax("12.22") private BigDecimal decimalMax; @DecimalMin("1.22") private BigDecimal decimalMin; @Min(10) private long min; @Max(10) private long max; @Past private Date past; @Future private Date future; @Digits(integer = 2, fraction = 4) private Long digits; //fluent getter and setter }
參見測試類 ValidBsJsrBeanTest
public void beanFailTest() { Date future = DateUtil.getFormatDate("90190101", DateUtil.PURE_DATE_FORMAT); Date past = DateUtil.getFormatDate("20190101", DateUtil.PURE_DATE_FORMAT); JsrUser jsrUser = new JsrUser(); jsrUser.assertFalse(true) .assertTrue(false) .decimalMin(new BigDecimal("1")) .decimalMax(new BigDecimal("55.55")) .min(5) .max(20) .digits(333333L) .future(past) .past(future) .nullVal("123") .notNullVal(null) .pattern("asdfasdf") .size("22222222222222222222"); IResult result = ValidBs.on(jsrUser) .fail(Fails.failOver()) .valid(JsrValidator.getInstance()) .result() .print(); Assert.assertFalse(result.pass()); }
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='值必須爲空', value=123, constraint='NullConstraint', expectValue='null'}, DefaultConstraintResult{pass=false, message='值必須爲非空', value=null, constraint='NotNullConstraint', expectValue='not null'}, DefaultConstraintResult{pass=false, message='值必須爲假', value=true, constraint='AssertFalseConstraint', expectValue='false'}, DefaultConstraintResult{pass=false, message='值必須爲真', value=false, constraint='AssertTrueConstraint', expectValue='true'}, DefaultConstraintResult{pass=false, message='值必須知足正則表達式', value=asdfasdf, constraint='PatternConstraint', expectValue='必須匹配正則表達式 [123456]{2}'}, DefaultConstraintResult{pass=false, message='值必須爲在指定範圍內', value=22222222222222222222, constraint='SizeConstraint', expectValue='大小必須在範圍內 [2, 5]'}, DefaultConstraintResult{pass=false, message='值必須小於金額最大值', value=55.55, constraint='DecimalMaxConstraint', expectValue='小於等於 12.22'}, DefaultConstraintResult{pass=false, message='值必須大於金額最小值', value=1, constraint='DecimalMinConstraint', expectValue='大於等於 1.22'}, DefaultConstraintResult{pass=false, message='值必須大於最小值', value=5, constraint='MinConstraint', expectValue='大於等於 10'}, DefaultConstraintResult{pass=false, message='值必須小於最大值', value=20, constraint='MaxConstraint', expectValue='小於等於 10'}, DefaultConstraintResult{pass=false, message='時間必須在過去', value=Fri Jan 01 00:00:00 CST 9019, constraint='PastConstraint', expectValue='小於等於 Sun Oct 13 12:12:07 CST 2019'}, DefaultConstraintResult{pass=false, message='時間必須在將來', value=Tue Jan 01 00:00:00 CST 2019, constraint='FutureConstraint', expectValue='大於等於 Sun Oct 13 12:12:07 CST 2019'}, DefaultConstraintResult{pass=false, message='值必須知足位數', value=333333, constraint='DigitsConstraint', expectValue='整數位數 [2], 小數位數 [4]'}], allList=null}
有時候咱們一個對象中,會引入其餘子對象。
咱們但願對子對象也進行相關屬性的驗證,這時候就可使用 @Valid
註解。
該註解爲 jsr-303 標準註解。
public class ValidUser { /** * 子節點 */ @Valid private User user; //fluent setter & getter }
參見測試類 ValidBsValidBeanTest
public void beanFailTest() { User user = new User(); user.sex("default").password("old").password2("new") .failType("DEFINE"); ValidUser validUser = new ValidUser(); validUser.user(user); IResult result = ValidBs.on(validUser) .fail(Fails.failOver()) .result() .print(); Assert.assertFalse(result.pass()); }
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='值 <null> 不是預期值', value=null, constraint='HasNotNullConstraint', expectValue=''}, DefaultConstraintResult{pass=false, message='值 <old> 不是預期值', value=old, constraint='AllEqualsConstraint', expectValue=''}, DefaultConstraintResult{pass=false, message='值 <default> 不是預期值', value=default, constraint='RangesConstraint', expectValue=''}, DefaultConstraintResult{pass=false, message='值 <DEFINE> 不是預期值', value=DEFINE, constraint='EnumRangesConstraint', expectValue=''}], allList=null}
有時候咱們可能會引用自身,這個也作了測試,是符合預期的。
參見 ValidBsSelfValidBeanTest
不一樣國家對於語言的要求確定也不一樣。
本項目目前支持中文/英文國際化支持,默認以當前地區編碼爲準,若是不存在,則使用英文。
感受其餘語言,暫時使用中沒有用到。(我的也不會,錯了也不知道。暫時不添加)
測試代碼參加 ValidBsI18NTest
public void i18nEnTest() { Locale.setDefault(Locale.ENGLISH); IResult result = ValidBs.on(null, JsrConstraints.notNullConstraint()) .result() .print(); Assert.assertEquals("Expect is <not null>, but actual is <null>.", result.notPassList().get(0).message()); }
public void i18nZhTest() { Locale.setDefault(Locale.CHINESE); IResult result = ValidBs.on(null, JsrConstraints.notNullConstraint()) .result() .print(); Assert.assertEquals("預期值爲 <not null>,實際值爲 <null>", result.notPassList().get(0).message()); }
對於不符合約束條件的處理方式,主要有如下兩種:
快速失敗。遇到一個約束不符合條件,直接返回。
優勢:耗時較短。
所有驗證,將全部的屬性都驗證一遍。
優勢:能夠一次性得到全部失敗信息。
參見工具類 Fails
,返回的實例爲單例,且線程安全。
參見測試類 ValidBsFailTest
咱們指定要求屬性值長度最小爲3,且必須知足正則表達式。
IResult result = ValidBs.on("12", JsrConstraints.sizeConstraintMin(3), JsrConstraints.patternConstraint("[678]{3}")) .fail(Fails.failFast()) .result() .print(); Assert.assertEquals(1, result.notPassList().size());
採用快速失敗模式,只有一個失敗驗證結果。
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='預期值爲 <必須匹配正則表達式 [678]{3}>,實際值爲 <12>', value=12, constraint='PatternConstraint', expectValue='必須匹配正則表達式 [678]{3}'}], allList=null}
保持其餘部分不變,咱們調整下失敗處理策略。
IResult result = ValidBs.on("12", JsrConstraints.sizeConstraintMin(3), JsrConstraints.patternConstraint("[678]{3}")) .fail(Fails.failOver()) .result() .print(); Assert.assertEquals(2, result.notPassList().size());
此時失敗處理結果爲2,日誌以下:
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='預期值爲 <必須匹配正則表達式 [678]{3}>,實際值爲 <12>', value=12, constraint='PatternConstraint', expectValue='必須匹配正則表達式 [678]{3}'}, DefaultConstraintResult{pass=false, message='預期值爲 <大小必須在範圍內 [3, 2147483647]>,實際值爲 <2>', value=12, constraint='SizeConstraint', expectValue='大小必須在範圍內 [3, 2147483647]'}], allList=null}
爲了便於集成不一樣框架的測試驗證,本框架支持 IValidator。
同時也容許用戶自定義本身的實現方式。
指定 valid 對應的驗證器,經過 ValidBs.valid(IValidator)
方法指定。
默認爲 DefaultValidator。
該驗證策略,支持符合 valid-api 的內置註解,及用戶自定義註解。
JsrValidator 支持 jsr-303 標準註解,及 valid-api 標準的相關注解實現和約束實現。
經過 valid 方法指定便可。
IResult result = ValidBs.on(jsrUser) .valid(JsrValidator.getInstance()) .result() .print();
若是你想添加本身的實現,直接實現 IValidator,而且在 valid() 中指定便可。
能夠參考 DefaultValidator,建議繼承自 AbstractValidator
。
對於驗證的結果,不一樣的場景,需求也各不相同。
你可能有以下需求:
(1)輸出驗證失敗的信息
(2)輸出全部驗證信息
(3)針對驗證失敗的信息拋出異常
(4)對驗證結果進行自定義處理。
爲了知足上述需求,提供了以下的接口,及內置默認實現。
public interface IResultHandler<T> { /** * 對約束結果進行統一處理 * @param constraintResultList 約束結果列表 * @return 結果 */ T handle(final List<IConstraintResult> constraintResultList); }
若是你想自定義處理方式,實現此接口。
並在 ValidBs.result(IResultHandler)
方法中指定使用便可。
僅僅對沒有經過測試的驗證結果進行保留。
參見測試代碼 ValidBsResultHandlerTest
ValidBs.on("12", JsrConstraints.sizeConstraintMin(2)) .result(ResultHandlers.simple()) .print();
DefaultResult{pass=true, notPassList=[], allList=null}
保留全部驗證結果信息,包含經過驗證測試的明細信息。
參見測試代碼 ValidBsResultHandlerTest
ValidBs.on("12", JsrConstraints.sizeConstraintMin(2)) .result(ResultHandlers.detail()) .print();
DefaultResult{pass=true, notPassList=[], allList=[DefaultConstraintResult{pass=true, message='null', value=12, constraint='SizeConstraint', expectValue='null'}]}
IResult 爲驗證結果處理的內置實現接口。
擁有如下常見方法:
方法 | 說明 |
---|---|
pass() | 是否經過驗證 |
notPassList() | 未經過驗證的列表 |
allList() | 全部驗證的列表 |
print() | 控臺輸出驗證結果 |
throwsEx() | 針對未經過驗證的信息拋出 ValidRuntimeException |
@Test(expected = ValidRuntimeException.class) public void methodsTest() { IResult result = ValidBs.on("12", JsrConstraints.sizeConstraintMin(3)) .result(ResultHandlers.detail()) .print() .throwsEx(); Assert.assertFalse(result.pass()); Assert.assertEquals(1, result.notPassList().size()); Assert.assertEquals(1, result.allList().size()); }
DefaultResult{pass=false, notPassList=[DefaultConstraintResult{pass=false, message='預期值爲 <大小必須在範圍內 [3, 2147483647]>,實際值爲 <2>', value=12, constraint='SizeConstraint', expectValue='大小必須在範圍內 [3, 2147483647]'}], allList=[DefaultConstraintResult{pass=false, message='預期值爲 <大小必須在範圍內 [3, 2147483647]>,實際值爲 <2>', value=12, constraint='SizeConstraint', expectValue='大小必須在範圍內 [3, 2147483647]'}]}
Hibernate-validator 主要是基於註解的 Bean 驗證,因此將註解和實現耦合在了一塊兒。
Valid 做爲一個 fluent-api 驗證框架,支持過程式編程,因此將針對屬性驗證的約束獨立出來,便於複用。
public interface IConstraint { /** * 觸發約束規則 * @param context 上下文 * @return 結果 * @since 0.0.3 */ IConstraintResult constraint(final IConstraintContext context); }
前面的例子已經演示瞭如何自定義實現。
直接實現上述接口也能夠,建議繼承 AbstractConstraint
等內置的各類約束抽象類。
當咱們將 IConstraint 獨立出來時,同時有下面的一些問題:
(1)如何指定對應 message
(2)如何指定約束生效條件 condition
(3)如何指定約束的分組信息 group
IValidEntry 接口就是爲了解決這些問題,在 IConstraint 的基礎之上進行一系列的功能加強。
測試代碼,參見類 ValidBsValidEntryTest
IValidEntry validEntry = ValidEntry.of(JsrConstraints.notNullConstraint()); IResult result = ValidBs.on(null, validEntry) .result() .print(); Assert.assertFalse(result.pass());
咱們能夠自定義改約束條件的提示消息。
final IValidEntry validEntry = ValidEntry.of(JsrConstraints.notNullConstraint()) .message("自定義:指定值不能爲空"); IResult result = ValidBs.on(null, validEntry) .valid() .result(); Assert.assertEquals("自定義:指定值不能爲空", result.notPassList().get(0).message());
有時候咱們但願只驗證某一種分組的約束條件。
按照以下方式制定,只有當 ValidEntry 的 group 信息與 ValidBs.group() 符合時,纔會被執行。
final IValidEntry firstEntry = ValidEntry.of(JsrConstraints.sizeConstraint(5, 10)) .group(String.class); final IValidEntry otherEntry = ValidEntry.of(JsrConstraints.sizeConstraint(3, 20)) .group(Integer.class); IResult result = ValidBs .on("12", firstEntry, otherEntry) .fail(Fails.failOver()) .group(String.class) .result(); Assert.assertEquals(1, result.notPassList().size());
其實能夠 group() 只是 condition 的一個特例。
後續將實現 ICondition 接口的相關內置支持,和 @Condition
註解的相關支持。
說到 hibernate-validator,我的以爲最靈魂的設計就是支持用戶自定義註解了。
註解使得使用便利,自定義註解同時保證了靈活性。
下面來看看,如何實現自定義註解。
你能夠認爲內置註解也是一種自定義註解。
本框架的全部實現理念都是如此,能夠認爲全部的內置實現,都是能夠被替換的。
咱們以 @AllEquals
註解爲例,
@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Constraint(AtAllEqualsConstraint.class) public @interface AllEquals { /** * 當前字段及其指定的字段 所有相等 * 1. 字段類型及其餘字段相同 * @return 指定的字段列表 */ String[] value(); /** * 提示消息 * @return 錯誤提示 */ String message() default ""; /** * 分組信息 * @return 分組類 * @since 0.1.2 */ Class[] group() default {}; }
其中 group()/message() 和 IValidEntry 中的方法一一對應。
固然你設計的註解中若是沒有這兩個方法也不要緊,建議提供這兩個屬性。
@Constraint(AtAllEqualsConstraint.class)
這個註解指定了當前註解與對應的約束實現,是最核心的部分。
@Inherited @Documented @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Constraint { /** * 約束條件實現類 * @return 實現類 class */ Class<? extends IAnnotationConstraint> value(); }
這個就是註解相關的約束接口,內容以下:
/** * 註解約束規則接口 * 注意:全部的實現類都須要提供無參構造函數。 * @author binbin.hou * @since 0.0.9 */ public interface IAnnotationConstraint<A extends Annotation> extends IConstraint { /** * 初始化映射關係 * @param annotation 註解信息 * @since 0.0.9 */ void initialize(A annotation); }