Spring boot JSR-303验证实战,简单又全面

作为一名码农,哪天工作不会遇到问题,但总是这边解决,那边忘记,过几天再遇到,再解决,恶性循环呢。摆脱恶性循环的第一步,用烂笔头来弥补自己的记忆。

也曾经使用过SSH框架,在使用SSH框架做后台验证的时候,并没有使用框架里自带的验证,而是纯手写的验证,那些验证呀,很多都是重复的,很是痛苦。后来使用spring cloud的框架,项目组的“恶习”并没有改掉,还是使用旧的验证模式。额,除了无语,更觉得抱歉,不能够说服项目主导人使用便利的验证方式。

写后台验证那么久,预想一下理想的验证应该是什么样子的呢?这也是今天所遇到的问题:

  1. 如何对一些不必输入字段,只做格式验证;
  2. 前台传递过来的数据是日期,怎么处理;
  3. 从mybatis查询出的数据,如果是日期,如何格式化为日期格式,而不去修改xml文件;
  4. 很懒,如何对一些经常用到的字段,做一个公共验证的方法或类;
  5. 还是懒,想用注解做验证,减少代码量,更减少和业务代码的耦合;

初次使用Spring Boot里面的验证,还需要先研究一下。Spring Boot里面都有什么验证呢?Spring Boot支持JSR-303验证规范,JSR是Java Specification Requests的缩写。JSR-303是Bean Validation 1.0 (JSR 303),说白了就是基于bean的验证,更多的解释参考JCP的官网。在默认情况下,Spring Boot会引入Hibernate Validator机制来支持JSR-303验证规范。

基于JSR-303的注解有哪些,上张图,以便日后查看。更多还需参考网址:https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html
Bean Validation 中的 constraint
表 1. Bean Validation 中内置的 constraint

Constraint 详细信息
@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(value) 被注释的元素必须符合指定的正则表达式

表 2. Hibernate Validator 附加的 constraint

Constraint 详细信息
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

JSR-303是基于Bean的验证,那就是需要在Bean上加注解喽,本次使用的Spring Boot版本是2.1.4.RELEASE,为什么强调版本,版本不一样,有些实现细节就存在差异。下面上代码。

第一步,在bean上增加注解,进行验证;
/**
 * 实体类
 * @author 程就人生
 *
 */
public class Test {
    private String userUid; 
    //用户名不为空,使用默认提示
    @NotNull
    private String userName;    
    
    //密码进行长度和格式的验证,个性化提示
    @Size(min=6, max=15,message="密码长度必须在 6 ~ 15 字符之间!")
    @Pattern(regexp="^[a-zA-Z0-9|_]+$",message="密码必须由字母、数字、下划线组成!")
    private String userPwd;
    
    //手机号码也用个性化提示,使用正则表达式进行匹配,非空时不验证
    @Pattern(regexp="^1(3|4|5|7|8)\\d{9}$",message="手机号码格式错误!")
    private String userMobile;
    
    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    @DateTimeFormat(pattern="yyyy-MM-dd")
    private Date userBirthday;
    
    private Byte status;    
    
    private Date updateDate;

    private String updateUser;

    private Date createDate;

    private String createUser;

    public String getUserUid() {
        return userUid;
    }

    public void setUserUid(String userUid) {
        this.userUid = userUid;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }

    public String getUserMobile() {
        return userMobile;
    }

    public void setUserMobile(String userMobile) {
        this.userMobile = userMobile;
    }

    public Byte getStatus() {
        return status;
    }

    public void setStatus(Byte status) {
        this.status = status;
    }

    public Date getUpdateDate() {
        return updateDate;
    }

    public void setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
    }

    public String getUpdateUser() {
        return updateUser;
    }

    public void setUpdateUser(String updateUser) {
        this.updateUser = updateUser;
    }

    public Date getCreateDate() {
        return createDate;
    }

    public void setCreateDate(Date createDate) {
        this.createDate = createDate;
    }

    public String getCreateUser() {
        return createUser;
    }

    public void setCreateUser(String createUser) {
        this.createUser = createUser;
    }

    public Date getUserBirthday() {
        return userBirthday;
    }

    public void setUserBirthday(Date userBirthday) {
        this.userBirthday = userBirthday;
    }
}

说明:注解@Size是限定字段长度的,@Pattern是匹配正则表达式的,@DateTimeFormat是用来转换前台传递过来的日期,前台传递过来的日期必须是yyyy-MM-dd格式的字符串,后台才能正确接收,这几个参数都没有做非空验证,所以允许为null。

用mybatis从数据库查询出来的日期格式的数据是long型,如:1558504462000,想把它转换成年月日的形式,就用注解@JsonFormat,转换出来的时间总是少一天,后面加上timezone = "GMT+8"就可以了。

第二步,在Controller上绑定验证
import java.util.HashMap;
import java.util.Map;

import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.bean.Test;

/**
 * 测试验证
 * @author 程就人生
 * 
 */
@RestController
public class ValidatorTestController {

    /**
     * 使用 @Validated 开启对象验证
     * @param test
     * @return
     */
    @PostMapping("/validator")
    public Object validatorObject(@Validated Test test, BindingResult br){
        Map errorMap = new HashMap();
        if(br.hasErrors()){
            //对错误集合进行遍历,有的话,直接放入map集合中
            br.getFieldErrors().forEach(p->{
                errorMap.put(p.getField(), p.getDefaultMessage());
            });
        }
        //返回错误信息
        return errorMap;
    }
}

说明:BindingResult必须紧跟在@Validated的后面,特别是有多个的时候,要一对一对的排列,不能乱了顺序。
乱了顺序,验证失败时,只会在后台抛异常,而在controller方法里获取步到。

第三步,进行测试,先输入非法的数据,在输入合法的数据,测试结果OK
测试结果-1
测试结果-2

如果想自定义验证方法,不希望在Bean里面加注解,怎么做呢?

第一步,自定义验证类,实现Validator 接口
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;


/**
 * 自定义验证类
 * @author 程就人生
 *
 */
public class TestValidator implements Validator{

    @Override
    public boolean supports(Class clazz) {
                //对需要验证的类进行绑定
        return Test.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        if(target == null){
            //TODO
            return;
        }
        // 把校验信息注册到Error的实现类里,两种写法
        // ValidationUtils.rejectIfEmpty(errors,"userMobile",null,"手机号码不能为空!!");
        // 对对象进行强转
        Test test = (Test) target;

        // 手机号码的验证,不为空时的一些验证
        if(StringUtils.isEmpty(test.getUserMobile())){
            errors.rejectValue("userMobile", null, "手机号不能为空!");
        }
        //其他自定义验证
    }

}

说明:重写接口里的两个方法,先对需要进行验证的实体进行绑定,这个类实现了Validator接口,重写接口里的validate,自定义验证方法。

第二步,在Controller添加initBinder方法进行绑定,其他不变
/**
     * 验证处理,initBinder方法在参数转换之前执行(转换规则,格式化)
     * @param webDataBinder
     */
    @InitBinder
    public void initBinder(WebDataBinder webDataBinder) {

        webDataBinder.addValidators(new TestValidator());

    }

说明:initBinder方法是参数转换之前执行,在执行具体的controller方法前。

第三步,进行测试,不输入手机号,测试结果OK
测试结果-3

还有问题,在实体类上写正则表达式的时候,比如说手机号码的验证,可能有好几个类都需要进行手机号码格式的验证,每个类都写一次,也是很繁琐的,有没有更简单更公共的的方法呢?

当然有,使用注解,根据JSR-303规范,一个 constraint 通常由 annotation 和相应的 constraint validator 组成,一个annotation可以对那个多个constraint validator。先不管这么多,写一个验证手机号的@Mobile试一试吧。

第一步,编写注解类Mobile
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;
/**
 * 验证手机号码的注解类
 * @author 程就人生
 * @date 2019年5月22日
 * @Description 
 *
 */
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MobileValidator.class)  //对应的验证实现类
public @interface Mobile { 
    
    //默认提示
    String message() default "手机号码格式错误!"; 

    Class[] groups() default {}; 

    Class[] payload() default {}; 

}
第二步,实现MobileValidator类
import java.util.regex.Pattern;

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

import com.alibaba.druid.util.StringUtils;
/**
 * 验证手机号码的实现类
 * @author 程就人生
 * @date 2019年5月22日
 * @Description 
 *
 */
public class MobileValidator implements ConstraintValidator { 

    //验证手机的正则表达式
    private String mobileReg = "^1(3|4|5|7|8)\\d{9}$";
    
    private Pattern mobilePattern = Pattern.compile(mobileReg); 

    public void initialize(Mobile mobile) {

    } 

    public boolean isValid(String value, ConstraintValidatorContext arg1) {
       //为空时,不进行验证
       if (StringUtils.isEmpty(value))

           return true;
       
       //返回匹配结果
       return mobilePattern.matcher(value).matches();

    } 

}
第三步,将Test类上验证手机号的正则表达式换成@Mobile注解
    //手机号码也用个性化提示,使用正则表达式进行匹配,非空时不验证
    //@Pattern(regexp="^1(3|4|5|7|8)\\d{9}$",message="手机号码格式错误!")
    @Mobile
    private String userMobile;
第四步,进行测试,输入非法的手机号,测试结果OK
测试结果-4

总结,这里面使用的注解和一些方法都是来自org.springframework.validation.annotation的架包,又一次感觉到了Spring组件的强大。

你可能感兴趣的:(Spring boot JSR-303验证实战,简单又全面)