Hibernate Validator 對全球化支持的實踐
輸入驗證是 Spring 處理的最重要 Web 開發任務之一,在 Spring MVC 中有兩種方式可以驗證輸入:一種是 Spring 自帶的驗證框架,另外一種是利用 JSR 實現,JSR 驗證比 Spring 自帶的驗證器使用起來方便很多。JSR 是一個規范文檔,指定了一整套 API, 通過標注給對象屬性添加約束。Hibernate Validator 就是 JSR 規范的具體實現,Hibernate Validator 提供了 JSR 規范中所有內置約束注解的實現,以及一些附加的約束注解,除此之外用戶還可以自定義約束注解。然而 Hibernate Validator 提供的接口沒有直接支持輸出本地化的錯誤驗證消息。本文結合項目實踐,總結了如何對內置的和用戶自定義的約束注解提供本地化支持,以及如何從用戶自定義的資源文件中讀取本地化的錯誤驗證消息。
Hibernate Validator 概述
在 Java 應用程序中,必須要對輸入進來的數據從語義上分析是有效的,也就是數據校驗。對數據的校驗是一項貫穿于從表示層到持久化層的常見任務,通常在每個層中都需要做相同的驗證邏輯,然而不同的層通常有不同的開發人員做編碼,這樣就會存在冗余代碼以及語義一致性等代碼管理上的問題,如圖 1 所示。
圖 1. 原始的 Java 分層驗證
為了避免重復驗證以及管理問題,開發人員經常將驗證邏輯與相應的域模型進行捆綁。JSR 349 Bean 驗證 1.1 是一個數據驗證的規范,為 Java Bean 驗證定義了相應的元數據模型和接口,默認的元數據是 Java 注釋,通過使用 XML 對原有的元數據進行覆蓋和擴展。在 Java 應用程序中,通過使用 Bean 驗證自帶的約束或者用戶自定義的約束驗證來確保 Java Bean 的正確性。Bean 驗證是一個 runtime 的數據驗證框架,如果驗證失敗,錯誤信息會立馬返回。從而使驗證邏輯從業務代碼中分離出來,如圖 2 所示。
圖 2. Bean 驗證模型
Hibernate Validator 是 對 JSR 349 驗證規范的具體實現,提供了 JSR 規范中所有內置約束注解的實現,以及一些附加的約束注解,表 1 是 Hibernate Validator 附加約束注解示例。
表 1. Hibernate Validator 附加的約束注解
約束注解 | 含義 |
---|---|
@NotEmpty | 被約束的字符串不能為空 |
@Length | 被約束的字符串長度必須在指定范圍內 |
被約束的是郵件格式 |
Hibernate Validator 對全球化支持概述
世界經濟日益全球化的同時軟件國際化勢在必然,當一個軟件或者應用需要在全球范圍內使用的時候,最簡單的要求就是界面上的信息能用本地化的語言來顯示。然而 Hibernate Validator 4.0 提供的接口對全球化支持存在下面兩個問題:
問題 1:只能顯示英文的錯誤消息,不能讀取翻譯的錯誤驗證消息。因為默認的消息解釋器(message interpolator)使用的是 JVM 默認的 locale(Locale.getDafult()),通常情況下為英文;
問題 2:Hibernate Validator 驗證過程中的失敗消息默認是從類路徑下的資源文件 ValidationMessage.properties 讀取, 然而在實際項目中,通常會根據模塊結構自定義資源文件名稱,方便源代碼的管理以及資源文件的重用。
問題 1 的解決方案
在實際項目中,有兩種方式使用 Hibernate Validator,一種就是使用自定義的約束注解進行驗證;另外一種方式就是使用 Hibernate Validator 內置的約束注解進行驗證。圖 3 列出了對于這兩種驗證方式是如何顯示本地化錯誤驗證消息的。
圖 3. 兩種約束注解對本地化支持的解決辦法
問題 2 的解決方案
如果需要從用戶自定義的資源文件中讀取錯誤驗證消息,而不是默認的 ValidationMessage.properites,可以自定義資源文件定位器 PlatformResourceBundleLocator,將自定義的資源文件列表作為參數傳遞給資源文件定位器。
下面將結合實際項目經驗,詳細介紹如何解決這兩個問題的。
自定義約束注解提供本地化支持
自定義約束注解就是用戶根據自己的需要重新定義一個新的約束注解,通常包括兩部分,一是約束注解的定義,二是約束驗證器。
約束注解就是對某一方法、字段、屬性或其組合形式等進行約束的注解,通常包含以下幾個部分:
- @Target({ }):約束注解應用的目標元素類型,METHOD(約束相關的 getter 方法), FIELD(約束屬性), TYPE(約束 java bean), ANNOTATION_TYPE(用在組合約束中), CONSTRUCTOR(對構造函數的約束), PARAMETER(對參數的約束)。
- @Retention():約束注解應用的時機,比如在應用程序運行時進行約束
- @Constraint(validatedBy ={}):與約束注解關聯的驗證器,每個約束注解都對應一個驗證器
- String message() default " ":約束注解驗證失敗時的輸出消息;
- Class<?>[] groups() default { };:約束注解在驗證時所屬的組別
- Class<? extends Payload>[] payload() default { };:約束注解的有效負載
清單 1 列出的@AlphaNumeric 就是一個自定義的約束注解示例,這是一個只對相關的 getter 方法和屬性進行約束的注解,在應用程序運行時進行驗證,當驗證失敗時,返回的錯誤消息為"Invalid value for it. Allowed characters are letters, numbers, and #.-_()"。
清單 1. 自定義的約束注解@AlphaNumeric
package com.ibm.bean.test;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
@Documented
@Constraint(validatedBy = AlphaNumericValidator.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface AlphaNumeric {
String message() default "Invalid value for it. Allowed characters are letters, numbers, and {allowedPunc}";
String allowedPunc() default "#.-_()";
Class<?>[] groups() default {};
Class<? extends javax.validation.Payload>[] payload() default {};
}
每一個約束注解都存在對應的約束驗證器,約束驗證器用來驗證具體的 Java Bean 是否滿足該約束注解聲明的條件。約束驗證器有兩個方法,方法 initialize 對驗證器進行實例化,它必須在驗證器的實例在使用之前被調用,并保證正確初始化驗證器,它的參數是約束注解;方法 isValid 是進行約束驗證的主體方法,其中 value 參數代表需要驗證的實例,context 參數代表約束執行的上下文環境。清單 1 定義的約束注解@AlphaNumeric,清單 2 給出了與該注解對應的驗證器的實現。
清單 2. 自定義約束注解@AlphaNumeric 對應的驗證器
package com.ibm.bean.test;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.silverpop.marketer.util.PropertyGetter;
/**
* Implementation of the AlphaNumeric validator
*/
public class AlphaNumericValidator implements ConstraintValidator<AlphaNumeric, String> {
AlphaNumeric alphaNumeric;
@Override
public void initialize(AlphaNumeric alphaNumeric) {
this.alphaNumeric = alphaNumeric;
}
@Override
public boolean isValid(String o, ConstraintValidatorContext arg1) {
return isValid(o, alphaNumeric.allowedPunc());
}
public boolean isValid(String o, String allowedPunc) {
boolean isValid = true;
if (o == null) {
return true;
}
for (char ch : ((String) o).toCharArray()) {
if (Character.isWhitespace(ch)) {
continue;
}
if (Character.isLetter(ch)) {
continue;
}
if (Character.isDigit(ch)) {
continue;
}
if (allowedPunc.indexOf(ch) >= 0) {
continue;
}
isValid = false;
}
return isValid;
}
對于這樣的自定義約束驗證器,當驗證失敗后輸出的錯誤信息將是驗證里定義的英文消息"Invalid value for it. Allowed characters are letters, numbers, and #.-_()"。對于支持多語言的應用程序來說,想針對不同區域的客戶顯示不同的錯誤驗證消息提示,ConstraintVioation 這個對象就是用來描述某一驗證的失敗信息。對某一個實體對象進行驗證的時候,會返回 ConstraintViolation 的集合。我們需禁用默認 ConstraintVioation,并將我們的本地化錯誤消息傳遞到 constraintViolationTemplate。清單 3 和清單 4 是就是支持多語言的自定義約束規范。
清單 3. 支持多語言的自定義的約束注解@AlphaNumeric
package com.ibm.bean.test;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
@Documented
@Constraint(validatedBy = AlphaNumericValidator.class)
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface AlphaNumeric {
String message() default " validator.invalidAlphaNumeric"
String allowedPunc() default "#.-_()";
Class<?>[] groups() default {};
Class<? extends javax.validation.Payload>[] payload() default {};
}
validator.invalidAlphaNumeric: 是資源文件中對應的鍵值(key)。這里認為讀者已經熟悉 Java 程序的國際化和本地化。
清單 4. 支持多語言的約束驗證器
package com.ibm.bean.test;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.silverpop.marketer.util.PropertyGetter;
/**
* Implementation of the AlphaNumeric validator
*/
public class AlphaNumericValidator implements ConstraintValidator<AlphaNumeric, String> {
AlphaNumeric alphaNumeric;
@Override
public void initialize(AlphaNumeric alphaNumeric) {
this.alphaNumeric = alphaNumeric;
}
@Override
public boolean isValid(String o, ConstraintValidatorContext arg1) {
boolean isValid =isValid(o, alphaNumeric.allowedPunc());
if(!isValid) {
arg1.disableDefaultConstraintViolation();
String errorMessage = PropertyGetter.getProperty(alphaNumeric.message() ,alphaNumeric.allowedPunc());
arg1.buildConstraintViolationWithTemplate(errorMessage ).addConstraintViolation();
}
return isValid;
}
- PropertyGetter.getProperty(alphaNumeric.message(), alphaNumeric.allowedPunc()):從對應資源文件中讀取相關的驗證信息。
- PropertyGetter:是將讀取 Java 資源文件相關的 API 進行了包裝,不屬于這篇文章討論范圍。有興趣的讀者可以參考文后鏈接。
- arg1.disableDefaultConstraintViolation(): 禁用默認 ConstraintVioation。
- arg1.buildConstraintViolationWithTemplate(errorMessage).addConstraintViolation():將指定的錯誤驗證消息傳遞給 ConstraintVioation。
內置約束注解提供本地化支持
Hibernate Validation 內置約束注解對 Java Bean 的驗證是通過調用 Validator.validate(JavaBeanInstance) 方法后,Bean Validation 會查找在 JavaBeanInstance 上所有的約束聲明,對每一個約束調用對應的約束驗證器進行驗證,最后的結果由約束驗證器的 isValid 方法產生,如果該方法返回 true,則約束驗證成功;如果 isValid 返回 false 驗證失敗,對于失敗的驗證,消息解釋器將從默認的 ValidationMessage.properties 中讀取驗證失敗信息放到約束違規對象(ConstraintViolation 的實例)中。對于支持多語言的應用軟件來說,要想將翻譯的驗證錯誤消息顯示給用戶,我們需要自定義消息解釋器 MessageInterpolator,通過將用戶的 locale 傳遞給自定義的 消息解釋器,實現讀取翻譯的錯誤驗證消息。
清單 5. 自定義的 MessageInterpolator
package com.ibm.bean.test;
public class LocalizedMessageInterpolator implements MessageInterpolator {
private MessageInterpolator defaultInterpolator;
private Locale defaultLocale;
public LocalizedMessageInterpolator(MessageInterpolator interpolator, Locale locale) {
this.defaultLocale = locale;
this.defaultInterpolator = interpolator;
}
/**
* 將用戶的 locale 信息傳遞給消息解釋器,而不是使用默認的 JVM locale 信息
*/
@Override
public String interpolate(String message, Context context) {
return defaultInterpolator.interpolate(message, context, this.defaultLocale);
}
@Override
public String interpolate(String message, Context context, Locale locale) {
return defaultInterpolator.interpolate(message, context, locale);
}
}
通過自定義接口 Validator 調用上述自定義的消息解析器,從而實現將用戶的 locale 信息傳遞給驗證器,當驗證失敗的時候自動讀取對應翻譯的錯誤驗證消息。示例代碼如清單 6 所示。
清單 6. 自定義 Validator Provider
package com.ibm.bean.test;
public class BeanValidator {
public boolean isValid(Object bean, Locale locale) {
return getValidationErrorsFor(bean, locale).isEmpty();
}
public List<ValidationError> getValidationErrorsFor(Object bean, Locale locale) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
MessageInterpolator interpolator = new LocalizedMessageInterpolator(locale);
javax.validation.Validator classValidator = factory.usingContext().messageInterpolator(interpolator).getValidator();
Set<ConstraintViolation<Object>> invalidValues = classValidator.validate(bean);
return validationErrors(invalidValues);
}
private List<ValidationError> validationErrors( Set<ConstraintViolation<Object>> invalidValues) {
List<ValidationError> validationErrors = list();
for (ConstraintViolation<Object> invalidValue : invalidValues) {
validationErrors.add(new ValidationError(invalidValue));
}
return validationErrors;
}
}
Hibernate Validator 使用用戶自定義的資源文件
文章開頭提到 Hibernate Validator 驗證過程中第二個問題是驗證失敗的錯誤消息默認從類路徑下的 ValidationMessage.properties 中讀取,但是很多時候我們希望從自定義的資源文件(resource bundle)文件中讀取,而不是指定路徑下的 ValidationMessage.properties,以便于翻譯以及重用已有的資源文件。在這種情況下,Hibernate Validator 的資源包定位器 ResourceBundleLocator 可以解決這個問題。
Hibernate Validator 中的默認資源文件解析器(ResourceBundleMessageInterpolator)將解析資源文件和檢索相應的錯誤消息委派給資源包定位器 ResourceBundleMessageInterpolator。如果要使用用戶自定義的資源文件,只需要將用戶自定義資源文件名作為參數傳遞給資源包定位器 PlatformResourceBundleLocator,在啟用 ValidatorFactory 的時候將新的資源包定位器示例作為參數傳遞給 ValidatorFactory。示例代碼如清單 7 所示。
清單 7. 使用用戶指定的資源文件
Validator validator = Validation.byDefaultProvider()
.configure()
.messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("MyMessages" )))
.buildValidatorFactory()
.getValidator();
除了 PlatformResourceBundleLocator 外,Hibernate Validator 還提供了另一種資源包定位器的實現,即 AggregateResourceBundleLocator,它允許從多個資源包檢索錯誤消息。 例如,您可以在多模塊應用程序中使用此實現,其中每個模塊都有一個資源文件。示例代碼如清單 8 所示。
清單 8. 從多個資源文件中讀取錯誤驗證消息
Validator validator = Validation.byDefaultProvider()
.configure()
.messageInterpolator(new ResourceBundleMessageInterpolator( new AggregateResourceBundleLocator(Arrays.asList("MyMessages1","MyMessages2","MyMessages3"))))
.buildValidatorFactory()
.getValidator();
讀取資源文件的順序是按照傳遞給構造函數的順序處理的。 這意味著如果多個 resource bundle 包含給定消息鍵的一個條目,則該值將從包含鍵的列表中的第一個 resource bundle 中獲取。
總結
本文總結了 Hibernate Validator 對國際化支持存在的一些問題,結合項目經驗,介紹了如何實現從翻譯的資源文件中讀取錯誤驗證消息,以及如何從用戶項目自定義的資源文件中讀取錯誤驗證消息。希望這篇文章能為正在開發國際化 Web 應用程序的讀者提供一定的參考價值。
參考資料 (resources)
- Hibernate Validator 5.3.4 參考文檔 :主要介紹了 Hibernate Validator 對 JSR349 參考實現的技術規范。
- Bean Validation 技術規范特性概述 :developerWork 上的一篇介紹 Bean Validation 技術規范的文章。
- JSR Bean Validation 介紹 :詳細介紹了 JSR Bean Validation 的規范。
- Java 程序的國際化和本地化介紹 :介紹了 Java 程序如何使用國際化標注。
來自:http://www.ibm.com/developerworks/cn/java/j-cn-hibernate-validator/index.html?ca=drs-