bean validation 1.1目前實作是hibernate validator 5,且spring4才支援。接下來我們從以下幾個方法講解bean validation 1.1,當然不一定是新特性:
內建bean validation 1.1到springmvc
分組驗證、分組順序及級聯驗證
消息中使用el表達式
方法參數/傳回值驗證
自定義驗證規則
類級别驗證器
腳本驗證器
cross-parameter,跨參數驗證
混合類級别驗證器和跨參數驗證器
組合多個驗證注解
本地化
因為大多數時候驗證都配合web架構使用,而且很多朋友都咨詢過如分組/跨參數驗證,是以本文介紹下這些,且是和springmvc架構內建的例子,其他使用方式(比如內建到jpa中)可以參考其官方文檔:
1.1、項目搭建
首先添加hibernate validator 5依賴:

<dependency>
<groupid>org.hibernate</groupid>
<artifactid>hibernate-validator</artifactid>
<version>5.0.2.final</version>
</dependency>
如果想在消息中使用el表達式,請確定el表達式版本是 2.2或以上,如使用tomcat6,請到tomcat7中拷貝相應的el jar包到tomcat6中。

<groupid>javax.el</groupid>
<artifactid>javax.el-api</artifactid>
<version>2.2.4</version>
<scope>provided</scope>
請確定您使用的web容器有相應版本的el jar包。
對于其他pom依賴請下載下傳附件中的項目參考。
1.2、spring mvc配置檔案(spring-mvc.xml):

<!-- 指定自己定義的validator -->
<mvc:annotation-driven validator="validator"/>
<!-- 以下 validator conversionservice 在使用 mvc:annotation-driven 會 自動注冊-->
<bean id="validator" class="org.springframework.validation.beanvalidation.localvalidatorfactorybean">
<property name="providerclass" value="org.hibernate.validator.hibernatevalidator"/>
<!-- 如果不加預設到 使用classpath下的 validationmessages.properties -->
<property name="validationmessagesource" ref="messagesource"/>
</bean>
<!-- 國際化的消息資源檔案(本系統中主要用于顯示/錯誤消息定制) -->
<bean id="messagesource" class="org.springframework.context.support.reloadableresourcebundlemessagesource">
<property name="basenames">
<list>
<!-- 在web環境中一定要定位到classpath 否則預設到目前web應用下找 -->
<value>classpath:messages</value>
<value>classpath:org/hibernate/validator/validationmessages</value>
</list>
</property>
<property name="usecodeasdefaultmessage" value="false"/>
<property name="defaultencoding" value="utf-8"/>
<property name="cacheseconds" value="60"/>
此處主要把bean validation的消息查找委托給spring的messagesource。
1.3、實體驗證注解:

public class user implements serializable {
@notnull(message = "{user.id.null}")
private long id;
@notempty(message = "{user.name.null}")
@length(min = 5, max = 20, message = "{user.name.length.illegal}")
@pattern(regexp = "[a-za-z]{5,20}", message = "{user.name.illegal}")
private string name;
@notnull(message = "{user.password.null}")
private string password;
}
1.4、錯誤消息檔案messages.properties:

user.id.null=使用者編号不能為空
user.name.null=使用者名不能為空
user.name.length.illegal=使用者名長度必須在5到20之間
user.name.illegal=使用者名必須是字母
user.password.null=密碼不能為空
1.5、控制器

@controller
public class usercontroller {
@requestmapping("/save")
public string save(@valid user user, bindingresult result) {
if(result.haserrors()) {
return "error";
}
return "success";
}
1.6、錯誤頁面:

<spring:hasbinderrors name="user">
<c:if test="${errors.fielderrorcount > 0}">
字段錯誤:<br/>
<c:foreach items="${errors.fielderrors}" var="error">
<spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultmessage}"/>
${error.field}------${message}<br/>
</c:foreach>
</c:if>
<c:if test="${errors.globalerrorcount > 0}">
全局錯誤:<br/>
<c:foreach items="${errors.globalerrors}" var="error">
<c:if test="${not empty message}">
${message}<br/>
</c:if>
</spring:hasbinderrors>
1.7、測試

name------使用者名必須是字母
name------使用者名長度必須在5到20之間
password------密碼不能為空
id------使用者編号不能為空
基本的內建就完成了。
如上測試有幾個小問題:
1、錯誤消息順序,大家可以看到name的錯誤消息順序不是按照書寫順序的,即不确定;
2、我想顯示如:使用者名【zhangsan】必須在5到20之間;其中我們想動态顯示:使用者名、min,max;而不是寫死了;
3、我想在修改的時候隻驗證使用者名,其他的不驗證怎麼辦。
接下來我們挨着試試吧。
如果我們想在新增的情況驗證id和name,而修改的情況驗證name和password,怎麼辦? 那麼就需要分組了。
首先定義分組接口:

public interface first {
public interface second {
分組接口就是兩個普通的接口,用于辨別,類似于java.io.serializable。
接着我們使用分組接口辨別實體:

@notnull(message = "{user.id.null}", groups = {first.class})
@length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {second.class})
@pattern(regexp = "[a-za-z]{5,20}", message = "{user.name.illegal}", groups = {second.class})
@notnull(message = "{user.password.null}", groups = {first.class, second.class})
驗證時使用如:

@requestmapping("/save")
public string save(@validated({second.class}) user user, bindingresult result) {
if(result.haserrors()) {
return "error";
return "success";
即通過@validate注解辨別要驗證的分組;如果要驗證兩個的話,可以這樣@validated({first.class, second.class})。
接下來我們來看看通過分組來指定順序;還記得之前的錯誤消息嗎? user.name會顯示兩個錯誤消息,而且順序不确定;如果我們先驗證一個消息;如果不通過再驗證另一個怎麼辦?可以通過@groupsequence指定分組驗證順序:

@groupsequence({first.class, second.class, user.class})
@length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {first.class})
通過@groupsequence指定驗證順序:先驗證first分組,如果有錯誤立即傳回而不會驗證second分組,接着如果first分組驗證通過了,那麼才去驗證second分組,最後指定user.class表示那些沒有分組的在最後。這樣我們就可以實作按順序驗證分組了。
另一個比較常見的就是級聯驗證:
如:

public class user {
@valid
@convertgroup(from=first.class, to=second.class)
private organization o;
1、級聯驗證隻要在相應的字段上加@valid即可,會進行級聯驗證;@convertgroup的作用是當驗證o的分組是first時,那麼驗證o的分組是second,即分組驗證的轉換。
假設我們需要顯示如:使用者名[name]長度必須在[min]到[max]之間,此處大家可以看到,我們不想把一些資料寫死,如name、min、max;此時我們可以使用el表達式。

@length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {first.class})
錯誤消息:

user.name.length.illegal=使用者名長度必須在{min}到{max}之間
其中我們可以使用{驗證注解的屬性}得到這些值;如{min}得到@length中的min值;其他的也是類似的。
到此,我們還是無法得到出錯的那個輸入值,如name=zhangsan。此時就需要el表達式的支援,首先确定引入el jar包且版本正确。然後使用如:

user.name.length.illegal=使用者名[${validatedvalue}]長度必須在5到20之間
使用如el表達式:${validatedvalue}得到輸入的值,如zhangsan。當然我們還可以使用如${min > 1 ? '大于1' : '小于等于1'},及在el表達式中也能拿到如@length的min等資料。
另外我們還可以拿到一個java.util.formatter類型的formatter變量進行格式化:

${formatter.format("%04d", min)}
有時候預設的規則可能還不夠,有時候還需要自定義規則,比如屏蔽關鍵詞驗證是非常常見的一個功能,比如在發帖時文章中不允許出現admin等關鍵詞。
1、定義驗證注解

package com.sishuok.spring4.validator;
import javax.validation.constraint;
import javax.validation.payload;
import java.lang.annotation.documented;
import java.lang.annotation.retention;
import java.lang.annotation.target;
import static java.lang.annotation.elementtype.*;
import static java.lang.annotation.retentionpolicy.*;
/**
* <p>user: zhang kaitao
* <p>date: 13-12-15
* <p>version: 1.0
*/
@target({ field, method, parameter, annotation_type })
@retention(runtime)
//指定驗證器
@constraint(validatedby = forbiddenvalidator.class)
@documented
public @interface forbidden {
//預設錯誤消息
string message() default "{forbidden.word}";
//分組
class<?>[] groups() default { };
//負載
class<? extends payload>[] payload() default { };
//指定多個時使用
@target({ field, method, parameter, annotation_type })
@retention(runtime)
@documented
@interface list {
forbidden[] value();
2、 定義驗證器

import org.hibernate.validator.internal.engine.constraintvalidation.constraintvalidatorcontextimpl;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.context.applicationcontext;
import org.springframework.util.stringutils;
import javax.validation.constraintvalidator;
import javax.validation.constraintvalidatorcontext;
import java.io.serializable;
public class forbiddenvalidator implements constraintvalidator<forbidden, string> {
private string[] forbiddenwords = {"admin"};
@override
public void initialize(forbidden constraintannotation) {
//初始化,得到注解資料
public boolean isvalid(string value, constraintvalidatorcontext context) {
if(stringutils.isempty(value)) {
return true;
for(string word : forbiddenwords) {
if(value.contains(word)) {
return false;//驗證失敗
}
return true;
驗證器中可以使用spring的依賴注入,如注入:@autowired private applicationcontext ctx;
3、使用

@forbidden()
4、當我們在送出name中含有admin的時候會輸出錯誤消息:

forbidden.word=您輸入的資料中有非法關鍵詞
問題來了,哪個詞是非法的呢?bean validation 和 hibernate validator都沒有提供相應的api提供這個資料,怎麼辦呢?通過跟蹤代碼,發現一種不是特别好的方法:我們可以覆寫org.hibernate.validator.internal.metadata.descriptor.constraintdescriptorimpl實作(即複制一份代碼放到我們的src中),然後覆寫buildannotationparametermap方法;

private map<string, object> buildannotationparametermap(annotation annotation) {
……
//将collections.unmodifiablemap( parameters );替換為如下語句
return parameters;
即允許這個資料可以修改;然後在forbiddenvalidator中:

for(string word : forbiddenwords) {
if(value.contains(word)) {
((constraintvalidatorcontextimpl)context).getconstraintdescriptor().getattributes().put("word", word);
return false;//驗證失敗
通過((constraintvalidatorcontextimpl)context).getconstraintdescriptor().getattributes().put("word", word);添加自己的屬性;放到attributes中的資料可以通過${} 擷取。然後消息就可以變成:

forbidden.word=您輸入的資料中有非法關鍵詞【{word}】
這種方式不是很友好,但是可以解決我們的問題。
典型的如密碼、确認密碼的場景,非常常用;如果沒有這個功能我們需要自己寫代碼來完成;而且經常重複自己。接下來看看bean validation 1.1如何實作的。
6.1、定義驗證注解

import javax.validation.constraints.notnull;
@target({ type, annotation_type})
@constraint(validatedby = checkpasswordvalidator.class)
public @interface checkpassword {
string message() default "";
checkpassword[] value();
6.2、 定義驗證器

import com.sishuok.spring4.entity.user;
public class checkpasswordvalidator implements constraintvalidator<checkpassword, user> {
public void initialize(checkpassword constraintannotation) {
public boolean isvalid(user user, constraintvalidatorcontext context) {
if(user == null) {
//沒有填密碼
if(!stringutils.hastext(user.getpassword())) {
context.disabledefaultconstraintviolation();
context.buildconstraintviolationwithtemplate("{password.null}")
.addpropertynode("password")
.addconstraintviolation();
return false;
if(!stringutils.hastext(user.getconfirmation())) {
context.buildconstraintviolationwithtemplate("{password.confirmation.null}")
.addpropertynode("confirmation")
//兩次密碼不一樣
if (!user.getpassword().trim().equals(user.getconfirmation().trim())) {
context.buildconstraintviolationwithtemplate("{password.confirmation.error}")
其中我們通過disabledefaultconstraintviolation禁用預設的限制;然後通過buildconstraintviolationwithtemplate(消息模闆)/addpropertynode(所屬屬性)/addconstraintviolation定義我們自己的限制。
6.3、使用

@checkpassword()
放到類頭上即可。

@scriptassert(script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")
通過腳本驗證是非常簡單而且強大的,lang指定腳本語言(請參考javax.script.scriptenginemanager jsr-223),alias是在腳本驗證中user對象的名字,但是大家會發現一個問題:錯誤消息怎麼顯示呢? 在springmvc 中會添加到全局錯誤消息中,這肯定不是我們想要的,我們改造下吧。
7.1、定義驗證注解

import org.hibernate.validator.internal.constraintvalidators.scriptassertvalidator;
import static java.lang.annotation.elementtype.type;
import static java.lang.annotation.retentionpolicy.runtime;
@target({ type })
@constraint(validatedby = {propertyscriptassertvalidator.class})
public @interface propertyscriptassert {
string message() default "{org.hibernate.validator.constraints.scriptassert.message}";
string lang();
string script();
string alias() default "_this";
string property();
@target({ type })
public @interface list {
propertyscriptassert[] value();
和scriptassert沒什麼差別,隻是多了個property用來指定出錯後給實體的哪個屬性。
7.2、驗證器

import javax.script.scriptexception;
import javax.validation.constraintdeclarationexception;
import com.sishuok.spring4.validator.propertyscriptassert;
import org.hibernate.validator.constraints.scriptassert;
import org.hibernate.validator.internal.util.contracts;
import org.hibernate.validator.internal.util.logging.log;
import org.hibernate.validator.internal.util.logging.loggerfactory;
import org.hibernate.validator.internal.util.scriptengine.scriptevaluator;
import org.hibernate.validator.internal.util.scriptengine.scriptevaluatorfactory;
import static org.hibernate.validator.internal.util.logging.messages.messages;
public class propertyscriptassertvalidator implements constraintvalidator<propertyscriptassert, object> {
private static final log log = loggerfactory.make();
private string script;
private string languagename;
private string alias;
private string property;
private string message;
public void initialize(propertyscriptassert constraintannotation) {
validateparameters( constraintannotation );
this.script = constraintannotation.script();
this.languagename = constraintannotation.lang();
this.alias = constraintannotation.alias();
this.property = constraintannotation.property();
this.message = constraintannotation.message();
public boolean isvalid(object value, constraintvalidatorcontext constraintvalidatorcontext) {
object evaluationresult;
scriptevaluator scriptevaluator;
try {
scriptevaluatorfactory evaluatorfactory = scriptevaluatorfactory.getinstance();
scriptevaluator = evaluatorfactory.getscriptevaluatorbylanguagename( languagename );
catch ( scriptexception e ) {
throw new constraintdeclarationexception( e );
evaluationresult = scriptevaluator.evaluate( script, value, alias );
throw log.geterrorduringscriptexecutionexception( script, e );
if ( evaluationresult == null ) {
throw log.getscriptmustreturntrueorfalseexception( script );
if ( !( evaluationresult instanceof boolean ) ) {
throw log.getscriptmustreturntrueorfalseexception(
script,
evaluationresult,
evaluationresult.getclass().getcanonicalname()
);
if(boolean.false.equals(evaluationresult)) {
constraintvalidatorcontext.disabledefaultconstraintviolation();
constraintvalidatorcontext
.buildconstraintviolationwithtemplate(message)
.addpropertynode(property)
return boolean.true.equals( evaluationresult );
private void validateparameters(propertyscriptassert constraintannotation) {
contracts.assertnotempty( constraintannotation.script(), messages.parametermustnotbeempty( "script" ) );
contracts.assertnotempty( constraintannotation.lang(), messages.parametermustnotbeempty( "lang" ) );
contracts.assertnotempty( constraintannotation.alias(), messages.parametermustnotbeempty( "alias" ) );
contracts.assertnotempty( constraintannotation.property(), messages.parametermustnotbeempty( "property" ) );
contracts.assertnotempty( constraintannotation.message(), messages.parametermustnotbeempty( "message" ) );
和之前的類級别驗證器類似,就不多解釋了,其他代碼全部拷貝自org.hibernate.validator.internal.constraintvalidators.scriptassertvalidator。
7.3、使用

@propertyscriptassert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")
和之前的差別就是多了個property,用來指定出錯時給哪個字段。 這個相對之前的類級别驗證器更通用一點。
直接看示例;

<bean class="org.springframework.validation.beanvalidation.methodvalidationpostprocessor">
<property name="validator" ref="validator"/>
8.2、service

@validated
@service
public class userservice {
@crossparameter
public void changepassword(string password, string confirmation) {
通過@validated注解userservice表示該類中有需要進行方法參數/傳回值驗證; @crossparameter注解方法表示要進行跨參數驗證;即驗證password和confirmation是否相等。
8.3、驗證注解

//省略import
@constraint(validatedby = crossparametervalidator.class)
@target({ method, constructor, annotation_type })
public @interface crossparameter {
string message() default "{password.confirmation.error}";
8.4、驗證器

@supportedvalidationtarget(validationtarget.parameters)
public class crossparametervalidator implements constraintvalidator<crossparameter, object[]> {
public void initialize(crossparameter constraintannotation) {
public boolean isvalid(object[] value, constraintvalidatorcontext context) {
if(value == null || value.length != 2) {
throw new illegalargumentexception("must have two args");
if(value[0] == null || value[1] == null) {
if(value[0].equals(value[1])) {
return false;
其中@supportedvalidationtarget(validationtarget.parameters)表示驗證參數; value将是參數清單。
8.5、使用

@requestmapping("/changepassword")
public string changepassword(
@requestparam("password") string password,
@requestparam("confirmation") string confirmation, model model) {
try {
userservice.changepassword(password, confirmation);
} catch (constraintviolationexception e) {
for(constraintviolation violation : e.getconstraintviolations()) {
system.out.println(violation.getmessage());
調用userservice.changepassword方法,如果驗證失敗将抛出constraintviolationexception異常,然後得到constraintviolation,調用getmessage即可得到錯誤消息;然後到前台顯示即可。
從以上來看,不如之前的使用友善,需要自己對錯誤消息進行處理。 下一節我們也寫個腳本方式的跨參數驗證器。
9.1、驗證注解

@constraint(validatedby = {
crossparameterscriptassertclassvalidator.class,
crossparameterscriptassertparametervalidator.class
})
@target({ type, field, parameter, method, constructor, annotation_type })
public @interface crossparameterscriptassert {
string message() default "error";
string property() default "";
constrainttarget validationappliesto() default constrainttarget.implicit;
}
此處我們通過@constraint指定了兩個驗證器,一個類級别的,一個跨參數的。validationappliesto指定為constrainttarget.implicit,表示隐式自動判斷。
9.2、驗證器
請下載下傳源碼檢視
9.3、使用
9.3.1、類級别使用

@crossparameterscriptassert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")
指定property即可,其他和之前的一樣。
9.3.2、跨參數驗證

@crossparameterscriptassert(script = "args[0] == args[1]", lang = "javascript", alias = "args", message = "{password.confirmation.error}")
public void changepassword(string password, string confirmation) {
通過args[0]==args[1] 來判斷是否相等。
這樣,我們的驗證注解就自動适應兩種驗證規則了。
有時候,可能有好幾個注解需要一起使用,此時就可以使用組合驗證注解

@target({ field})
@notnull(message = "{user.name.null}")
@length(min = 5, max = 20, message = "{user.name.length.illegal}")
@pattern(regexp = "[a-za-z]{5,20}", message = "{user.name.length.illegal}")
@constraint(validatedby = { })
public @interface composition {
這樣我們驗證時隻需要:

@composition()
private string name;
簡潔多了。
即根據不同的語言選擇不同的錯誤消息顯示。
1、本地化解析器

<bean id="localeresolver" class="org.springframework.web.servlet.i18n.cookielocaleresolver">
<property name="cookiename" value="locale"/>
<property name="cookiemaxage" value="-1"/>
<property name="defaultlocale" value="zh_cn"/>
此處使用cookie存儲本地化資訊,當然也可以選擇其他的,如session存儲。
2、設定本地化資訊的攔截器

<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.localechangeinterceptor">
<property name="paramname" value="language"/>
</bean>
</mvc:interceptors>
即請求參數中通過language設定語言。
3、消息檔案
4、 浏覽器輸入
http://localhost:9080/spring4/changepassword?password=1&confirmation=2&language=en_us
轉自:http://jinnianshilongnian.iteye.com/blog/1990081