springBoot验证器Validator花样玩法

在开始之前还是老样子,为什么要用验证器Validator,很多码友都写过增删改查,所有数据在入库之前都要在后端进行字段值验证,否不否合对应的数据库格式以及字符长度,当然也有很多码友偷懒,前端加了验证后端就不管了,这是不可取的,前端因为是透明的,在传输到后端之前首先在浏览器F12就可以篡改跳过校验,在经过网线各种路由器数据包被拦截也可以被篡改,所以如果不在后端对数据格式验证是很危险的,那么勤快的小伙伴就会在后端验证了,可是写的都是一堆一堆的if判断,当然这种方式的确达到了验证的要求但是对代码侵入性太大而且要验证的字段多了更加难以维护,带着这个问题那怎么办呢?

Hibernate的Validator横空出世,相信很多码友也都用过,可能都是停留在单个字段的验证上面,那如果我要多个字段联合验证呢?对整个类验证呢?自定义验证注解呢?不用慌这就是这篇文章要做的事,常用的玩法我们一起玩。

文章的项目是springBoot的,其实jpa也有验证器,基于springMvc的,也可以做到字段验证的要求可是不好用,不过这里也会介绍一下,jpa也已经集成了hibernate的验证器,我们的项目不需要额外引包,下面就开始介绍各种玩法,

1、只对字段属性验证

在项目包目录下创建一个vo的包,创建一个UserVo类,用于验证器注解的测试

注解说明

@NotEmpty 是不能为空,也就是空字符串也不行
@NotNull 是不能为null
@AssertTrue 只能为true
@Range 在什么范围之间
@Future 将来的日期
@Past 过去的日期
@Pattern 正则验证
message 提示消息
public class UserVo {

//    @NotNull(message = "id不能为空")
    @NotEmpty(message = "id不能为空")
    private String id;

    @Length(min=20,max=30,message = "${validatedValue} 字符串长度要求{min}到{max}之间")
    private String userName;

    @NotNull(message = "密码不能为空")
    private String userPswd;

    @AssertTrue
    private boolean flagTrue;

    @AssertFalse
    private boolean flagFalse;

    @DecimalMin(value = "10",message = "最小值{value}")
    @DecimalMax(value="20",message = "最大值{value}")
    private BigDecimal bigDecimal;

    @NotNull
    @DecimalMin(value="0.01",message = "最小值{value}")
    @DecimalMax(value="999.00",message = "最大值{value}")
    private Double doubleValue=null;

    @NotNull
    @Min(value=1,message = "最小值{value}")
    @Max(value=88,message = "最大值{value}")
    private Integer intValue=null;

    @Range(min=1,max=888,message = "范围{min}到{max}")
    private Long range;

    @Email(message = "邮箱格式错误")
    private String email;

    @Future(message = "需要一个将来的日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date createDate;


    private String createBy;

    @Past(message = "需要一个过去的日期")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Timestamp updateDate;

    @Length(min=2,max=8,message = "这个值${validatedValue}不符合,至少{min}个字符,最多{max}个字符")
    private String updateBy;

    @Pattern(regexp = "^[0-9]*$",message = "只能为数字")
    private String phoneNo;
    ......省略get set方法......
}

用测试用例先测试一下,后面会调用controller测试

编写测试用例代码

在test下新建UserVoValidator测试类

package com.apgblogs.firstspringboot;

import com.apgblogs.springbootstudy.vo.UserVo;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.math.BigDecimal;
import java.util.Iterator;
import java.util.Set;

/**
 * @author xiaomianyang
 * @description
 * @date 2019-07-09 10:19
 */
public class UserVoValidator {

    private static Logger logger= LoggerFactory.getLogger(UserVoValidator.class);

    private static Validator validator;

    @BeforeClass
    public static void setUpValidator(){
        ValidatorFactory validatorFactory= Validation.buildDefaultValidatorFactory();
        validator=validatorFactory.getValidator();
    }

    @Test
    public void userIsNull(){
        UserVo userVo=new UserVo();
        userVo.setId("你最帅");
        userVo.setUserName("关注我");
        userVo.setBigDecimal(new BigDecimal(88));
        userVo.setIntValue(32432);
        userVo.setDoubleValue(22.3);
        userVo.setEmail("sdfsdfdsfs");
        userVo.setFlagFalse(true);
        userVo.setFlagTrue(false);
        userVo.setPhoneNo("2343243");
        Set<ConstraintViolation<UserVo>> constraintViolationSet=validator.validate(userVo);
        for(Iterator<ConstraintViolation<UserVo>> iterator=constraintViolationSet.iterator();iterator.hasNext();){
            ConstraintViolation<UserVo> constraintViolation=iterator.next();
            logger.info("验证结果,属性:{},结果:{}",constraintViolation.getPropertyPath(),constraintViolation.getMessage());
        }
    }
}

运行之后看到如下结果,如果不符合条件的属性就会提示出来,这个可以自己改变参数试一试

2、自定义注解对字段联合验证,springMvc的方式

这种方式呢可以说是被弃用的方式,这里也可以说一下,在validator包下新建一个类UserValidator实现Validator接口并覆写两个方法

package com.apgblogs.springbootstudy.validator;

import com.apgblogs.springbootstudy.vo.UserVo;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

/**
 * @author xiaomianyang
 * @description
 * @date 2019-07-09 17:17
 */
public class UserValidator implements Validator {

    @Override
    public boolean supports(Class<?> aClass) {
        return aClass.equals(UserVo.class);
    }

    @Override
    public void validate(Object o, Errors errors) {
        if(o == null){
            errors.rejectValue("",null,"用户不能为空");
            return;
        }
        UserVo userVo=(UserVo)o;
        if(StringUtils.isEmpty(userVo.getCreateBy())){
            errors.rejectValue("createBy",null,"创建人不能为空");
        }
    }
}

新建一个控制器ValidateController,载入验证器测试

启动项目使用postman进行测试

可以看到自定义的验证器已经起作用了,这里只写了对一个字段验证的,也可以写关联其他字段验证,但是看到结果我想都会有个疑问吧,为啥其他字段验证的结果没有返回,只有一个createBy的提示消息,这也就是为啥不用这个验证器的原因之一,可能也可以做到但是我觉得还是不太合适

3、自定义注解对字段联合验证,Hibernate的方式

hibernate已经提供了一些验证注解给我们使用,可是实际项目中远远是满足不了要求的,比如我们对某个用户名验证是否在数据库中存在,常规的方式可能都是在业务层写代码判断,但这里我们使用验证器处理这个需求

在validator包下创建两个类,一个是UserNameNotExist注解,一个是UserNameValidator验证器

UserNameNotExist

自定义注解,这里面有很多的注解我这里就不详细说了,hibernate官网有详细解释
@Target 是目标对象,就是这个注解可以写在什么地方,方法,字段,参数等
@Constraint(validatedBy = UserNameValidator.class) 我们自定义的验证器,也就是这个注解的验证逻辑是那个类来处理
package com.apgblogs.springbootstudy.validator;


import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

/**
 * @author xiaomianyang
 * @description
 * @date 2019-07-10 17:06
 */
@Documented
@Retention(RUNTIME)
@Target({METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR, PARAMETER, TYPE_USE})

public @interface UserNameNotExist {

    String message() default "用户${validatedValue}不存在";

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

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

UserNameValidator

这里是用户名验证逻辑,主要判断用户名是否存在,这里只是写死的值,也可以将service Autowired进来做数据库层面的验证
package com.apgblogs.springbootstudy.validator;

import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * @author xiaomianyang
 * @description
 * @date 2019-07-10 17:12
 */
public class UserNameValidator implements ConstraintValidator<UserNameNotExist,String> {

    @Override
    public void initialize(UserNameNotExist constraintAnnotation) {
    }

    @Override
    public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) {
        if(StringUtils.isEmpty(username)){
            return true;
        }
        if(username.equals("Java")){
            return true;
        }
        return false;
    }
}

使用自定义注解到UserVo属性上

//    @Length(min=20,max=30,message = "${validatedValue} 字符串长度要求{min}到{max}之间")
    @UserNameNotExist
    private String userName;

测试自定义注解,测试用例代码如下

    @Test
    public void userIsNull(){
        UserVo userVo=new UserVo();
        userVo.setId("你最帅");
        userVo.setUserName("关注我");
        userVo.setBigDecimal(new BigDecimal(88));
        userVo.setIntValue(32432);
        userVo.setDoubleValue(22.3);
        userVo.setEmail("sdfsdfdsfs");
        userVo.setFlagFalse(true);
        userVo.setFlagTrue(false);
        userVo.setPhoneNo("2343243");
        Set<ConstraintViolation<UserVo>> constraintViolationSet=validator.validate(userVo);
        for(Iterator<ConstraintViolation<UserVo>> iterator=constraintViolationSet.iterator();iterator.hasNext();){
            ConstraintViolation<UserVo> constraintViolation=iterator.next();
            logger.info("验证结果,属性:{},结果:{}",constraintViolation.getPropertyPath(),constraintViolation.getMessage());
        }
    }

可以看到控制台的提示,用户关注我不存在

说明自定义注解起作用了

4、对整个类验证以及类下的所有属性验证

可以看到目前的验证都是只能验证单个字段,如果想验证多个字段,联合验证,比如单价*数量和库存联合验证,再比如如果选择了某个值另一个值必须是啥这种验证,那么上述的方式除了springMvc都满足不了要求,不用怕用hibernate的类级别验证器就可以实现

依然在validator新建一个注解UserClassCheck和一个类UserClassValidator

UserClassCheck

类级别验证器,可以加到类上面,里面的所有属性和方法都可以访问到
package com.apgblogs.springbootstudy.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.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author xiaomianyang
 * @description
 * @date 2019-07-10 17:45
 */
@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = UserClassValidator.class)
@Documented
public @interface UserClassCheck {

    String message() default "";

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

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

UserClassValidator

类级别验证逻辑,在这里就可以编写上述各种需求的验证
package com.apgblogs.springbootstudy.validator;

import com.apgblogs.springbootstudy.vo.UserVo;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

/**
 * @author xiaomianyang
 * @description
 * @date 2019-07-10 17:52
 */
public class UserClassValidator implements ConstraintValidator<UserClassCheck, UserVo> {

    @Override
    public void initialize(UserClassCheck constraintAnnotation) {

    }

    @Override
    public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
        if(userVo == null){
            return true;
        }
        String messageTemplate;
        if(userVo.getUserName().equals("关注我") && userVo.getId().equals("你最帅")){
            return true;
        }
        messageTemplate=String.format("用户名:%1$s和ID:%2$s不匹配",userVo.getUserName(),userVo.getId());
        constraintValidatorContext.disableDefaultConstraintViolation();
        constraintValidatorContext.buildConstraintViolationWithTemplate(messageTemplate).addConstraintViolation();
        return false;

    }
}

将类验证器注解加到UserVo上面测试

@UserClassCheck
public class UserVo {
    ......
}

测试用例代码

@Test
    public void userIsNull(){
        UserVo userVo=new UserVo();
        userVo.setId("你最帅啦啦啦");
        userVo.setUserName("关注我");
        userVo.setBigDecimal(new BigDecimal(88));
        userVo.setIntValue(32432);
        userVo.setDoubleValue(22.3);
        userVo.setEmail("sdfsdfdsfs");
        userVo.setFlagFalse(true);
        userVo.setFlagTrue(false);
        userVo.setPhoneNo("2343243");
        Set<ConstraintViolation<UserVo>> constraintViolationSet=validator.validate(userVo);
        for(Iterator<ConstraintViolation<UserVo>> iterator=constraintViolationSet.iterator();iterator.hasNext();){
            ConstraintViolation<UserVo> constraintViolation=iterator.next();
            logger.info("验证结果,属性:{},结果:{}",constraintViolation.getPropertyPath(),constraintViolation.getMessage());
        }
    }

测试结果

可以看到结果,不光自定义的类验证注解起作用了,其他的属性注解也在发挥作用,这样就可以共同使用了,是不是很嗨皮

6、文章源码地址

码云:https://gitee.com/apgblogs/springBootStudy/tree/validator/

现在就已经介绍完了比较常用的一些验证方式,对于参数和字段验证就不要在业务层写一堆的判断了,使用这种方式验证会更加优雅,而且可以自定义通用验证器,其他的业务也可以拿来使用,减少代码重复,如果验证规则变更只需要更改验证器就可以,对业务层代码没有侵入性是不是很方便。

hibernate的Validator也不仅仅只有这些功能,还有其他比如交叉约束,分组校验,方法参数验证,方法返回值验证,集合内部属性验证,比如写一个集合当一个集合中不应该出现某个值时就可以使用这种验证规则,这些在hibernate官网都有详细的文档解释,可以好好看一下。

发表评论