`
zzc1684
  • 浏览: 1188256 次
  • 性别: Icon_minigender_1
  • 来自: 广州
文章分类
社区版块
存档分类
最新评论

Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC

阅读更多

在之前的《跟我学SpringMVC》中的《第七章 注解式控制器的数据验证、类型转换及格式化》中已经介绍过SpringMVC集成Bean Validation 1.0(JSR-303),目前Bean Validation最新版本是Bean Validation 1.1(JSR-349),新特性可以到官网查看,笔者最喜欢的两个特性是:跨参数验证(比如密码和确认密码的验证)和支持在消息中使用EL表达式,其他的还有如方法参数/返回值验证、CDI和依赖注入、分组转换等。对于方法参数/返回值验证,大家可以参阅《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》。

 

 

 

Bean Validation 1.1当前实现是Hibernate validator 5,且spring4才支持。接下来我们从以下几个方法讲解Bean Validation 1.1,当然不一定是新特性:

 

  1.  集成Bean Validation 1.1到SpringMVC
  2.  分组验证、分组顺序及级联验证
  3.  消息中使用EL表达式
  4.  方法参数/返回值验证
  5.  自定义验证规则
  6.  类级别验证器
  7.  脚本验证器
  8.  cross-parameter,跨参数验证
  9. 混合类级别验证器和跨参数验证器
  10. 组合多个验证注解
  11. 本地化

 

因为大多数时候验证都配合web框架使用,而且很多朋友都咨询过如分组/跨参数验证,所以本文介绍下这些,且是和SpringMVC框架集成的例子,其他使用方式(比如集成到JPA中)可以参考其官方文档:

 

规范:http://beanvalidation.org/1.1/spec/

 

hibernate validator文档:http://hibernate.org/validator/ 

 

 

 

 1、集成Bean Validation 1.1到SpringMVC

 

1.1、项目搭建

 

首先添加hibernate validator 5依赖:

 

Java代码  收藏代码
  1. <dependency>  
  2.     <groupId>org.hibernate</groupId>  
  3.     <artifactId>hibernate-validator</artifactId>  
  4.     <version>5.0.2.Final</version>  
  5. </dependency>  

 

如果想在消息中使用EL表达式,请确保EL表达式版本是 2.2或以上,如使用Tomcat6,请到Tomcat7中拷贝相应的EL jar包到Tomcat6中。

 

Java代码  收藏代码
  1. <dependency>  
  2.     <groupId>javax.el</groupId>  
  3.     <artifactId>javax.el-api</artifactId>  
  4.     <version>2.2.4</version>  
  5.     <scope>provided</scope>  
  6. </dependency>  

 

请确保您使用的Web容器有相应版本的el jar包。

 

 

 

对于其他POM依赖请下载附件中的项目参考。

 

 

 

1.2、Spring MVC配置文件(spring-mvc.xml):

 

Java代码  收藏代码
  1. <!-- 指定自己定义的validator -->  
  2. <mvc:annotation-driven validator="validator"/>  
  3.   
  4. <!-- 以下 validator  ConversionService 在使用 mvc:annotation-driven 会 自动注册-->  
  5. <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">  
  6.     <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>  
  7.     <!-- 如果不加默认到 使用classpath下的 ValidationMessages.properties -->  
  8.     <property name="validationMessageSource" ref="messageSource"/>  
  9. </bean>  
  10.   
  11. <!-- 国际化的消息资源文件(本系统中主要用于显示/错误消息定制) -->  
  12. <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">  
  13.     <property name="basenames">  
  14.         <list>  
  15.             <!-- 在web环境中一定要定位到classpath 否则默认到当前web应用下找  -->  
  16.             <value>classpath:messages</value>  
  17.             <value>classpath:org/hibernate/validator/ValidationMessages</value>  
  18.         </list>  
  19.     </property>  
  20.     <property name="useCodeAsDefaultMessage" value="false"/>  
  21.     <property name="defaultEncoding" value="UTF-8"/>  
  22.     <property name="cacheSeconds" value="60"/>  
  23. </bean>  

 

此处主要把bean validation的消息查找委托给spring的messageSource。

 

 

 

1.3、实体验证注解:

 

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.     @NotNull(message = "{user.id.null}")  
  3.     private Long id;  
  4.   
  5.     @NotEmpty(message = "{user.name.null}")  
  6.     @Length(min = 5, max = 20, message = "{user.name.length.illegal}")  
  7.     @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}")  
  8.     private String name;  
  9.   
  10.     @NotNull(message = "{user.password.null}")  
  11.     private String password;  
  12. }  

 

对于验证规则可以参考官方文档,或者《第七章 注解式控制器的数据验证、类型转换及格式化》。

 

 

 

1.4、错误消息文件messages.properties:

 

Java代码  收藏代码
  1. user.id.null=用户编号不能为空  
  2. user.name.null=用户名不能为空  
  3. user.name.length.illegal=用户名长度必须在520之间  
  4. user.name.illegal=用户名必须是字母  
  5. user.password.null=密码不能为空  

 

 

 

1.5、控制器

 

Java代码  收藏代码
  1. @Controller  
  2. public class UserController {  
  3.   
  4.     @RequestMapping("/save")  
  5.     public String save(@Valid User user, BindingResult result) {  
  6.         if(result.hasErrors()) {  
  7.             return "error";  
  8.         }  
  9.         return "success";  
  10.     }  
  11. }  

 

 

 

1.6、错误页面:

 

Java代码  收藏代码
  1. <spring:hasBindErrors name="user">  
  2.     <c:if test="${errors.fieldErrorCount > 0}">  
  3.         字段错误:<br/>  
  4.         <c:forEach items="${errors.fieldErrors}" var="error">  
  5.             <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/>  
  6.             ${error.field}------${message}<br/>  
  7.         </c:forEach>  
  8.     </c:if>  
  9.   
  10.     <c:if test="${errors.globalErrorCount > 0}">  
  11.         全局错误:<br/>  
  12.         <c:forEach items="${errors.globalErrors}" var="error">  
  13.             <spring:message var="message" code="${error.code}" arguments="${error.arguments}" text="${error.defaultMessage}"/>  
  14.             <c:if test="${not empty message}">  
  15.                 ${message}<br/>  
  16.             </c:if>  
  17.         </c:forEach>  
  18.     </c:if>  
  19. </spring:hasBindErrors>  

 

 

 

大家以后可以根据这个做通用的错误消息显示规则。比如我前端页面使用validationEngine显示错误消息,那么我可以定义一个tag来通用化错误消息的显示:showFieldError.tag。  

 

 

 

1.7、测试

 

输入如:http://localhost:9080/spring4/save?name=123 , 我们得到如下错误:

 

Java代码  收藏代码
  1. name------用户名必须是字母  
  2. name------用户名长度必须在520之间  
  3. password------密码不能为空  
  4. id------用户编号不能为空  

 

 

 

基本的集成就完成了。

 

 

 

如上测试有几个小问题:

 

1、错误消息顺序,大家可以看到name的错误消息顺序不是按照书写顺序的,即不确定;

 

2、我想显示如:用户名【zhangsan】必须在5到20之间;其中我们想动态显示:用户名、min,max;而不是写死了;

 

3、我想在修改的时候只验证用户名,其他的不验证怎么办。

 

接下来我们挨着试试吧。

 

 

 

2、分组验证及分组顺序

 

如果我们想在新增的情况验证id和name,而修改的情况验证name和password,怎么办? 那么就需要分组了。

 

首先定义分组接口:

 

Java代码  收藏代码
  1. public interface First {  
  2. }  
  3.   
  4. public interface Second {  
  5. }  

 

分组接口就是两个普通的接口,用于标识,类似于java.io.Serializable。

 

 

 

接着我们使用分组接口标识实体:

 

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.   
  3.     @NotNull(message = "{user.id.null}", groups = {First.class})  
  4.     private Long id;  
  5.   
  6.     @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {Second.class})  
  7.     @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})  
  8.     private String name;  
  9.   
  10.     @NotNull(message = "{user.password.null}", groups = {First.class, Second.class})  
  11.     private String password;  
  12. }  

 

 

 

验证时使用如:

 

Java代码  收藏代码
  1. @RequestMapping("/save")  
  2. public String save(@Validated({Second.class}) User user, BindingResult result) {  
  3.     if(result.hasErrors()) {  
  4.         return "error";  
  5.     }  
  6.     return "success";  
  7. }  

 

即通过@Validate注解标识要验证的分组;如果要验证两个的话,可以这样@Validated({First.class, Second.class})。

 

 

 

接下来我们来看看通过分组来指定顺序;还记得之前的错误消息吗? user.name会显示两个错误消息,而且顺序不确定;如果我们先验证一个消息;如果不通过再验证另一个怎么办?可以通过@GroupSequence指定分组验证顺序:

 

 

 

Java代码  收藏代码
  1. @GroupSequence({First.class, Second.class, User.class})  
  2. public class User implements Serializable {  
  3.     private Long id;  
  4.   
  5.     @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})  
  6.     @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.illegal}", groups = {Second.class})  
  7.     private String name;  
  8.       
  9.     private String password;  
  10. }  

 

通过@GroupSequence指定验证顺序:先验证First分组,如果有错误立即返回而不会验证Second分组,接着如果First分组验 证通过了,那么才去验证Second分组,最后指定User.class表示那些没有分组的在最后。这样我们就可以实现按顺序验证分组了。

 

 

 

另一个比较常见的就是级联验证:

 

如:

 

Java代码  收藏代码
  1. public class User {  
  2.   
  3.     @Valid   
  4.     @ConvertGroup(from=First.class, to=Second.class)  
  5.     private Organization o;  
  6.   
  7. }  

 

 1、级联验证只要在相应的字段上加@Valid即可,会进行级联验证;@ConvertGroup的作用是当验证o的分组是First时,那么验证o的分组是Second,即分组验证的转换。

 

 

 

3、消息中使用EL表达式

 

假设我们需要显示如:用户名[NAME]长度必须在[MIN]到[MAX]之间,此处大家可以看到,我们不想把一些数据写死,如NAME、MIN、MAX;此时我们可以使用EL表达式。

 

 

 

如:

 

Java代码  收藏代码
  1. @Length(min = 5, max = 20, message = "{user.name.length.illegal}", groups = {First.class})  

 

错误消息:

 

Java代码  收藏代码
  1. user.name.length.illegal=用户名长度必须在{min}到{max}之间  

 

 

 

其中我们可以使用{验证注解的属性}得到这些值;如{min}得到@Length中的min值;其他的也是类似的。

 

 

 

到此,我们还是无法得到出错的那个输入值,如name=zhangsan。此时就需要EL表达式的支持,首先确定引入EL jar包且版本正确。然后使用如:

 

Java代码  收藏代码
  1. user.name.length.illegal=用户名[${validatedValue}]长度必须在520之间  

 

使用如EL表达式:${validatedValue}得到输入的值,如zhangsan。当然我们还可以使用如${min > 1 ? '大于1' : '小于等于1'},及在EL表达式中也能拿到如@Length的min等数据。

 

 

 

另外我们还可以拿到一个java.util.Formatter类型的formatter变量进行格式化:

 

Java代码  收藏代码
  1. ${formatter.format("%04d", min)}  

 

 

 

4、方法参数/返回值验证

 

这个可以参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》,概念是类似的,具体可以参考Bean Validation 文档。

 

 

 

5、自定义验证规则

 

有时候默认的规则可能还不够,有时候还需要自定义规则,比如屏蔽关键词验证是非常常见的一个功能,比如在发帖时帖子中不允许出现admin等关键词。

 

 

 

1、定义验证注解

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import javax.validation.Constraint;  
  4. import javax.validation.Payload;  
  5. import java.lang.annotation.Documented;  
  6. import java.lang.annotation.Retention;  
  7. import java.lang.annotation.Target;  
  8. import static java.lang.annotation.ElementType.*;  
  9. import static java.lang.annotation.RetentionPolicy.*;  
  10. /** 
  11.  * <p>User: Zhang Kaitao 
  12.  * <p>Date: 13-12-15 
  13.  * <p>Version: 1.0 
  14.  */  
  15.   
  16. @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })  
  17. @Retention(RUNTIME)  
  18. //指定验证器  
  19. @Constraint(validatedBy = ForbiddenValidator.class)  
  20. @Documented  
  21. public @interface Forbidden {  
  22.   
  23.     //默认错误消息  
  24.     String message() default "{forbidden.word}";  
  25.   
  26.     //分组  
  27.     Class<?>[] groups() default { };  
  28.   
  29.     //负载  
  30.     Class<? extends Payload>[] payload() default { };  
  31.   
  32.     //指定多个时使用  
  33.     @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })  
  34.     @Retention(RUNTIME)  
  35.     @Documented  
  36.     @interface List {  
  37.         Forbidden[] value();  
  38.     }  
  39. }  

 

 

 

2、 定义验证器

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;  
  4. import org.springframework.beans.factory.annotation.Autowired;  
  5. import org.springframework.context.ApplicationContext;  
  6. import org.springframework.util.StringUtils;  
  7.   
  8. import javax.validation.ConstraintValidator;  
  9. import javax.validation.ConstraintValidatorContext;  
  10. import java.io.Serializable;  
  11.   
  12. /** 
  13.  * <p>User: Zhang Kaitao 
  14.  * <p>Date: 13-12-15 
  15.  * <p>Version: 1.0 
  16.  */  
  17. public class ForbiddenValidator implements ConstraintValidator<Forbidden, String> {  
  18.   
  19.     private String[] forbiddenWords = {"admin"};  
  20.   
  21.     @Override  
  22.     public void initialize(Forbidden constraintAnnotation) {  
  23.         //初始化,得到注解数据  
  24.     }  
  25.   
  26.     @Override  
  27.     public boolean isValid(String value, ConstraintValidatorContext context) {  
  28.         if(StringUtils.isEmpty(value)) {  
  29.             return true;  
  30.         }  
  31.   
  32.         for(String word : forbiddenWords) {  
  33.             if(value.contains(word)) {  
  34.                 return false;//验证失败  
  35.             }  
  36.         }  
  37.         return true;  
  38.     }  
  39. }  

 

 验证器中可以使用spring的依赖注入,如注入:@Autowired  private ApplicationContext ctx; 

 

 

 

3、使用

 

Java代码  收藏代码
  1. public class User implements Serializable {  
  2.     @Forbidden()  
  3.     private String name;  
  4. }  

 

 

 

4、当我们在提交name中含有admin的时候会输出错误消息:

 

Java代码  收藏代码
  1. forbidden.word=您输入的数据中有非法关键词  

 

 

 

问题来了,哪个词是非法的呢?bean validation 和 hibernate validator都没有提供相应的api提供这个数据,怎么办呢?通过跟踪代码,发现一种不是特别好的方法:我们可以覆盖 org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl 实现(即复制一份代码放到我们的src中),然后覆盖buildAnnotationParameterMap方法;

 

Java代码  收藏代码
  1. private Map<String, Object> buildAnnotationParameterMap(Annotation annotation) {  
  2.     ……  
  3.     //将Collections.unmodifiableMap( parameters );替换为如下语句  
  4.     return parameters;  
  5. }  

 

 即允许这个数据可以修改;然后在ForbiddenValidator中:

 

Java代码  收藏代码
  1. for(String word : forbiddenWords) {  
  2.     if(value.contains(word)) {  
  3.         ((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);  
  4.         return false;//验证失败  
  5.     }  
  6. }  

 

通过 ((ConstraintValidatorContextImpl)context).getConstraintDescriptor().getAttributes().put("word", word);添加自己的属性;放到attributes中的数据可以通过${} 获取。然后消息就可以变成:

 

Java代码  收藏代码
  1. forbidden.word=您输入的数据中有非法关键词【{word}】  

 

这种方式不是很友好,但是可以解决我们的问题。

 

 

 

典型的如密码、确认密码的场景,非常常用;如果没有这个功能我们需要自己写代码来完成;而且经常重复自己。接下来看看bean validation 1.1如何实现的。

 

 

 

6、类级别验证器

 

6.1、定义验证注解

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import javax.validation.Constraint;  
  4. import javax.validation.Payload;  
  5. import javax.validation.constraints.NotNull;  
  6. import java.lang.annotation.Documented;  
  7. import java.lang.annotation.Retention;  
  8. import java.lang.annotation.Target;  
  9. import static java.lang.annotation.ElementType.*;  
  10. import static java.lang.annotation.RetentionPolicy.*;  
  11. /** 
  12.  * <p>User: Zhang Kaitao 
  13.  * <p>Date: 13-12-15 
  14.  * <p>Version: 1.0 
  15.  */  
  16.   
  17. @Target({ TYPE, ANNOTATION_TYPE})  
  18. @Retention(RUNTIME)  
  19. //指定验证器  
  20. @Constraint(validatedBy = CheckPasswordValidator.class)  
  21. @Documented  
  22. public @interface CheckPassword {  
  23.   
  24.     //默认错误消息  
  25.     String message() default "";  
  26.   
  27.     //分组  
  28.     Class<?>[] groups() default { };  
  29.   
  30.     //负载  
  31.     Class<? extends Payload>[] payload() default { };  
  32.   
  33.     //指定多个时使用  
  34.     @Target({ FIELD, METHOD, PARAMETER, ANNOTATION_TYPE })  
  35.     @Retention(RUNTIME)  
  36.     @Documented  
  37.     @interface List {  
  38.         CheckPassword[] value();  
  39.     }  
  40. }  

 

6.2、 定义验证器

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import com.sishuok.spring4.entity.User;  
  4. import org.springframework.util.StringUtils;  
  5.   
  6. import javax.validation.ConstraintValidator;  
  7. import javax.validation.ConstraintValidatorContext;  
  8.   
  9. /** 
  10.  * <p>User: Zhang Kaitao 
  11.  * <p>Date: 13-12-15 
  12.  * <p>Version: 1.0 
  13.  */  
  14. public class CheckPasswordValidator implements ConstraintValidator<CheckPassword, User> {  
  15.   
  16.     @Override  
  17.     public void initialize(CheckPassword constraintAnnotation) {  
  18.     }  
  19.   
  20.     @Override  
  21.     public boolean isValid(User user, ConstraintValidatorContext context) {  
  22.         if(user == null) {  
  23.             return true;  
  24.         }  
  25.   
  26.         //没有填密码  
  27.         if(!StringUtils.hasText(user.getPassword())) {  
  28.             context.disableDefaultConstraintViolation();  
  29.             context.buildConstraintViolationWithTemplate("{password.null}")  
  30.                     .addPropertyNode("password")  
  31.                     .addConstraintViolation();  
  32.             return false;  
  33.         }  
  34.   
  35.         if(!StringUtils.hasText(user.getConfirmation())) {  
  36.             context.disableDefaultConstraintViolation();  
  37.             context.buildConstraintViolationWithTemplate("{password.confirmation.null}")  
  38.                     .addPropertyNode("confirmation")  
  39.                     .addConstraintViolation();  
  40.             return false;  
  41.         }  
  42.   
  43.         //两次密码不一样  
  44.         if (!user.getPassword().trim().equals(user.getConfirmation().trim())) {  
  45.             context.disableDefaultConstraintViolation();  
  46.             context.buildConstraintViolationWithTemplate("{password.confirmation.error}")  
  47.                     .addPropertyNode("confirmation")  
  48.                     .addConstraintViolation();  
  49.             return false;  
  50.         }  
  51.         return true;  
  52.     }  
  53. }  

 

其中我们通过disableDefaultConstraintViolation禁用默认的约束;然后通过 buildConstraintViolationWithTemplate(消息模板)/addPropertyNode(所属属性) /addConstraintViolation定义我们自己的约束。

 

 

 

6.3、使用

 

Java代码  收藏代码
  1. @CheckPassword()  
  2. public class User implements Serializable {  
  3. }  

 

 放到类头上即可。

 

 

 

7、通过脚本验证

 

Java代码  收藏代码
  1. @ScriptAssert(script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")  
  2. public class User implements Serializable {  
  3. }  

 

通过脚本验证是非常简单而且强大的,lang指定脚本语言(请参考javax.script.ScriptEngineManager JSR-223),alias是在脚本验证中User对象的名字,但是大家会发现一个问题:错误消息怎么显示呢? 在springmvc 中会添加到全局错误消息中,这肯定不是我们想要的,我们改造下吧。

 

 

 

7.1、定义验证注解

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator;  
  4.   
  5. import java.lang.annotation.Documented;  
  6. import java.lang.annotation.Retention;  
  7. import java.lang.annotation.Target;  
  8. import javax.validation.Constraint;  
  9. import javax.validation.Payload;  
  10.   
  11. import static java.lang.annotation.ElementType.TYPE;  
  12. import static java.lang.annotation.RetentionPolicy.RUNTIME;  
  13.   
  14. @Target({ TYPE })  
  15. @Retention(RUNTIME)  
  16. @Constraint(validatedBy = {PropertyScriptAssertValidator.class})  
  17. @Documented  
  18. public @interface PropertyScriptAssert {  
  19.   
  20.     String message() default "{org.hibernate.validator.constraints.ScriptAssert.message}";  
  21.   
  22.     Class<?>[] groups() default { };  
  23.   
  24.     Class<? extends Payload>[] payload() default { };  
  25.   
  26.     String lang();  
  27.   
  28.     String script();  
  29.   
  30.     String alias() default "_this";  
  31.   
  32.     String property();  
  33.   
  34.     @Target({ TYPE })  
  35.     @Retention(RUNTIME)  
  36.     @Documented  
  37.     public @interface List {  
  38.         PropertyScriptAssert[] value();  
  39.     }  
  40. }  

 

和ScriptAssert没什么区别,只是多了个property用来指定出错后给实体的哪个属性。

 

 

 

7.2、验证器

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. import javax.script.ScriptException;  
  4. import javax.validation.ConstraintDeclarationException;  
  5. import javax.validation.ConstraintValidator;  
  6. import javax.validation.ConstraintValidatorContext;  
  7.   
  8. import com.sishuok.spring4.validator.PropertyScriptAssert;  
  9. import org.hibernate.validator.constraints.ScriptAssert;  
  10. import org.hibernate.validator.internal.util.Contracts;  
  11. import org.hibernate.validator.internal.util.logging.Log;  
  12. import org.hibernate.validator.internal.util.logging.LoggerFactory;  
  13. import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluator;  
  14. import org.hibernate.validator.internal.util.scriptengine.ScriptEvaluatorFactory;  
  15.   
  16. import static org.hibernate.validator.internal.util.logging.Messages.MESSAGES;  
  17.   
  18. public class PropertyScriptAssertValidator implements ConstraintValidator<PropertyScriptAssert, Object> {  
  19.   
  20.     private static final Log log = LoggerFactory.make();  
  21.   
  22.     private String script;  
  23.     private String languageName;  
  24.     private String alias;  
  25.     private String property;  
  26.     private String message;  
  27.   
  28.     public void initialize(PropertyScriptAssert constraintAnnotation) {  
  29.         validateParameters( constraintAnnotation );  
  30.   
  31.         this.script = constraintAnnotation.script();  
  32.         this.languageName = constraintAnnotation.lang();  
  33.         this.alias = constraintAnnotation.alias();  
  34.         this.property = constraintAnnotation.property();  
  35.         this.message = constraintAnnotation.message();  
  36.     }  
  37.   
  38.     public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {  
  39.   
  40.         Object evaluationResult;  
  41.         ScriptEvaluator scriptEvaluator;  
  42.   
  43.         try {  
  44.             ScriptEvaluatorFactory evaluatorFactory = ScriptEvaluatorFactory.getInstance();  
  45.             scriptEvaluator = evaluatorFactory.getScriptEvaluatorByLanguageName( languageName );  
  46.         }  
  47.         catch ( ScriptException e ) {  
  48.             throw new ConstraintDeclarationException( e );  
  49.         }  
  50.   
  51.         try {  
  52.             evaluationResult = scriptEvaluator.evaluate( script, value, alias );  
  53.         }  
  54.         catch ( ScriptException e ) {  
  55.             throw log.getErrorDuringScriptExecutionException( script, e );  
  56.         }  
  57.   
  58.         if ( evaluationResult == null ) {  
  59.             throw log.getScriptMustReturnTrueOrFalseException( script );  
  60.         }  
  61.         if ( !( evaluationResult instanceof Boolean ) ) {  
  62.             throw log.getScriptMustReturnTrueOrFalseException(  
  63.                     script,  
  64.                     evaluationResult,  
  65.                     evaluationResult.getClass().getCanonicalName()  
  66.             );  
  67.         }  
  68.   
  69.         if(Boolean.FALSE.equals(evaluationResult)) {  
  70.             constraintValidatorContext.disableDefaultConstraintViolation();  
  71.             constraintValidatorContext  
  72.                     .buildConstraintViolationWithTemplate(message)  
  73.                     .addPropertyNode(property)  
  74.                     .addConstraintViolation();  
  75.         }  
  76.   
  77.         return Boolean.TRUE.equals( evaluationResult );  
  78.     }  
  79.   
  80.     private void validateParameters(PropertyScriptAssert constraintAnnotation) {  
  81.         Contracts.assertNotEmpty( constraintAnnotation.script(), MESSAGES.parameterMustNotBeEmpty( "script" ) );  
  82.         Contracts.assertNotEmpty( constraintAnnotation.lang(), MESSAGES.parameterMustNotBeEmpty( "lang" ) );  
  83.         Contracts.assertNotEmpty( constraintAnnotation.alias(), MESSAGES.parameterMustNotBeEmpty( "alias" ) );  
  84.         Contracts.assertNotEmpty( constraintAnnotation.property(), MESSAGES.parameterMustNotBeEmpty( "property" ) );  
  85.         Contracts.assertNotEmpty( constraintAnnotation.message(), MESSAGES.parameterMustNotBeEmpty( "message" ) );  
  86.     }  
  87. }  

 

和之前的类级别验证器类似,就不多解释了,其他代码全部拷贝自org.hibernate.validator.internal.constraintvalidators.ScriptAssertValidator。

 

 

 

7.3、使用

 

Java代码  收藏代码
  1. @PropertyScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")  

 

和之前的区别就是多了个property,用来指定出错时给哪个字段。 这个相对之前的类级别验证器更通用一点。

 

 

 

8、cross-parameter,跨参数验证

 

直接看示例;

 

 

 

8.1、首先注册MethodValidationPostProcessor,起作用请参考《Spring3.1 对Bean Validation规范的新支持(方法级别验证) 》 

 

Java代码  收藏代码
  1. <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">  
  2.     <property name="validator" ref="validator"/>  
  3. </bean>  

 

 

 

8.2、Service 

 

Java代码  收藏代码
  1. @Validated  
  2. @Service  
  3. public class UserService {  
  4.   
  5.     @CrossParameter  
  6.     public void changePassword(String password, String confirmation) {  
  7.   
  8.     }  
  9. }  

 

通过@Validated注解UserService表示该类中有需要进行方法参数/返回值验证;   @CrossParameter注解方法表示要进行跨参数验证;即验证password和confirmation是否相等。

 

 

 

8.3、验证注解 

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. //省略import  
  4.   
  5. @Constraint(validatedBy = CrossParameterValidator.class)  
  6. @Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })  
  7. @Retention(RUNTIME)  
  8. @Documented  
  9. public @interface CrossParameter {  
  10.   
  11.     String message() default "{password.confirmation.error}";  
  12.     Class<?>[] groups() default { };  
  13.     Class<? extends Payload>[] payload() default { };  
  14.   
  15. }  

 

 

 

8.4、验证器 

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. //省略import  
  4.   
  5. @SupportedValidationTarget(ValidationTarget.PARAMETERS)  
  6. public class CrossParameterValidator implements ConstraintValidator<CrossParameter, Object[]> {  
  7.   
  8.     @Override  
  9.     public void initialize(CrossParameter constraintAnnotation) {  
  10.     }  
  11.   
  12.     @Override  
  13.     public boolean isValid(Object[] value, ConstraintValidatorContext context) {  
  14.         if(value == null || value.length != 2) {  
  15.             throw new IllegalArgumentException("must have two args");  
  16.         }  
  17.         if(value[0] == null || value[1] == null) {  
  18.             return true;  
  19.         }  
  20.         if(value[0].equals(value[1])) {  
  21.             return true;  
  22.         }  
  23.         return false;  
  24.     }  
  25. }  

 

其中@SupportedValidationTarget(ValidationTarget.PARAMETERS)表示验证参数; value将是参数列表。 

 

 

 

8.5、使用

 

Java代码  收藏代码
  1. @RequestMapping("/changePassword")  
  2. public String changePassword(  
  3.         @RequestParam("password") String password,  
  4.         @RequestParam("confirmation") String confirmation, Model model) {  
  5.     try {  
  6.         userService.changePassword(password, confirmation);  
  7.     } catch (ConstraintViolationException e) {  
  8.         for(ConstraintViolation violation : e.getConstraintViolations()) {  
  9.             System.out.println(violation.getMessage());  
  10.         }  
  11.     }  
  12.     return "success";  
  13. }  

 

调用userService.changePassword方法,如果验证失败将抛出ConstraintViolationException异常,然后得到ConstraintViolation,调用getMessage即可得到错误消息;然后到前台显示即可。

 

 

 

从以上来看,不如之前的使用方便,需要自己对错误消息进行处理。 下一节我们也写个脚本方式的跨参数验证器。

 

 

 

9、混合类级别验证器和跨参数验证器

 

9.1、验证注解

 

Java代码  收藏代码
  1. package com.sishuok.spring4.validator;  
  2.   
  3. //省略import  
  4.   
  5. @Constraint(validatedBy = {  
  6.         CrossParameterScriptAssertClassValidator.class,  
  7.         CrossParameterScriptAssertParameterValidator.class  
  8. })  
  9. @Target({ TYPE, FIELD, PARAMETER, METHOD, CONSTRUCTOR, ANNOTATION_TYPE })  
  10. @Retention(RUNTIME)  
  11. @Documented  
  12. public @interface CrossParameterScriptAssert {  
  13.     String message() default "error";  
  14.     Class<?>[] groups() default { };  
  15.     Class<? extends Payload>[] payload() default { };  
  16.     String script();  
  17.     String lang();  
  18.     String alias() default "_this";  
  19.     String property() default "";  
  20.     ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;  
  21. }   

 

 

 

此处我们通过@Constraint指定了两个验证器,一个类级别的,一个跨参数的。validationAppliesTo指定为ConstraintTarget.IMPLICIT,表示隐式自动判断。

 

 

 

9.2、验证器

 

请下载源码查看

 

 

 

9.3、使用

 

9.3.1、类级别使用

 

Java代码  收藏代码
  1. @CrossParameterScriptAssert(property = "confirmation", script = "_this.password==_this.confirmation", lang = "javascript", alias = "_this", message = "{password.confirmation.error}")  

 

指定property即可,其他和之前的一样。

 

9.3.2、跨参数验证

 

Java代码  收藏代码
  1. @CrossParameterScriptAssert(script = "args[0] == args[1]", lang = "javascript", alias = "args", message = "{password.confirmation.error}")  
  2. public void changePassword(String password, String confirmation) {  
  3.   
  4. }  

 

通过args[0]==args[1] 来判断是否相等。

 

 

 

这样,我们的验证注解就自动适应两种验证规则了。  

 

 

 

10、组合验证注解 

 

有时候,可能有好几个注解需要一起使用,此时就可以使用组合验证注解

 

Java代码  收藏代码
  1. @Target({ FIELD})  
  2. @Retention(RUNTIME)  
  3. @Documented  
  4. @NotNull(message = "{user.name.null}")  
  5. @Length(min = 5, max = 20, message = "{user.name.length.illegal}")  
  6. @Pattern(regexp = "[a-zA-Z]{5,20}", message = "{user.name.length.illegal}")  
  7. @Constraint(validatedBy = { })  
  8. public @interface Composition {  
  9.     String message() default "";  
  10.     Class<?>[] groups() default { };  
  11.     Class<? extends Payload>[] payload() default { };  
  12. }  

 

这样我们验证时只需要:

 

Java代码  收藏代码
  1. @Composition()  
  2. private String name;  

 

简洁多了。 

 

 

 

11、本地化 

 

即根据不同的语言选择不同的错误消息显示。

 

1、本地化解析器

 

Java代码  收藏代码
  1. <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">  
  2.     <property name="cookieName" value="locale"/>  
  3.     <property name="cookieMaxAge" value="-1"/>  
  4.     <property name="defaultLocale" value="zh_CN"/>  
  5. </bean>  

 

此处使用cookie存储本地化信息,当然也可以选择其他的,如Session存储。

 

 

 

2、设置本地化信息的拦截器

 

Java代码  收藏代码
  1. <mvc:interceptors>  
  2.     <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">  
  3.         <property name="paramName" value="language"/>  
  4.     </bean>  
  5. </mvc:interceptors>  

 

即请求参数中通过language设置语言。

 

 

 

3、消息文件

 

 

 

 

4、 浏览器输入

 

http://localhost:9080/spring4/changePassword?password=1&confirmation=2&language=en_US

 

 

 

 

 

 

 

到此,我们已经完成大部分Bean Validation的功能实验了。对于如XML配置、编程式验证API的使用等对于我们使用SpringMVC这种web环境用处不大,所以就不多介绍了,有兴趣可以自己下载官方文档学习。

 

 

 

 

 

 

 
评论
25 楼 yiyuyuyuyu 2014-09-16   引用
博主,hibernate-validator-5.1.2.Final和validation-api-1.1.0.Final这两个jar包从官网上下载下来,加入项目,启动web项目之后就报错(配置文件还没配置),这个怎么破
24 楼 njbble 2014-07-24   引用
mingming_java 写道
mingming_java 写道
博 主,你好,由于我的项目是spring4+hibernate4的,引入的是hibernate-validator-5.1.1.Final.jar的 jar包,可是在网上又找不到validation-api  的1.1版本的jar包,这个在那下载呢?我是手动搭建的框架

急需这个包,麻烦博主看到后能发往邮箱305281722@qq.com,感激不尽!

Xml代码  收藏代码
  1. <dependency>  
  2.     <groupId>org.hibernate</groupId>  
  3.     <artifactId>hibernate-validator</artifactId>  
  4.     <version>5.1.1.Final</version>  
  5. </dependency>  
23 楼 mingming_java 2014-07-23   引用
mingming_java 写道
博 主,你好,由于我的项目是spring4+hibernate4的,引入的是hibernate-validator-5.1.1.Final.jar的 jar包,可是在网上又找不到validation-api  的1.1版本的jar包,这个在那下载呢?我是手动搭建的框架

急需这个包,麻烦博主看到后能发往邮箱305281722@qq.com,感激不尽!
22 楼 mingming_java 2014-07-22   引用
博主,你好,由于我的项目是spring4+hibernate4的,引入的是 hibernate-validator-5.1.1.Final.jar的jar包,可是在网上又找不到validation-api  的1.1版本的jar包,这个在那下载呢?我是手动搭建的框架
21 楼 mingming_java 2014-07-22   引用
博主,你好,由于我的项目是spring4+hibernate4的,引入的是hibernate-validator-5.1.1.Final.jar的jar包,可是在网上又找不到val的1.1版本的jar包,这个在那下载呢?我是手动搭建的框架
20 楼 cyberus 2014-05-19   引用
cyberus 写道
最 近在做一个项目,请求有2个参数比如orderData(订单参数组成的字符串)和signedData(orderData签名后的数 据),orderData的字符串实际上是一个json字符串,我希望能够将orderData先验签,再转成对象以后,再valid其中的数据,为了实 现这个功能,我自己加了一个注解(@VerifyAndToJson),并写写了一个类去实现 HandlerMethodArgumentResolver来处理这个它。

现在的使用他的方式是这样的
Java代码  收藏代码
  1. public String doSomeThing(  
  2.             @Valid  @VerifyAndToJson(originalMsgKey = "orderData", signedDataKey = "signedData", type = Xxx.class)  Xxx xxx,  
  3.             BindingResult br){...}  


但是请求过来以后,会报一个异常,说BindingResult必须跟在model attribute或者@RequestBody或@ResponseBody之后。于是我加上了@ModelAtrribute注解,改成了如下形式
Java代码  收藏代码
  1. public String doSomeThing(  
  2.             @Valid @ModelAttribute @VerifyAndToJson(originalMsgKey = "orderData", signedDataKey = "signedData", type = Xxx.class)  Xxx xxx,  
  3.             BindingResult br){...}  


但是这样一来,@VerifyAndToJson好像就失效了,程序不会进入我实现HandlerMethodArgumentResolver的类中去。

请问有什么办法,能够实现我的需求吗?


我自己解决了:),将实现HandlerMethodArgumentResolver的类参考了RequestResponseBodyMethodProcessor的写法,就解决了~
19 楼 cyberus 2014-05-18   引用
最近在做一个项目,请求有2个参数比如orderData(订单参数组成的字符串)和 signedData(orderData签名后的数据),orderData的字符串实际上是一个json字符串,我希望能够将orderData先验 签,再转成对象以后,再valid其中的数据,为了实现这个功能,我自己加了一个注解(@VerifyAndToJson),并写写了一个类去实现 HandlerMethodArgumentResolver来处理这个它。

现在的使用他的方式是这样的
Java代码  收藏代码
  1. public String doSomeThing(  
  2.             @Valid  @VerifyAndToJson(originalMsgKey = "orderData", signedDataKey = "signedData", type = Xxx.class)  Xxx xxx,  
  3.             BindingResult br){...}  


但是请求过来以后,会报一个异常,说BindingResult必须跟在model attribute或者@RequestBody或@ResponseBody之后。于是我加上了@ModelAtrribute注解,改成了如下形式
Java代码  收藏代码
  1. public String doSomeThing(  
  2.             @Valid @ModelAttribute @VerifyAndToJson(originalMsgKey = "orderData", signedDataKey = "signedData", type = Xxx.class)  Xxx xxx,  
  3.             BindingResult br){...}  


但是这样一来,@VerifyAndToJson好像就失效了,程序不会进入我实现HandlerMethodArgumentResolver的类中去。

请问有什么办法,能够实现我的需求吗?
18 楼 wenxiang_tune 2014-05-12   引用
请问:
<mvc:annotation-driven validator="validator"/> 
怎么用java类来配置代替呢?
17 楼 jsdit 2014-04-08   引用
自己写的Spring MVC4的demo,在tomcat7下执行,消息一直不能解析EL表达式。今天debug代码后才发现,问题的原因是配置项中设置 useCodeAsDefaultMessage为true,当找不到validatedValue的时候,默认使用了validatedValue作为 Message.此属性默认为false.

validator会根据bundle.getString(key)去查找资源文件中是否会存在validatedValue,如果不存在,则抛出MissingResourceException异常,并对异常进行处理,设置code不变。

所以还有一个地方需要注意的就是,在资源文件中不能出现隐藏的保留key:validatedValue、min、max等等。
16 楼 hellostory 2013-12-20   引用
jinnianshilongnian 写道
hellostory 写道
mayuanfei 写道
这个校验为什么不用Jquery的Validater呢?为什么要到后台校验?

如果你用过firebug,或chrome的调试工具,你就不会有这种疑问!



你的问题搞定了没?


还好,就是自己提取message大括号里面的“{XXX}”变量,然后再读取文件进行匹配。
15 楼 jinnianshilongnian 2013-12-18   引用
hellostory 写道
mayuanfei 写道
这个校验为什么不用Jquery的Validater呢?为什么要到后台校验?

如果你用过firebug,或chrome的调试工具,你就不会有这种疑问!



你的问题搞定了没?
14 楼 hellostory 2013-12-18   引用
mayuanfei 写道
这个校验为什么不用Jquery的Validater呢?为什么要到后台校验?

如果你用过firebug,或chrome的调试工具,你就不会有这种疑问!


13 楼 mayuanfei 2013-12-18   引用
这个校验为什么不用Jquery的Validater呢?为什么要到后台校验?
12 楼 jinnianshilongnian 2013-12-17   引用
lengyun3566 写道
除了具体实现细节的更新,我觉得spring 4在架构思想的变化也值得关注,重新关注轻量级,关注微服务,关注快速开发,spring boot挺有意思的。

嗯,spring boot 你使用过吗? 还没去试呢。从其helloworld来看,如果写很小的服务的话,非常方便
11 楼 lengyun3566 2013-12-17   引用
除了具体实现细节的更新,我觉得spring 4在架构思想的变化也值得关注,重新关注轻量级,关注微服务,关注快速开发,spring boot挺有意思的。
10 楼 jinnianshilongnian 2013-12-16   引用
need_faith 写道
博主很勤奋,这两天一直在更新博客,厉害,支持,赞一个

哈哈,正好是自己需要的,然后再分享给其他需要的朋友
9 楼 need_faith 2013-12-16   引用
博主很勤奋,这两天一直在更新博客,厉害,支持,赞一个
8 楼 jinnianshilongnian 2013-12-16   引用
hellostory 写道
jinnianshilongnian 写道
hellostory 写道
你好!请问Ajax提交表单后,验证消息如何国际化,能否给个DEMO,比较现在Ajax使用越来越频繁!

配置上LocaleResolver即可
http://www.mkyong.com/spring-mvc/spring-mvc-internationalization-example/
http://fhd001.iteye.com/blog/1131378


如 果返回的是JSP文件,且采用Spring MVC相关标签的话,可以很容易的将验证信息中的message="{user.name.required}"进行消息文件映射,但如果需要返回 Ajax消息的话,则需要手动自己读取消息配置文件,然后再将“{user.name.required}”进行解析,比较麻烦,能否写个DEMO?


你参考下这段代码
Java代码  收藏代码
  1. @RequestMapping("/ajax")  
  2. @ResponseBody  
  3. public Object ajaxError(@Valid User user, BindingResult result) {  
  4.     if(result.hasErrors()) {  
  5.         return AjaxError.from(result, null);  
  6.     }  
  7.     return "success";  
  8. }  


代码我上传到附近了,你下载看下

具体如何命名错误消息,你自己控制
7 楼 jinnianshilongnian 2013-12-16   引用
hellostory 写道
你好!请问Ajax提交表单后,验证消息如何国际化,能否给个DEMO,比较现在Ajax使用越来越频繁!

本地化的已经加到例子中了;一会我写个ajax的错误返回
6 楼 jinnianshilongnian 2013-12-16   引用
hellostory 写道
jinnianshilongnian 写道
hellostory 写道
你好!请问Ajax提交表单后,验证消息如何国际化,能否给个DEMO,比较现在Ajax使用越来越频繁!

配置上LocaleResolver即可
http://www.mkyong.com/spring-mvc/spring-mvc-internationalization-example/
http://fhd001.iteye.com/blog/1131378


如 果返回的是JSP文件,且采用Spring MVC相关标签的话,可以很容易的将验证信息中的message="{user.name.required}"进行消息文件映射,但如果需要返回 Ajax消息的话,则需要手动自己读取消息配置文件,然后再将“{user.name.required}”进行解析,比较麻烦,能否写个DEMO?

对于ajax的话,错误消息应该是固定的,如
{
   error:"内容"
}

对于这种方式我一会写个例子给你吧;

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics