数据校验是任何一个应用程序都会用到的功能,无论是显示层还是持久层. 通常,相同的校验逻辑会分散在各个层中, 这样,不仅浪费了时间还会导致错误的发生(译注: 重复代码). 为了避免重复, 开发人员经常会把这些校验逻辑直接写在领域模型里面, 但是这样又把领域模型代码和校验代码混杂在了一起, 而这些校验逻辑更应该是描述领域模型的元数据.

spring校验参数统一父类 spring参数校验注解_Hibernate validator


JSR 303 - Bean Validation - 为实体验证定义了元数据模型和API. 默认的元数据模型是通过Annotations来描述的,但是也可以使用XML来重载或者扩展. Bean Validation API 并不局限于应用程序的某一层或者哪种编程模型, 例如,如图所示, Bean Validation 可以被用在任何一层, 或者是像类似Swing的富客户端程序中.

 

Hibernate Validator is the reference implementation of

spring校验参数统一父类 spring参数校验注解_spring校验参数统一父类_02

this JSR. The implementation itself as well as the Bean Validation API and TCK are all provided and distributed under the Apache Software License 2.0

 

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    
 
 
Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内
 

1.配置

spring-boot-starter-web包自动依赖hibernate-validator,不用再重复引入,直接开搞

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.3.1.Final</version>
</dependency>

声明一个bean注册到spring容器,这个bean是一个容器后处理器,会把校验的逻辑通过AOP织入有@Validated注解的class,具体可以看这个类的源码

这一步在springboot其实也不用做,ValidationAutoConfiguration这个配置类自动帮我们做了

@Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(){
     return new MethodValidationPostProcessor();
    }

验证不通过会产生异常,因为我们项目提供rest接口,所以通过全局捕获异常,然后转换为json给前台

@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 用来处理bean validation异常
     * @param ex
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public  WebResult resolveConstraintViolationException(ConstraintViolationException ex){
        WebResult errorWebResult = new WebResult(WebResult.FAILED);
        Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
        if(!CollectionUtils.isEmpty(constraintViolations)){
            StringBuilder msgBuilder = new StringBuilder();
            for(ConstraintViolation constraintViolation :constraintViolations){
                msgBuilder.append(constraintViolation.getMessage()).append(",");
            }
            String errorMessage = msgBuilder.toString();
            if(errorMessage.length()>1){
                errorMessage = errorMessage.substring(0,errorMessage.length()-1);
            }
            errorWebResult.setInfo(errorMessage);
            return errorWebResult;
        }
        errorWebResult.setInfo(ex.getMessage());
        return errorWebResult;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public WebResult resolveMethodArgumentNotValidException(MethodArgumentNotValidException ex){
        WebResult errorWebResult = new WebResult(WebResult.FAILED);
        List<ObjectError>  objectErrors = ex.getBindingResult().getAllErrors();
        if(!CollectionUtils.isEmpty(objectErrors)) {
            StringBuilder msgBuilder = new StringBuilder();
            for (ObjectError objectError : objectErrors) {
                msgBuilder.append(objectError.getDefaultMessage()).append(",");
            }
            String errorMessage = msgBuilder.toString();
            if (errorMessage.length() > 1) {
                errorMessage = errorMessage.substring(0, errorMessage.length() - 1);
            }
            errorWebResult.setInfo(errorMessage);
            return errorWebResult;
        }
        errorWebResult.setInfo(ex.getMessage());
        return errorWebResult;
    }
}

这两个异常分别对应校验的两种使用方式

  1. 在方法里面校验
  2. 在bean对象里面校验
    经过测试,以上两种形式的数据验证不仅仅对controller层有用,在service层也行,只要这个类在spring ioc容器里面

 

可以自定义异常格式

package com.start.base.common.validator;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;import javax.validation.ConstraintViolation;
 import javax.validation.Validation;
 import javax.validation.Validator;
 import javax.validation.ValidatorFactory;import org.springframework.beans.factory.InitializingBean;
 import org.springframework.stereotype.Component; @Component
 public class CommonValidator implements InitializingBean {   private Validator validator;
   //实现校验方法并返回结果
    public ValidationResult validate(Object bean) {
        ValidationResult validationResult = new ValidationResult();
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(bean);
        if (constraintViolations.size() > 0) {
            //有错误
            validationResult.setHasError(true);
            Map<String,String> errMsgMap = new HashMap<>();
            constraintViolations.forEach(constraintViolation -> {
                String errMsg = constraintViolation.getMessage();
                String propertyName = constraintViolation.getPropertyPath().toString();
                errMsgMap.put(propertyName,errMsg);
                
            });
            validationResult.setErrMsgMap(errMsgMap);
        }
        return validationResult;
    }   @Override
    public void afterPropertiesSet() throws Exception {
        //使hibernate validator通过工厂初始化进行实例化
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        this.validator = factory.getValidator();
       
    }
 }package com.start.base.common.validator;
import java.util.HashMap;
 import java.util.Map;import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
//import org.springframework.util.StringUtils;
public class ValidationResult {
    //判断校验是否有错
     private boolean hasError = false;    //存放错误信息的map
     private Map<String,String> errMsgMap = new HashMap<>();    //格式化字符串信息获得错误结果的msg方法
     public String getErrMsg(){
        
         return JSONObject.toJSONString(errMsgMap);
     }    public void setHasError(boolean b) {
         // TODO Auto-generated method stub
         this.hasError=b;
     }    public Object getErrMsgMap() {
         // TODO Auto-generated method stub
         return errMsgMap;
     }    public void setErrMsgMap(Map<String, String> errMsgMap) {
          this.errMsgMap = errMsgMap;
     }
 }

 

2.使用

2.1常用校验注解

@AssertFalse 校验false
@AssertTrue 校验true
@DecimalMax(value=,inclusive=) 小于等于value,
inclusive=true,是小于等于
@DecimalMin(value=,inclusive=) 与上类似
@Max(value=) 小于等于value
@Min(value=) 大于等于value
@NotNull  检查Null
@Past  检查日期
@Pattern(regex=,flag=)  正则
@Size(min=, max=)  字符串,集合,map限制大小
@Valid 对po实体类进行校验

这篇文章介绍的注解更全一点

2.2在方法参数上使用

@Controller
@Validated
public class ValidationController {

    @GetMapping("/validate1")
    @ResponseBody
    public String validate1(
            @Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
            @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age,
            @Future @RequestParam("birth")@DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth){
        return "validate1";
    }
}

注意类名需要加注解@Validated
校验失败会抛出ConstraintViolationException异常
然后我们在全局异常捕获类捕获这个异常,返回给前台对应的错误json

2.3在bean内属性上使用

给model类增加校验注解

public class User {

    @Size(min = 1,max = 10,message = "姓名长度必须为1到10")
    private String name;

    @NotEmpty
    private String firstName;

    @Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100")
    private Integer age;

    @Future
    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Date birth;

    ...getter setter
}

在controller对应User实体前增加@Valid注解

@PostMapping("/validate2")
@ResponseBody
public User validate2(@Valid @RequestBody User user){
    return user;
}

message支持表达式和EL表达式 ,比如message = "姓名长度限制为{min}到{max} ${1+2}")

想把错误描述统一写到properties的话,在classpath下面新建ValidationMessages_zh_CN.properties文件(注意value需要转换为unicode编码),然后用{}格式的占位符

3.扩展

除了默认提供的校验注解外,我们可以定义自己的校验注解

3.1.创建约束注解类

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { HandsomeBoyValidator.class})
public @interface HandsomeBoy {

    String message() default "123";

    String name();

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default {};
}

注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = { HandsomeBoyValidator.class})用来指定处理这个注解逻辑的类

一开始写了这个自定义注解和验证类,发现没有生效,最后发现是@Constraint这个注解里的类没有配置,还跟了很多源码,蛋疼,总的来讲,这个配置还是挺方便的

3.2.创建验证器类

public class HandsomeBoyValidator implements ConstraintValidator<HandsomeBoy, User> {

    private String name;

    /**
     * 用于初始化注解上的值到这个validator
     * @param constraintAnnotation
     */
    @Override
    public void initialize(HandsomeBoy constraintAnnotation) {
        name =constraintAnnotation.name();
    }

    /**
     * 具体的校验逻辑
     * @param value
     * @param context
     * @return
     */
    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        return name ==null || name.equals(value.getName());
    }
}

这边的功能是user类里面的name字段必须和配置的一样,否则输出一个事实

3.3. 测试

@PostMapping("/validate3")
@ResponseBody
public User validate3(@Valid @HandsomeBoy(name = "scj",message = "666") @RequestBody  User user){
    return user;
}

如果验证不通过,会输出盛超杰第二帅,全局异常处理器不要忘记配置

3.4.创建自定义大小验证器类

public enum CaseMode {
     UPPER,
     LOWER;
 } @Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
 @Retention(RetentionPolicy.RUNTIME)
 @Constraint(validatedBy = CheckCaseValidator.class)
 @Documented
 public @interface CheckCase {
     String message() default "";    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    CaseMode value();
 } public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {
     private CaseMode caseMode;
     public void initialize(CheckCase checkCase) {
         this.caseMode = checkCase.value();
     }    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
         if (s == null) {
             return true;
         }        if (caseMode == CaseMode.UPPER) {
             return s.equals(s.toUpperCase());
         } else {
             return s.equals(s.toLowerCase());
         }
     }
 }

 

3.5.创建自定义日期格式(yyyy-MM-dd HH:mm:ss)验证器类

package com.start.base.common.validator;
import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;import javax.validation.Constraint;
 import javax.validation.Payload;
 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target; import javax.validation.Constraint;
 import javax.validation.Payload;@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
 @Retention(RetentionPolicy.RUNTIME)
 @Constraint(validatedBy = CheckDateValidator.class)
 @Documented
 public @interface CheckDate {
     String message() default "";    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String date()  default "";
 } 
 
package com.start.base.common.validator;
import java.text.ParseException;
 import java.text.SimpleDateFormat;import javax.validation.ConstraintValidator;
 import javax.validation.ConstraintValidatorContext;public class CheckDateValidator implements ConstraintValidator<CheckDate, String> {
     private String date;
     public void initialize(CheckDate checkDate) {
         this.date= checkDate.date();
     }    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
         if (s == null) {
             return false;
         }       SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         try {
             sdf.parse(s);
             this.date = sdf.parse(s).toString();
             return true;
         } catch (ParseException e) {
             //e.printStackTrace();
             this.date = null;
             
         }
         return false;    }
 }

 

4.demo源码下载

https://github.com/hsn999/start-cloud/tree/master/base-common

 

参考

Hibernate validator官方文档 

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface