Java資料校驗詳解
一切從超程式設計開始
一個健壯的系統都要對外部提交的資料進行完整性、合法性的校驗。即使開發一個不面對終端使用者的工具包,也需要對傳入的資料進行縝密的校驗來防止引發底層難以追蹤的問題。各路大神當然也會注意到這個問題,所以在“超程式設計”(見JSR250與資源控制)提出之後相續提交了JSR-303、JSR-349以及JSR-380來完善使用註解進行資料校驗的機制,這三個JSR也被稱為Bean Validation 1.0、Bean Validation 1.1和Bean Validation 2.0,後文統稱為Bean Validation。
先看一個不使用Bean Validation校驗資料的程式碼:
public class StandardValidation { public static void main(String[] args) { System.out.println(validationWithoutAnnotation(" ", -1)); } public static String validationWithoutAnnotation(String inputString, Integer inputInt) { String error = null; if (null == inputString) { error = "inputString不能為null"; } else if (null == inputInt) { error = "inputInt不能為null"; } else if (1 > inputInt.compareTo(0)) { error = "inputInt必須大於0"; } else if (inputString.isEmpty() || inputString.trim().isEmpty()) { error = "inputString不能為空字串"; } else { // DO } return error; } }
相信很多碼友多少都寫過類似的程式碼。使用IF—ELSE是否優雅這種高階問題暫且不談,但是大量的IF—ELSE會導致業務內容越來越多的巢狀在程式碼中。針對這些問題Bean Validation為資料校驗提供了更加規範化、通用化、複用程度更高的校驗方法。
資料校驗的原理並不複雜,主要是用註解(Annotation)在域或setter方法上宣告JavaBean中資料的準則。Java的資料校驗程式碼主要在javax.validation包中,包括註解、校驗器以及校驗器工廠,接下來通過例子說明。(例子可執行程式碼在本人的gitee庫,本文程式碼在chkui.springcore.example.javabase.validation包)
標準資料校驗
JSR提交的Javax.validation定義中已經為資料校驗定義了很多方法和註解,但是需要清晰的是JSR僅僅制定了一個規範,具體的功能是由各種框架實現的。本文的例子引入了Hibernate Validator 6.0.12.Final包,他與Spring Validator一樣,都是根據JSR規範實現校驗功能。
資料校驗是圍繞一個實體類展開的,下面的程式碼聲明瞭一個實體類,通過註解標註每個域上的賦值規則:
package chkui.springcore.example.javabase.validation.entity;
public class Game {
@NotNull //非空
@Length(min=0, max=5) //字串長度小於5,這個是一個Hibernate Validator增加的註解
private String name;
@NotNull
private String description;
@NotNull
@Min(0) //最小值>=0
@Max(10) //最大值<=10
private int currentVersion;
//getter and setter…………
}
使用校驗器對其進行校驗:
public StandardValidation {
public void validate() {
//引入校驗工具
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
//獲取校驗器
Validator validator = factory.getValidator();
Game wow = new Game();
//執行校驗
Set<ConstraintViolation<Game>> violationSet = validator.validate(wow);
violationSet.forEach(violat -> {
violat.getPropertyPath();//校驗錯誤的域
violat.getMessage());//校驗錯誤的資訊
});
//設定值之後再次進行校驗
wow.setName("World Of Warcraft");
wow.setDescription("由著名遊戲公司暴雪娛樂所製作的第一款網路遊戲,屬於大型多人線上角色扮演遊戲。");
wow.setCurrentVersion(8);
violationSet = validator.validate(wow);
violationSet.forEach(violat -> {});
}
}
執行完畢之後violationSet中就是校驗的結果。如果校驗通過那麼返回的Set長度為0。
Bean Validation已經為常規的校驗功能預設了很多註解,詳見關於所有註解的介紹。
自定義校驗規則
雖然在javax.validation.constraints已經定義了很多用於校驗的註解,但是肯定無法滿足複雜多樣的業務需求。所以Bean Validation也支援自定義校驗規則。在JSR的文件中對資料域的一個校驗被稱為Constraint(約束),一個Constraint由一個Annotation(註解)繫結1~n個Validator(校驗器)組成。 因此可以通過新增Annotation和Validator來定義新的校驗方式(或者說是定義新的Constraint)。
組合註解校驗
可以通過組合已有的註解來實現新的資料校驗規則。例如下面的例子。
定義新的校驗註解:
package chkui.springcore.example.javabase.validation.annotation;
@Min(1)//最小值>=1
@Max(300)//最大值<=300
@Constraint(validatedBy = {}) //不制定校驗器
@Documented
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Price {
String message() default "定價必須在$1~$200之間";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
在@Price註解中我們標記了@Min(1)和@Max(300),之後直接在域上標記@Price就會校驗對應的值是否滿足這個條件:
package chkui.springcore.example.javabase.validation.entity;
public class Game {
@Price
private float price;
//Other field
//setter and getter
}
自定義校驗器
除了組合javax.validation.constraints中的註解,還可以自定義校驗器(Validator)進行資料校驗。
宣告一個用於自定義校驗的註解:
package chkui.springcore.example.javabase.validation.annotation;
@Constraint(validatedBy = { TypeValidator.class }) //指定校驗器
@Documented
@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Type {
String message() default "遊戲型別錯誤,可選型別為RPG、ACT、SLG、ARPG";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
注意@Constraint(validatedBy = { TypeValidator.class })這一行程式碼,他的作用就是將這個註解和校驗器進行繫結,當我們執行Validator::validator方法時對應的校驗器會被呼叫。
TypeValidator類:
package chkui.springcore.example.javabase.validation.validator;
public class TypeValidator implements ConstraintValidator<Type, String> {
private final List<String> TYPE = Arrays.asList(new String[]{"RPG", "ACT", "SLG", "ARPG"});
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return TYPE.contains(value);
}
}
TypeValidator必須實現ConstraintValidator這個介面,並在範型中宣告對應的校驗註解和資料型別(ConstraintValidator<T, E>,T是繫結的註解型別、E是資料型別)。TypeValidator中判斷數值是不是"RPG", "ACT", "SLG", "ARPG"當中的一個,若不是則TypeValidator::isValid返回false表示校驗沒通過。
在實體類的域上使用自定義的@Type註解:
public class Game {
@NotNull
@Type
private String type;
//Other field ......
//getter and setter ......
}
分組校驗
對於業務來說資料錄入的規則並不是一成不變的,往往需要根據某些狀態來對單個或一組資料進行校驗。這個時候我們可以用到分組功能——根據狀態啟用一組約束。
觀察自定義註解或javax.validation.constraints包中預定以的註解,都有一個groups引數:
public @interface Max {
String message() default "{javax.validation.constraints.Max.message}";
Class<?>[] groups() default { }; //用於分組的引數
Class<? extends Payload>[] payload() default { };
long value();
}
如果未指定該引數,那麼校驗都屬於javax.validation.groups.Default分組。
先定義一個分組,用一個沒有任何功能的類或者介面即可:
package chkui.springcore.example.javabase.validation.groups;
public interface BetaGroup {}
然後在校驗的註解上通過groups指定分組:
public class Game {
@NotNull
@Min(0) //最小值>=0
@Max(10) //最大值<=10
@Max(value=0, message="未發行的遊戲版本為0!", groups = BetaGroup.class)//分組校驗
private int currentVersion;
@AssertTrue(groups = BetaGroup.class)//分組校驗
//表示是否為內側版
private boolean beta;
//Other field ......
//getter and setter ......
}
然後執行分組校驗:
public enum StandardValidation {
public void validate() {
//引入校驗工具
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Game wow = new Game();
wow.setName("World Of Warcraft");
wow.setDescription("由著名遊戲公司暴雪娛樂所製作的第一款網路遊戲,屬於大型多人線上角色扮演遊戲。");
wow.setCurrentVersion(8);
wow.setType("RPG");
wow.setPrice(401.01F);
//使用預設分組校驗
violationSet = validator.validate(wow);
//指定分組校驗
violationSet = validator.validate(wow, BetaGroup.class);
}
}
Validator::validator方法未指定分組時,相當於使用javax.validation.groups.Default分組。而在violationSet=validator.validate(wow, BetaGroup.class);這一行程式碼指定分組之後,只會執行groups = BetaGroup.class註解的校驗。
可以一次指定多個分組的校驗,這樣有利於處理複雜的狀態:
validator.validate(wow, Default.class, BetaGroup.class, OtherGroup.class);
校驗錯誤級別
校驗的註解中還有一個引數——payload,他表示“校驗問題”的級別。這個引數就像使用Log4j輸出日誌會指定DEBUG、INFO、WARN等級別一樣,在校驗資料時會有對“校驗問題”進行分類的需求,比如某些頁面會對使用者錄入的資料進行“錯誤”或“警告”的提示。
在使用payload時需要先宣告PalyLoad介面類以標定“問題級別”:
package chkui.springcore.example.javabase.validation;
public class PayLoadLevel {
//警告級別
static public interface WARN extends Payload {}
//錯誤級別
static public interface Error extends Payload {}
}
然後在JavaBean上指定“校驗問題”的級別:
public class Game {
//預設分組校驗錯誤時,錯誤級別為Error
@NotNull(payload=PayLoadLevel.Error.class)
@Min(value=0, payload=PayLoadLevel.Error.class)
@Max(value=10, payload=PayLoadLevel.Error.class)
//BetaGroup分組錯誤級別為WARN
@Max(value=0, message="未發行的遊戲版本為0!", groups = BetaGroup.class, payload=PayLoadLevel.WARN.class)
private int currentVersion;
@AssertTrue(groups = BetaGroup.class, payload=PayLoadLevel.WARN.class)
private boolean beta;
//Other field ......
//getter and setter ......
}
然後在執行校驗的時候使用ConstraintViolation::getConstraintDescriptor::getPayload方法獲取每一個校驗問題的錯誤級別:
violationSet = validator.validate(wow, BetaGroup.class);
violationSet.forEach(violat -> {
violat.getPropertyPath();//錯誤域的名稱
violat.getMessage();//錯誤訊息
violat.getConstraintDescriptor().getPayload();//錯誤級別
});
本文轉自:https://dwz.pm/88