Bean Validation 介紹及最佳實踐
https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/
關於 Bean Validation
在任何時候,當你要處理一個應用程式的業務邏輯,資料校驗是你必須要考慮和麵對的事情。應用程式必須通過某種手段來確保輸入進來的資料從語義上來講是正確的。在通常的情況下,應用程式是分層的,不同的層由不同的開發人員來完成。很多時候同樣的資料驗證邏輯會出現在不同的層,這樣就會導致程式碼冗餘和一些管理的問題,比如說語義的一致性等。為了避免這樣的情況發生,最好是將驗證邏輯與相應的域模型進行繫結。
Bean Validation 為 JavaBean 驗證定義了相應的元資料模型和 API。預設的元資料是 Java Annotations,通過使用 XML 可以對原有的元資料資訊進行覆蓋和擴充套件。在應用程式中,通過使用 Bean Validation 或是你自己定義的 constraint,例如 @NotNull
@Max
, @ZipCode
,
就可以確保資料模型(JavaBean)的正確性。constraint 可以附加到欄位,getter 方法,類或者介面上面。對於一些特定的需求,使用者可以很容易的開發定製化的 constraint。Bean Validation 是一個執行時的資料驗證框架,在驗證之後驗證的錯誤資訊會被馬上返回。
Hibernate Validator 是 Bean Validation 的參考實現 . Hibernate Validator 提供了 JSR 303 規範中所有內建 constraint 的實現,除此之外還有一些附加的 constraint。如果想了解更多有關 Hibernate Validator 的資訊,請檢視
Bean Validation 中的 constraint
表 1. Bean Validation 中內建的 constraint
Constraint | 詳細資訊 |
---|---|
@Null |
被註釋的元素必須為 null |
@NotNull |
被註釋的元素必須不為 null |
@AssertTrue |
被註釋的元素必須為 true |
@AssertFalse |
被註釋的元素必須為 false |
@Min(value) |
被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@Max(value) |
被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@DecimalMin(value) |
被註釋的元素必須是一個數字,其值必須大於等於指定的最小值 |
@DecimalMax(value) |
被註釋的元素必須是一個數字,其值必須小於等於指定的最大值 |
@Size(max,
min) |
被註釋的元素的大小必須在指定的範圍內 |
@Digits (integer,
fraction) |
被註釋的元素必須是一個數字,其值必須在可接受的範圍內 |
@Past |
被註釋的元素必須是一個過去的日期 |
@Future |
被註釋的元素必須是一個將來的日期 |
@Pattern(value) |
被註釋的元素必須符合指定的正則表示式 |
表 2. Hibernate Validator 附加的 constraint
Constraint | 詳細資訊 |
---|---|
@Email |
被註釋的元素必須是電子郵箱地址 |
@Length |
被註釋的字串的大小必須在指定的範圍內 |
@NotEmpty |
被註釋的字串的必須非空 |
@Range |
被註釋的元素必須在合適的範圍內 |
一個 constraint 通常由 annotation 和相應的 constraint validator 組成,它們是一對多的關係。也就是說可以有多個 constraint validator 對應一個 annotation。在執行時,Bean Validation 框架本身會根據被註釋元素的型別來選擇合適的 constraint validator 對資料進行驗證。
有些時候,在使用者的應用中需要一些更復雜的 constraint。Bean Validation 提供擴充套件 constraint 的機制。可以通過兩種方法去實現,一種是組合現有的 constraint 來生成一個更復雜的 constraint,另外一種是開發一個全新的 constraint。
建立一個包含驗證邏輯的簡單應用(基於 JSP)
在本文中,通過建立一個虛構的訂單管理系統(基於 JSP 的 web 應用)來演示如何在 Java 開發過程中應用 Bean Validation。該簡化的系統可以讓使用者建立和檢索訂單。
系統設計和運用的技術
圖 1. 系統架構
圖 1 是報表管理系統的結構圖,是典型的 MVC(Model-View-Controller)應用。Controller 負責接收和處理請求,Servlet 扮演 Controller 的角色去處理請求、業務邏輯並轉向合適的 JSP 頁面。在 Servlet 中對資料進行驗證。JSP 扮演 View 的角色以圖型化介面的方式呈現 Model 中的資料方便使用者互動。Model 就是此係統進行操作的資料模型,我們對這部分加以簡化不對資料進行持久化。
資料模型
圖 2. 資料模型
圖 2 展示的是訂單管理系統的資料模型。
聲明瞭 contraint 的 JavaBean
清單 1. Order.java
public class Order { // 必須不為 null, 大小是 10 @NotNull @Size(min = 10, max = 10) private String orderId; // 必須不為空 @NotEmpty private String customer; // 必須是一個電子信箱地址 @Email private String email; // 必須不為空 @NotEmpty private String address; // 必須不為 null, 必須是下面四個字串'created', 'paid', 'shipped', 'closed'其中之一 // @Status 是一個定製化的 contraint @NotNull @Status private String status; // 必須不為 null @NotNull private Date createDate; // 巢狀驗證 @Valid private Product product; … getter 和 setter }
清單 2. Product.java
public class Product {
// 必須非空
@NotEmpty
private String productName;
// 必須在 8000 至 10000 的範圍內
// @Price 是一個定製化的 constraint
@Price
private float price;
…
Getter 和 setter
}
清單 3. OrderQuery.java
// 'to'所表示的日期必須在'from'所表示的日期之後 // @QueryConstraint 是一個定製化的 constraint @QueryConstraint public class OrderQuery { private Date from; private Date to; … omitted … Getter and setter }
定製化的 constraint
@Price
是一個定製化的
constraint,由兩個內建的 constraint 組合而成。
清單 4. @Price 的 annotation 部分
// @Max 和 @Min 都是內建的 constraint @Max(10000) @Min(8000) @Constraint(validatedBy = {}) @Documented @Target( { ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Price { String message() default "錯誤的價格"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Status
是一個新開發的
constraint.
清單 5. @Status 的 annotation 部分
@Constraint(validatedBy = {StatusValidator.class}) @Documented @Target( { ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface Status { String message() default "不正確的狀態 , 應該是 'created', 'paid', shipped', closed'其中之一"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
清單 6. @Status 的 constraint validator 部分
public class StatusValidator implements ConstraintValidator<Status, String>{ private final String[] ALL_STATUS = {"created", "paid", "shipped", "closed"}; public void initialize(Status status) { } public boolean isValid(String value, ConstraintValidatorContext context) { if(Arrays.asList(ALL_STATUS).contains(value)) return true; return false; } }
Bean Validation API 使用示例
建立訂單
使用者在建立一條訂單記錄時,需要填寫以下資訊:訂單編號,客戶,電子信箱,地址,狀態,產品名稱,產品價格
圖 3. 建立訂單
對這些資訊的校驗,使用 Bean Validation API
清單 7. 程式碼片段
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); // 從 request 中獲取輸入資訊 String orderId = (String) req.getParameter("orderId"); String customer = (String) req.getParameter("customer"); String email = (String) req.getParameter("email"); String address = (String) req.getParameter("address"); String status = (String) req.getParameter("status"); String productName = (String) req.getParameter("productName"); String productPrice = (String) req.getParameter("productPrice"); // 將 Bean 放入 session 中 Order order = new Order(); order.setOrderId(orderId); order.setCustomer(customer); order.setEmail(email); order.setAddress(address); order.setStatus(status); order.setCreateDate(new Date()); Product product = new Product(); product.setName(productName); if(productPrice != null && productPrice.length() > 0) product.setPrice(Float.valueOf(productPrice)); order.setProduct(product); session.setAttribute("order", order); ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Set<ConstraintViolation<Order>> violations = validator.validate(order); if(violations.size() == 0) { session.setAttribute("order", null); session.setAttribute("errorMsg", null); resp.sendRedirect("creatSuccessful.jsp"); } else { StringBuffer buf = new StringBuffer(); ResourceBundle bundle = ResourceBundle.getBundle("messages"); for(ConstraintViolation<Order> violation: violations) { buf.append("-" + bundle.getString(violation.getPropertyPath().toString())); buf.append(violation.getMessage() + "<BR>\n"); } session.setAttribute("errorMsg", buf.toString()); resp.sendRedirect("createOrder.jsp"); } }
如果使用者不填寫任何資訊提交訂單,相應的錯誤資訊將會顯示在頁面上
圖 4. 驗證後返回錯誤資訊
其實在整個程式的任何地方都可以呼叫 JSR 303 API 去對資料進行校驗,然後將校驗後的結果返回。
清單 8. 呼叫 JSR 303 API 進行校驗
Order order = new Order(); … ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Set<ConstraintViolation<Order>> violations = validator.validate(order);