表单的数据检验对一个程序来讲非常重要,因为对于客户端的数据不能完全信任,常规的检验类型有:

  • 参数为空,根据不同的业务规定要求表单项是必填项
  • 参数值的有效性,比如产品的价格,一定不能是负数
  • 多个表单项组合检验,比如在注册时密码与确认密码必须相同
  • 参数值的数据范围,常见的是一些状态值,或者叫枚举值,如果传递的参数超出已经定义的枚举那么也是无意义的

上面的这些检验基本上都是纯数据方面的,还不算具体的业务数据检验,下面是一些强业务相关的数据检验

  • 根据产品ID,去检验ID是否真实存在
  • 注册用户时,需要检验用户名的唯一性
  • ....

根据上面的需求,如果我们将这些检验的逻辑全部与业务逻辑耦合在一起,那么我们的程序逻辑将会变得冗长而且不便于代码复用,下面的代码就是耦合性强的一种体现:

           if (isvUserRequestDTO == null) {
log.error("can not find isv request by request id, " + isvRequestId);
return return_value_error(ErrorDef.FailFindIsv);
}
if (isvUserRequestDTO.getAuditStatus() != 1) {
log.error("isv request is not audited, " + isvRequestId);
return return_value_error(ErrorDef.IsvRequestNotAudited);
}

我们可以利用spring提供的validator来解耦表单数据的检验逻辑,可以将上述的代码从具体的业务代码的抽离出去。


Hibernate validator,它是JSR-303的一种具体实现。它是基于注解形式的,我们看一下它原生支持的一些注解。

注解 说明
@Null 只能为空,这个用途场景比较少
@NotNull 不能为空,常用注解
@AssertFalse 必须为false,类似于常量
@AssertTrue 必须为true,类似于常量
@DecimalMax(value)  
@DecimalMin(value)  
@Digits(integer,fraction)  
@Future 代表是一个将来的时间
@Max(value) 最大值,用于一个枚举值的数据范围控制
@Min(value) 最小值,用于一个枚举值的数据范围控制
@Past 代表是一个过期的时间
@Pattern(value) 正则表达式,比如验证手机号,邮箱等,非常常用
@Size(max,min)

限制字符长度必须在min到max之间

基础数据类型的使用示例

@NotNull(message = "基础数量不能为空")
@Min(value = 0,message = "基础数量不合法")
private Integer baseQty;

嵌套检验,如果一个对象中包含子对象(非基础数据类型)需要在属性上增加@Valid注解。

   @Valid
@NotNull(message = "价格策略内容不能为空")
private List<ProductPricePolicyItem> policyItems;

除了原生提供的注解外,我们还可以自定义一些限制性的检验类型,比如上面提到的多个属性之间的联合检验。该注解需要使用@Constraint标注,这里我编写了一个用于针对两个属性之间的数据检验的规则,它支持两个属性之间的如下操作符,而且可以设置多组属性对。

  • ==
  • >
  • >=
  • <
  • <=

创建注解

  • 通过@Constraint指定检验的实现类CrossFieldMatchValidator
  • 增加两个属性名称字段,用于后续的检验
  • 增加一个注解的List,用来支持一个对象中检验多组属性对。比如即需要检验最大数量与最小数量,也需要检验密码与确认密码
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = CrossFieldMatchValidator.class)
@Documented
public @interface CrossFieldMatch { String message() default "{constraints.crossfieldmatch}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; /**
* @return The first field
*/
String first(); /**
* @return The second field
*/
String second(); /**
* first operator second
* @return
*/
CrossFieldOperator operator(); /**
* Defines several <code>@FieldMatch</code> annotations on the same element
*
* @see CrossFieldMatch
*/
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
CrossFieldMatch[] value();
}
}

检验实现类

isValid方法,通过反射可以取到需要检验的两个字段的值以及数据类型,然后根据指定的数据操作符以及数据类型做出计算。目前这个检验只针对我的业务并不十分通用,需要根据自己的项目情况来灵活处理。

public class CrossFieldMatchValidator implements ConstraintValidator<CrossFieldMatch, Object> {

    private String firstFieldName;
private String secondFieldName;
private CrossFieldOperator operator; @Override
public void initialize(final CrossFieldMatch constraintAnnotation) {
firstFieldName = constraintAnnotation.first();
secondFieldName = constraintAnnotation.second();
operator=constraintAnnotation.operator();
} @Override
public boolean isValid(final Object value, final ConstraintValidatorContext context) {
try {
Class valueClass=value.getClass();
final Field firstField = valueClass.getDeclaredField(firstFieldName);
final Field secondField = valueClass.getDeclaredField(secondFieldName);
//不支持为null的字段
if(null==firstField||null==secondField){
return false;
} firstField.setAccessible(true);
secondField.setAccessible(true);
Object firstFieldValue= firstField.get(value);
Object secondFieldValue= secondField.get(value); //不支持类型不同的字段
if(!firstFieldValue.getClass().equals(secondFieldValue.getClass())){
return false;
} //整数支持 long int short
//浮点数支持 double
if(operator==CrossFieldOperator.EQ) {
return firstFieldValue.equals(secondFieldValue);
}
else if(operator==CrossFieldOperator.GT){
if(firstFieldValue.getClass().equals(Long.class)||firstFieldValue.getClass().equals(Integer.class)||firstFieldValue.getClass().equals(Short.class)) {
return (Long)firstFieldValue > (Long) secondFieldValue;
}
else if(firstFieldValue.getClass().equals(Double.class)) {
return (Double)firstFieldValue > (Double) secondFieldValue;
} }
else if(operator==CrossFieldOperator.GE){
if(firstFieldValue.getClass().equals(Long.class)||firstFieldValue.getClass().equals(Integer.class)||firstFieldValue.getClass().equals(Short.class)) {
return Long.valueOf(firstFieldValue.toString()) >= Long.valueOf(secondFieldValue.toString());
}
else if(firstFieldValue.getClass().equals(Double.class)) {
return Double.valueOf(firstFieldValue.toString()) >= Double.valueOf(secondFieldValue.toString());
}
}
else if(operator==CrossFieldOperator.LT){
if(firstFieldValue.getClass().equals(Long.class)||firstFieldValue.getClass().equals(Integer.class)||firstFieldValue.getClass().equals(Short.class)) {
return (Long)firstFieldValue < (Long) secondFieldValue;
}
else if(firstFieldValue.getClass().equals(Double.class)) {
return (Double)firstFieldValue < (Double) secondFieldValue;
}
}
else if(operator==CrossFieldOperator.LE){
if(firstFieldValue.getClass().equals(Long.class)||firstFieldValue.getClass().equals(Integer.class)||firstFieldValue.getClass().equals(Short.class)) {
return Long.valueOf(firstFieldValue.toString()) <= Long.valueOf(secondFieldValue.toString());
}
else if(firstFieldValue.getClass().equals(Double.class)) {
return Double.valueOf(firstFieldValue.toString()) <= Double.valueOf(secondFieldValue.toString());
}
}
}
catch (final Exception ignore) {
// ignore
}
return false;
}
}

调用示例:

@CrossFieldMatch.List({
@CrossFieldMatch(first = "minQty", second = "maxQty",operator = CrossFieldOperator.LE ,message = "最小数量必须小于等于最大数量")
})
public class ProductPriceQtyRange implements Serializable{
/**
* 最小数量
*/
@Min(value = 0,message = "最小数量不合法")
private int minQty;
/**
* 最大数量
*/
@Min(value = 0,message = "最大数量不合法")
private int maxQty; public int getMinQty() {
return minQty;
} public void setMinQty(int minQty) {
this.minQty = minQty;
} public int getMaxQty() {
return maxQty;
} public void setMaxQty(int maxQty) {
this.maxQty = maxQty;
}
}


需要在mvc的配置文件中增加如下节点以启动检验

 <mvc:annotation-driven validator="validator">

  <bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="messageSource"/>
</bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="useCodeAsDefaultMessage" value="false"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>

经过上面在对象属性上的数据检验注解,我们将大部分的数据检验逻辑从业务逻辑中转移出去,不光是精简了代码还使得原本复杂的代码变得简单清晰,代码的重复利用率也增强了。

本文引用:

http://stackoverflow.com/questions/1972933/cross-field-validation-with-hibernate-validator-jsr-303

用好spring mvc validator可以简化代码的更多相关文章

  1. Spring mvc下载文件java代码

    /** * 下载模板文件 * @author cq */ @RequestMapping("/downloadExcel.do") public ResponseEntity< ...

  2. Spring MVC拦截器完整代码示例

     拦截器的作用: 编写一个自定义的类,实现相关拦截器接口:  preHandler不放行,直接return false:直接跳转到错误页面error.jsp postHandler后置处理器,也就是C ...

  3. Spring MVC 学习总结(五)——校验与文件上传

    Spring MVC不仅是在架构上改变了项目,使代码变得可复用.可维护与可扩展,其实在功能上也加强了不少. 验证与文件上传是许多项目中不可缺少的一部分.在项目中验证非常重要,首先是安全性考虑,如防止注 ...

  4. spring mvc接收数组

    (一)前言 对于springmvc接收数组的问题啊,我试验过几次,但是了有时候成功了,有时候失败了,也不知道为啥的,然后现在又要用到了,所以打算具体看看到底怎么回事,但是了我实验成功了顺便找了好多资料 ...

  5. spring MVC 使用 hibernate validator验证框架,国际化配置

    spring mvc使用hibernate validator框架可以实现的功能: 1. 注解java bean声明校验规则. 2. 添加message错误信息源实现国际化配置. 3. 结合sprin ...

  6. springboot学习章节代码-Spring MVC基础

    1.项目搭建. <?xml version="1.0" encoding="UTF-8"?> <project xmlns="htt ...

  7. Spring MVC 到 Spring Boot 的简化之路(山东数漫江湖)

    背景 从Servlet技术到Spring和Spring MVC,开发Web应用变得越来越简捷.但是Spring和Spring MVC的众多配置有时却让人望而却步,相信有过Spring MVC开发经验的 ...

  8. 【代码总结】Spring MVC数据校验

    1.实验介绍 --------------------------------------------------------------------------------------------- ...

  9. Spring MVC 到 Spring BOOT 的简化之路

    背景 Spring vs Spring MVC vs Spring Boot Spring FrameWork Spring 还能解决什么问题 Spring MVC 为什么需要Spring Boot ...

随机推荐

  1. Nginx负载均衡的详细配置及使用案例详解.

    感谢看过这一些列博文和评论的小伙伴, 我把自己所看到的学到的拿到这里来分享是想和大家一起学习进步, 想听听园友给出的意见, 也是对自己学习过程的一个总结. 技术无止境, 我们仍需努力! 1,话不多说, ...

  2. Js toString()方法笔记

    一.前言 toString()用于将该对象以字符串(string)的形式返回,该方法属于Object对象;也就是说对象都能够使用此方法. 之前所接触的是array.toString()数组转换为字符串 ...

  3. Spring学习记录(六)---使用外部属性文件

    在bean配置资源或系统部署,如数据库的连接时,需要这样: 要包含相关jar包:c3p0.jar 和mysql.connector.jar xml配置: <bean id="dataS ...

  4. Enterprise Solution 虚拟测试环境

    在不联网的情况下,一台物理电脑安装数据库服务,VMware创建多个虚拟机,虚拟机中多个客户端并发连接到物理主机.可共用同一个物理主机的数据库,也可以测试多用户并发等问题. 1  安装微软虚拟网卡.在控 ...

  5. ASP.NET MVC5 网站开发实践(二) Member区域–我的咨询列表及添加咨询

    上次把咨询的架构搭好了,现在分两次来完成咨询:1.用户部分,2管理部分.这次实现用户部分,包含两个功能,查看我的咨询和进行咨询. 目录: ASP.NET MVC5 网站开发实践 - 概述 ASP.NE ...

  6. poj 3414 Pots bfs+模拟

    #include<iostream> #include<cstring> #define fillA 1 #define pourAB 2 #define dropA 3 #d ...

  7. 【原创】开源Math.NET基础数学类库使用(17)C#计算矩阵条件数

                   本博客所有文章分类的总目录:[总目录]本博客博文总目录-实时更新  开源Math.NET基础数学类库使用总目录:[目录]开源Math.NET基础数学类库使用总目录 上个月 ...

  8. 让你的Mac支持NTFS

    前段时间换成Mac电脑之后,发现有一点不爽,不能在Mac下写入NTFS格式的磁盘,所以就去研究了一下. 解决方法有如下三种. 第一种,直接使用第三方软件,如Paragon NTFS for MAC,T ...

  9. Deferred在jQuery和Angular中的使用与简单实现

    Deferred在jQuery和Angular中的使用与简单实现 Deferred是在jQuery1.5版本中加入的,并且jQuery使用它完全重写了AJax,以前也只是偶尔使用.但是上次在使用Ang ...

  10. 韩国"被申遗" (转自果壳)

    "被申遗"不是指"没申遗",而是全都经过了中国人重新包装. 近日,有报道称韩国计划将火炕申报世界遗产,联系近年来韩国多起"申遗事件",国内网 ...