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<String,Object> errorMap = new HashMap<String,Object>();
        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<? extends Payload>[] 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<Mobile, String> { 

    //验证手机的正则表达式
    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组件的强大。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容