SpringBoot学习之旅(五)---Hibernate Validator(完美的参数校验)

前言

科技的进步是靠懒人推动的,但是这里说的懒人不是说纯懒的那种!开发过程我想最痛苦的事情就是做那种不懂脑子的体力活,即苦逼,又无趣,那么这样的体力活,我们就得想办法偷懒去做,既能完成相关的工作,又能快速达到效果,他好我也好!这种体力活参数校验就是其中的一个;

源码地址

点击下载

示例

看下面一段注册的接口

   /**
     * 注册
     *
     * @param name     用户昵称
     * @param phoneNum 用户号码
     * @param code     用户验证码
     * @param age      年龄
     * @param sex      性别
     * @param email    邮箱
     * @param pwd      密码
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public BaseRespObj register(
            @RequestParam(name = "name") String name,
            @RequestParam(name = "phoneNum") String phoneNum,
            @RequestParam(name = "phoneCode") Integer code,
            @RequestParam(name = "age") Integer age,
            @RequestParam(name = "sex") String sex,
            @RequestParam(name = "email") String email,
            @RequestParam(name = "pwd") String pwd) throws Exception {
            
    }

为了代码的健壮性,那么这里传上来的每一个参数都得做判空的校验,又不能不做,那么可能就会出现下面这样的校验:

	if(null==name || name.length()<=0 || null==phoneNum || phoneNum.length()<=0 || .....){
		throw new BusiException("参数数有误")
	}

这种操作能写到人高潮迭起,全身乏力,可能这一系列的参数校验,就占用了几十行代码,导致代码的可读性极差;那有没有更好的方式呢,当然是有的;hibernate-validator

优点及注解说明

  • validator的优点

    • 解耦,数据的校验与业务逻辑进行分离,降低耦合度
    • 规范的校验方式,减少参数校验所带来的繁琐体力活
    • 简洁代码,提高代码的可读性
  • Validation constraint

    注解作用
    @AssertFalse被注释的元素必须为 false
    @AssertTrue被注释的元素必须为 true
    @DecimalMin(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @DecimalMax(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Digits (integer, fraction)被注释的元素必须是一个数字,其值必须在可接受的范围内
    @Email被注释的元素必须是电子邮箱地址
    @Future被注释的元素必须是一个将来的日期
    @Length(min=,max=)被注释的字符串的大小必须在指定的范围内
    @Min(value)被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value)被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Negative该值必须小于0
    @NegativeOrZero该值必须小于等于0
    @Null被注释的元素必须为 null
    @NotNull被注释的元素必须不为 null
    @NotBlank(message =)验证字符串非null,且长度必须大于0
    @NotEmpty被注释的字符串的必须非空
    @Past被注释的元素必须是一个过去的日期
    @Pattern(regex=,flag=)被注释的元素必须符合指定的正则表达式
    @Positive该值必须大于0
    @PositiveOrZero该值必须大于等于0
    @Range(min=,max=,message=)被注释的元素必须在合适的范围内
    @Size(max=, min=)数组大小必须在[min,max]这个区间
    @URL(protocol=,host,port)检查是否是一个有效的URL,如果提供了protocol,host等,则该URL还需满足提供的条件
    @Valid该注解主要用于字段为一个包含其他对象的集合或map或数组的字段,或该字段直接为一个其他对象的引用,这样在检查当前对象的同时也会检查该字段所引用的对象

资源导入

pom.xml添加以下配置

        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.13.Final</version>
        </dependency>

代码实现

  • 校验响应对象

    package com.lupf.springboottest.utils.validator;
    
    import org.apache.commons.lang3.StringUtils;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 验证的响应对象
     */
    public class ValidationResult {
        /**
         * 是否存在错误
         */
        private Boolean hasError = false;
    
        /**
         * 错误的信息的map
         */
        private Map<String, String> errMsgMap = new HashMap<>();
    
        public Boolean getHasError() {
            return hasError;
        }
    
        public void setHasError(Boolean hasError) {
            this.hasError = hasError;
        }
    
        public Map<String, String> getErrMsgMap() {
            return errMsgMap;
        }
    
        public void setErrMsgMap(Map<String, String> errMsgMap) {
            this.errMsgMap = errMsgMap;
        }
    
        /**
         * 用于获取校验之后错误描述的信息
         *
         * @return
         */
        public String getErrMsg() {
            if (null != this.errMsgMap && this.errMsgMap.size() > 0)
                return StringUtils.join(this.errMsgMap.values().toArray(), ",");
            return null;
        }
    }
    
    
  • 校验帮助类

    package com.lupf.springboottest.utils.validator;
    
    import com.lupf.springboottest.error.BusiErrCodeEm;
    import com.lupf.springboottest.error.BusiException;
    import org.hibernate.validator.HibernateValidator;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.stereotype.Component;
    
    import javax.validation.ConstraintViolation;
    import javax.validation.Validation;
    import javax.validation.Validator;
    import java.util.Set;
    
    //把这个工具类交由Spring管理
    @Component
    public class ValidatorUtil implements InitializingBean {
    
        private Validator validator;
    
        /**
         * 通过Validator校验对象
         *
         * @param object
         * @param groups
         * @return
         */
        public ValidationResult validata(Object object, Class<?>... groups) {
            ValidationResult validationResult = new ValidationResult();
            Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(object, groups);
            if (constraintViolationSet.size() > 0) {
                constraintViolationSet.forEach(constraintViolation -> {
                    validationResult.setHasError(true);
                    String msg = constraintViolation.getMessage();
                    String propertyName = constraintViolation.getPropertyPath().toString();
                    validationResult.getErrMsgMap().put(propertyName, msg);
                });
            }
            return validationResult;
        }
    
        /**
         * 校验请求参数对象,如果出现未校验通过的,直接抛出异常
         *
         * @param object 待处理的对象
         * @param groups 校验分组
         * @throws BusiException
         */
        public void reqValidata(Object object, Class<?>... groups) throws BusiException {
            ValidationResult validationResult = this.validata(object, groups);
            if (null != validationResult && validationResult.getHasError()) {
                //请求参数存在不合法的数据,这里直接抛出异常
                throw new BusiException(BusiErrCodeEm.REQ_PARAM_10001, validationResult.getErrMsg());
            }
        }
    
        @Override
        public void afterPropertiesSet() {
            //failFast(true) true:快速校验,遇到不合法的就直接返回  false:全量校验,找出所有不合法的数据
            validator = Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();
        }
    }
    
    
  • 测试对象

    package com.lupf.springboottest.service.model;
    
    import org.hibernate.validator.constraints.Length;
    import org.hibernate.validator.constraints.URL;
    
    import javax.validation.constraints.*;
    
    /**
     * 用户的业务对象
     */
    public class UserModel {
        private Integer id;
    
        @NotBlank(message = "用户昵称不能为空")
        private String name;
    
        @NotNull(message = "性别选择不能为空")
        private Byte sex;
    
        @Max(value = 200, message = "年龄不能大于200岁")
        @Min(value = 0, message = "年龄不能小于0岁")
        private Integer age;
    
        @NotBlank(message = "手机号码不能为空")
        @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!")
        private String telphone;
    
        @Email(message = "邮箱格式错误")
        private String email;
    
        private String registerMode;
        private String thirdPartyId;
    
        @URL(message = "头像必须是链接地址")
        private String avatar;
    
        //这里是定义的用户加密后密码
        //由于DAO层
        //由于DO层用户信息和用户密码是分开的 但是从业务层面来看,这个密码确实是用户一部分,因此在这里的业务对象将用户信息和密码信息合并
        @Length(min = 6, max = 18, message = "密码长度为6-18个字符")//这个规则是校验明文的
        private String encrptPassword;
    
     	//为了减少不必要的代码量 这里自行添加get set方法
    }
    
    
  • 测试使用

    • BaseController对象中注入工具类
          @Autowired
          public ValidatorUtil validatorUtil;
      
    • 测试示例
      	validatorUtil.reqValidata(userModel);
      

自定义校验器(字母大小写)

  • 需求
    校验某个参数自允许存在全大写或者全小写的字母
  • 定义大小写的枚举
    package com.lupf.springboottest.utils.validator.myvalidator;
    
    /**
     * 字母大小写枚举
     */
    public enum CaseMode {
        //大写
        UPPER,
        //小写
        LOWER;
    }
    
  • 定义校验的注解
    package com.lupf.springboottest.utils.validator.myvalidator;
    
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.*;
    
    @Documented
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    //指定校验器
    @Constraint(validatedBy = CaseCheckValidator.class)
    public @interface CaseCheck {
        String message() default "";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        CaseMode value();
    }
    
    
  • 定义校验器
    package com.lupf.springboottest.utils.validator.myvalidator;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.util.regex.Pattern;
    
    /**
     * 字母大小写校验器
     */
    public class CaseCheckValidator implements ConstraintValidator<CaseCheck, String> {
        //大小写的枚举
        private CaseMode caseMode;
    
        @Override
        public void initialize(CaseCheck caseCheck) {
            this.caseMode = caseCheck.value();
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context) {
            //如果文本是空,则不进行校验,因为有其他的注解是可以校验空或者空字符串的
            if (null == value)
                return true;
    
            //文本只能是字母的正则
            String pattern = "^[a-zA-Z]*$";
            //校验传进来的是否是只包含了字母的文本
            boolean isMatch = Pattern.matches(pattern, value);
            //如果存在其他字符则返回校验失败
            if (!isMatch)
                return false;
    
            //如果没有指定方式,则直接返回false
            if (null == caseMode)
                return false;
    
            //判断是否符合大小写条件
            if (caseMode == CaseMode.UPPER) {
                return value.equals(value.toUpperCase());
            } else {
                return value.equals(value.toLowerCase());
            }
        }
    }
    
    
  • 自定义校验的使用
    对应参数加上相关注解即可
        @CaseCheck(value = CaseMode.UPPER, message = "注册方式必须是大写字母")
        private String registerMode;
    

分组校验

  • 使用场景
    例:用户的登录和注册使用的是同一个用户对象,用户对象里面包含了所有用户相关的数据;
    当用户注册的时候,我们就需要校验前端上传的号码、昵称、性别、密码、邮箱等等相关信息
    当登录的时候,就只需要校验用户名和密码,其他的参数都不用校验

  • 定义校验的分组接口

    package com.lupf.springboottest.utils.validator;
    
    /**
     * 验证分组接口
     */
    public class ValidationGroup {
        /**
         * 注册的分组
         */
        public interface Register {
        }
    
        /**
         * 登录的分组
         */
        public interface Login {
        }
    }
    
    
  • 修改用户模型对象

    package com.lupf.springboottest.service.model;
    
    import com.lupf.springboottest.utils.validator.ValidationGroup;
    import com.lupf.springboottest.utils.validator.myvalidator.CaseCheck;
    import com.lupf.springboottest.utils.validator.myvalidator.CaseMode;
    import org.hibernate.validator.constraints.Length;
    import org.hibernate.validator.constraints.URL;
    
    import javax.validation.constraints.*;
    
    /**
     * 用户的业务对象
     */
    public class UserModel {
        private Integer id;
    
        @NotBlank(message = "用户昵称不能为空", groups = ValidationGroup.Register.class)
        private String name;
    
        @NotNull(message = "性别选择不能为空", groups = ValidationGroup.Register.class)
        private Byte sex;
    
        @Max(value = 200, message = "年龄不能大于200岁", groups = ValidationGroup.Register.class)
        @Min(value = 0, message = "年龄不能小于0岁", groups = ValidationGroup.Register.class)
        private Integer age;
    
        @NotBlank(message = "手机号码不能为空", groups = {ValidationGroup.Login.class, ValidationGroup.Register.class})
        @Pattern(regexp = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$", message = "号码格式不正确!", groups = {ValidationGroup.Login.class, ValidationGroup.Register.class})
        private String telphone;
    
        @Email(message = "邮箱格式错误", groups = ValidationGroup.Register.class)
        private String email;
    
        @CaseCheck(value = CaseMode.UPPER, message = "注册方式必须是大写字母", groups = ValidationGroup.Register.class)
        private String registerMode;
    
        private String thirdPartyId;
    
        @URL(message = "头像必须是链接地址", groups = ValidationGroup.Register.class)
        private String avatar;
    
        //这里是定义的用户加密后密码
        //由于DAO层
        //由于DO层用户信息和用户密码是分开的 但是从业务层面来看,这个密码确实是用户一部分,因此在这里的业务对象将用户信息和密码信息合并
        //这个规则是校验明文的
        @Length(min = 6, max = 18, message = "密码长度为6-18个字符", groups = {ValidationGroup.Login.class, ValidationGroup.Register.class})
        private String encrptPassword;
    }
    
    
  • 使用

    • 注册
      validatorUtil.reqValidata(userModel, ValidationGroup.Register.class);
      
      这里注册的时候就只会校验带有ValidationGroup.Register.class的参数
    • 登录
      validatorUtil.reqValidata(userModel, ValidationGroup.Login.class);
      
      登录的时候就只会校验带有ValidationGroup.Login.class的参数
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页