( 五 )、 SpringBoot 中优雅的参数效验 hibernate-validator

 ( 五 )、 SpringBoot 中优雅的参数效验 hibernate-validator

 

 

 

官网:  http://hibernate.org/validator/documentation/

1、Maven依赖:

如果是Springboot1.x spring-boot-starter-web 包里面有 hibernate-validator 包,不需要引用hibernate validator依赖。在 pom.xml 中添加上 spring-boot-starter-web 的依赖即可

如果是Springboot2.x 需要单独引入maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

JSR 303 是Bean验证的规范 ,Hibernate Validator 是该规范的参考实现,它除了实现规范要求的注解外,还额外实现了一些注解。

@Null 被注释的元素必须为 null     
@NotNull 限制必须不为null
@NotEmpty 验证注解的元素值不为 null 且不为空(字符串长度不为0、集合大小不为0)
@NotBlank 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Pattern(value) 限制必须符合指定的正则表达式
@Size(max,min) 限制字符长度必须在 min 到 max 之间(也可以用在集合上)
@Email 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式
@Max(value) 限制必须为一个不大于指定值的数字
@Min(value) 限制必须为一个不小于指定值的数字
@DecimalMax(value) 限制必须为一个不大于指定值的数字
@DecimalMin(value) 限制必须为一个不小于指定值的数字
@AssertFalse 限制必须为false (很少用)
@AssertTrue 限制必须为true (很少用)
@Past 限制必须是一个过去的日期
@Future 限制必须是一个将来的日期
@Digits(integer,fraction) 限制必须为一个小数,且整数部分的位数不能超过 integer,小数部分的位数不能超过 fraction (很少用)
@Length 被注释的字符串的大小必须在指定的范围内
@Size 被注释的list、map在指定的大小范围
@Range 被注释的元素必须在合适的范围内
@URL 被注释的元素必须是有效URL
@Negative 负数
@NegativeOrZero 0或者负数
@Positive 整数
@PositiveOrZero 0或者整数
@Valid 递归的对关联的对象进行校验(对象内部包含另一个对象作为属性,属性上加@Valid,可以验证作为属性的对象内部的验证)

 

2、参数校验模式:

Hibernate Validator有以下两种验证模式:

  • 普通模式(默认是这个模式), 效验完所有的字段然后返回失败。
  • 快速失败返回模式。

快速失败返回模式配置:

@Configuration
public class ValidatorConfiguration {

    /**
     * failFast(true)快速返回失败
     *
     * @return
     */
    @Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .failFast(true)
//                .addProperty( "hibernate.validator.fail_fast", "true" )
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        return validator;
    }
}

 

3、参数验证异常时全局异常处理

验证不通过时,抛出了ConstraintViolationException、BindException、MethodArgumentNotValidException异常,使用全局统一捕获异常处理:

/**
 * @Author dw
 * @ClassName GlobalExceptionHandler
 * @Description 全局异常处理
 * @Date 2022/4/14 22:49
 * @Version 1.0
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * post请求参数校验抛出的异常
     *
     * @param ex
     * @return
     */
    @ExceptionHandler({BindException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class})
    public String resolveMethodArgumentNotValidException(Exception ex) {
        return handlerNotValidException(ex);
    }

    /**
     * 封装参数校验异常
     *
     * @param e 异常
     * @return 封装参数校验异常
     */
    private String handlerNotValidException(Exception e) {
        BindingResult bindingResult = null;
        // 存储字段校验异常内容
        Set<ConstraintViolation<?>> constraintViolations = new HashSet<>();
        if (e instanceof BindException) {
            BindException exception = (BindException) e;
            bindingResult = exception.getBindingResult();
        } else if (e instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            bindingResult = exception.getBindingResult();
        } else if (e instanceof ConstraintViolationException) {
            ConstraintViolationException exception = (ConstraintViolationException) e;
            constraintViolations = exception.getConstraintViolations();
        }
        //通过字段校验后存储校验结果
        Map<String, Object> maps;
        if (Objects.nonNull(bindingResult) && bindingResult.hasErrors()) {
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            maps = new HashMap<>(fieldErrors.size());
            fieldErrors.forEach(error -> maps.put(error.getField(), error.getDefaultMessage()));
        } else if (!CollectionUtils.isEmpty(constraintViolations)) {
            maps = new HashMap<>(constraintViolations.size());
            constraintViolations.forEach(error -> {
                if (error instanceof ConstraintViolationImpl) {
                    ConstraintViolationImpl<?> constraintViolation = (ConstraintViolationImpl<?>) error;
                    maps.put(constraintViolation.getPropertyPath().toString(), constraintViolation.getMessage());
                }
            });
        } else {
            maps = Collections.emptyMap();
        }
        //返回结果
        return maps.toString();
    }
}

 

 

4、参数验证示例

4.1、验证简单对象

@Data
public class Product {
    @NotNull(message = "产品名称不能为空")
    private String productName;

    @NotBlank(message = "产品描述不能为空")
    private String productDes;

    @Range(min = 1, max = 100)
    @NotNull
    private Integer price;
}

Controller

@RestController
public class TestValidateController {
    @PostMapping("validaObject")
    public String validaObject(@RequestBody @Valid Product product) {
        return "验证成功!";
    }
}

测试结果:

 

 4.2、验证单个参数get请求

@RestController
@Validated
public class TestValidateGetController {

    @GetMapping("getTest")
    public String validaObject(@NotNull(message = "产品名字不能为空!") String productName) {
        return "验证成功!";
    }
}

 测试:

 

4.3、@PathVariable 方式的参数验证

@RestController
@Validated
public class TestValidateGetController {

    @GetMapping("getTest/{price}")
    public String validaObject(@PathVariable @Range(min = 1, max = 100) Integer price) {
        return "验证成功!";
    }
}

 测试:

 

4.4、验证嵌套对象

@Data
public class Product {
    @NotNull(message = "产品名称不能为空")
    private String productName;

    @NotBlank(message = "产品描述不能为空")
    private String productDes;

    @Range(min = 1, max = 100)
    @NotNull
    private Integer price;
    
    @NotNull(message = "产品类别不能为空!")
    @Valid
    private Category category; 
}
Category: 
public class Category {
    @NotNull
    private String categoryName;
    
    @NotBlank
    private String categoryCode;
}

Controller

@RestController
public class TestValidatePostController {

    @PostMapping("validaObject")
    public String validaObject(@RequestBody @Valid Product product) {
        return "验证成功!";
    }
}

 

测试1:

测试2:

 

 

4.5、验证集合

注意验证集合类上必须添加 @Validated 才行。

@RestController
@Validated
public class TestValidatePostController {

    @PostMapping("validaList")
    public String validaList(@RequestBody @Valid List<Product> products) {
        return "验证成功!";
    }
}

测试:

5、分组校验:

比如:有这样一种场景,新增用户信息的时候,不需要验证userId(因为系统生成);修改的时候需要验证userId,这时候可用用户到validator的分组验证功能。

定义两个接口groupA、groupB

public interface GroupA {
}

public interface GroupB {
}

验证model:Person 

@Data
public class Person {
    @NotBlank
    @Range(min = 1,max = Integer.MAX_VALUE,message = "必须大于0",groups = {GroupA.class})
    /**用户id*/
    private Integer userId;
    @NotBlank
    @Length(min = 4,max = 20,message = "必须在[4,20]",groups = {GroupB.class})
    /**用户名*/
    private String userName;
    @NotBlank
    @Range(min = 0,max = 100,message = "年龄必须在[0,100]",groups={Default.class})
    /**年龄*/
    private Integer age;
    @Range(min = 0,max = 2,message = "性别必须在[0,2]",groups = {GroupB.class})
    /**性别 0:未知;1:男;2:女*/
    private Integer sex;
}

如上Person所示,3个分组分别验证字段如下:

  • GroupA验证字段userId;
  • GroupB验证字段userName、sex;
  • Default验证字段age(Default是Validator自带的默认分组)

controller验证:

 @RequestMapping("/demo6")
    public void demo6(@Validated({GroupA.class, GroupB.class}) Person p, BindingResult result){
        if(result.hasErrors()){
            List<ObjectError> allErrors = result.getAllErrors();
            for (ObjectError error : allErrors) {
                System.out.println(error);
            }
        }
    }

 

6、自定义校验注解

  • 注解上必须有 @Constraint(validatedBy = {******.class}) 注解标注,validateBy 的值就是校验逻辑的实现类,实现类必须实现接口 ConstraintValidator
  • 自定义注解 必须包含 message ,groups,payload 属性。

1、创建约束注解类

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = { MyValidator.class})
public @interface MyVolidate {

    String message() default "默认的错误信息";

    String name();

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

    Class<? extends Payload>[] payload() default {};
}
注意:message用于显示错误信息这个字段是必须的,groups和payload也是必须的
@Constraint(validatedBy = { MyValidator.class})用来指定处理这个注解逻辑的类

2.创建验证器类

public class MyValidator implements ConstraintValidator<MyVolidate, User> {

    private String name;

    /**
     * 用于初始化注解上的值到这个validator
     * @param constraintAnnotation
     */
    @Override
    public void initialize(MyVolidate  constraintAnnotation) {
        name =constraintAnnotation.name();
    }

    /**
     * 具体的校验逻辑
     * @param value
     * @param context
     * @return
     */
    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        return name ==null || name.equals(value.getName());
    }
}

3. 测试

@PostMapping("/validateTest")
@ResponseBody
public User validateTest(@Valid @MyVolidate(name = "成都",message = "如果效验不通过,显示我") @RequestBody  User user){
    return user;
}

 

7、校验工具类
/**
 * @Author dw
 * @ClassName ValidationUtil
 * @Description 验证工具类
 * @Date 2022/4/14 22:31
 * @Version 1.0
 */
public class ValidationUtil {

    /**
     * 开启快速结束模式 failFast (true)
     */
    private static Validator validator = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory()
            .getValidator();

    /**
     * 校验对象
     *
     * @param t      bean
     * @param groups 校验组
     * @return ValidResult
     */
    public static <T> ValidResult validateBean(T t, Class<?>... groups) {
        ValidResult result = new ValidationUtil().new ValidResult();
        Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
        boolean hasError = violationSet != null && violationSet.size() > 0;
        result.setHasErrors(hasError);
        if (hasError) {
            for (ConstraintViolation<T> violation : violationSet) {
                result.addError(violation.getPropertyPath().toString(), violation.getMessage());
            }
        }
        return result;
    }

    /**
     * 校验bean的某一个属性
     *
     * @param obj          bean
     * @param propertyName 属性名称
     * @return ValidResult
     */
    public static <T> ValidResult validateProperty(T obj, String propertyName) {
        ValidResult result = new ValidationUtil().new ValidResult();
        Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
        boolean hasError = violationSet != null && violationSet.size() > 0;
        result.setHasErrors(hasError);
        if (hasError) {
            for (ConstraintViolation<T> violation : violationSet) {
                result.addError(propertyName, violation.getMessage());
            }
        }
        return result;
    }

    /**
     * 校验结果类
     */
    @Data
    public class ValidResult {

        /**
         * 是否有错误
         */
        private boolean hasErrors;

        /**
         * 错误信息
         */
        private List<ErrorMessage> errors;

        public ValidResult() {
            this.errors = new ArrayList<>();
        }

        public boolean hasErrors() {
            return hasErrors;
        }

        public void setHasErrors(boolean hasErrors) {
            this.hasErrors = hasErrors;
        }

        /**
         * 获取所有验证信息
         *
         * @return 集合形式
         */
        public List<ErrorMessage> getAllErrors() {
            return errors;
        }

        /**
         * 获取所有验证信息
         *
         * @return 字符串形式
         */
        public String getErrors() {
            StringBuilder sb = new StringBuilder();
            for (ErrorMessage error : errors) {
                sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
            }
            return sb.toString();
        }

        public void addError(String propertyName, String message) {
            this.errors.add(new ErrorMessage(propertyName, message));
        }
    }

    @Data
    public class ErrorMessage {

        private String propertyPath;

        private String message;

        public ErrorMessage() {
        }

        public ErrorMessage(String propertyPath, String message) {
            this.propertyPath = propertyPath;
            this.message = message;
        }
    }

    public static void main(String[] args) {
        Product product = new Product();
        product.setProductName("测试!!!");
        ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(product);
        if(validResult.hasErrors()){
            String errors = validResult.getErrors();
            System.out.println(errors);
        }
    }

}

 

 8、自定义注解效验枚举值
8.1、自定义注解
/**
 * @Author dw
 * @ClassName EnumValueValidator
 * @Description 自定义枚举值效验
 * @Date 2022/4/20 23:40
 * @Version 1.0
 */
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValue.Validator.class)
public @interface EnumValue {

    String message() default "{EnumValueValidator's value is invalid}";

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

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

    Class<? extends Enum<?>> enumClass();

    class Validator implements ConstraintValidator<EnumValue, Object> {

        private Class<? extends Enum<?>> enumClass;

        /**
         * 枚举中效验是否合法的方法名字
         */
        private static final String ENUM_METHOD_NAME="isValidValue";


        @Override
        public void initialize(EnumValue enumValue) {
            enumClass=enumValue.enumClass();
        }

        @Override
        public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
            if (value == null) {
                return Boolean.FALSE;
            }
            Class<?> valueClass = value.getClass();
            try {
                Method method = enumClass.getMethod(ENUM_METHOD_NAME, valueClass);
                // 返回结果必须是boolean
                if (!Boolean.TYPE.equals(method.getReturnType()) && !Boolean.class.equals(method.getReturnType())) {
                    return Boolean.FALSE;
                }
                // 必须是静态方法
                if(!Modifier.isStatic(method.getModifiers())) {
                    return Boolean.FALSE;
                }
                // 通过反射调度枚举中的方法进行验证
                Boolean result = (Boolean)method.invoke(null, value);
                return result == null ? false : result;
            } catch (Exception e) {
                e.printStackTrace();
                return Boolean.FALSE;
            }
        }
    }
}

 

8.2、新建一个枚举测试

@Getter
@AllArgsConstructor
public enum ProductTypeEnum {

    BOOK(1, "书籍"),

    CAR(2, "汽车"),

    FOOD(3, "食品");

    private Integer key;

    private String value;

    public static boolean isValidValue(Integer val) {
        for (ProductTypeEnum value : values()) {
            if (value.getKey().equals(val)) {
                return true;
            }
        }
        return false;
    }
}

 

8.3、实体类中使用自定义注解

@Data
public class Product {
    @NotNull(message = "产品名称不能为空")
    private String productName;

    @NotBlank(message = "产品描述不能为空")
    private String productDes;

    @Range(min = 1, max = 100)
    @NotNull
    private Integer price;

    @NotNull(message = "产品类别不能为空!")
    @Valid
    private Category category;

    @EnumValue(enumClass = ProductTypeEnum.class, message = "产品类型枚举值不合法!")
    private Integer projectType;
}

 

8.4 、新建Controller测试

@RestController
@Validated
public class TestValidateEnumController {

    @PostMapping("validaEnum")
    public String validaObject(@RequestBody @Valid Product product) {
        return "验证成功!";
    }
}

 

8.5、测试结果

 

9、@ScriptAssert 注解实现参数个性化校验

@ScriptAssert() 主要用于类注释

 

9.1、以用户注册为例。在用户注册时我们要校验输入密码与确认密码是否一致,和用户名中是否包含特殊字符:

@ScriptAssert(script = "_this.password.equals(_this.rePassword)",lang = "javascript",message = "确认密码与输入密码不一致")

上面是校验对象中password属性与rePassword属性是否相等,equals返回true表示校验通过,返回false表示校验不通过(会返回错误信息)。_this是默认的对象别名,lang="javascript" 表示用java 执行script中的脚本。

9.2、script指定特定的静态方法校验用户名是否包含特殊字符:

@ScriptAssert(script = "com.dw.study.utils.UserValidate.checkUserName(_this.userName)",lang = "javascript",
                message = "用户名不能包含! @ # $ % & * \\ / ? ?特殊符号")
public class UserValidate {public static boolean checkUserName(String userName){
        String[] chars = {"!","@","#","$","%","&","*","\\","/","?","?"};
        for(String c : chars){
            if(userName.contains(c)){
                return false;
            }
        }
        return true;
    }
}

注意script中方法是一个静态的方法,且包含完整的调用路径。校验参数时,会调用CheckUserName这个静态方法,方法参数为这个对象的userName属性。

多个 @ScriptAssert 需要使用 @ScriptAssert.List :

@ScriptAssert.List({
        @ScriptAssert(script = "com.wl.account.dto.RegisterUserDto.checkUserName(_this.userName)",lang = "javascript",
                message = "用户名不能包含! @ # $ % & * \\ / ? ?特殊符号"),
        @ScriptAssert(script = "_this.password.equals(_this.rePassword)",lang = "javascript",message = "确认密码与输入密码不一致")
})
@Data
public class RegisterUserDto {
 
    @NotBlank(message = "用户名不能为空")
    @Length(message = "用户名长度应在3-12位",max = 12,min = 3)
    private String userName;
 
    @NotBlank(message = "密码不能为空")
    @Length(message = "密码长度应在6-15位",max = 15,min = 6)
    private String password;
 
    @NotBlank(message = "确认密码不能为空")
    private String rePassword;
 
    private String mobile;
 
    @Email
    private String email;
 
    public static boolean checkUserName(String userName){
        String[] chars = {"!","@","#","$","%","&","*","\\","/","?","?"};
        for(String c : chars){
            if(userName.contains(c)){
                return false;
            }
        }
        return true;
    }
}

 

 

 

 
 
 

 

posted @ 2020-06-02 00:06  邓维-java  阅读(914)  评论(0编辑  收藏  举报