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