美文网首页IT@程序员猿媛Java架构技术进阶SpringBoot精选
Spring boot JSR-303验证实战,简单又全面

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

作者: 程就人生 | 来源:发表于2019-05-22 20:32 被阅读2次

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

    也曾经使用过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组件的强大。

    相关文章

      网友评论

        本文标题:Spring boot JSR-303验证实战,简单又全面

        本文链接:https://www.haomeiwen.com/subject/epkpzqtx.html