使用Hibernate-Validator优雅的校验参数_学习-CSDN博客_hibernate validator

文章目录

何为Hibernate-Validator

       在RESTful 的接口服务中,会有各种各样的入参,我们不可能完全不做任何校验就直接进入到业务处理的环节,通常我们会有一个基础的数据验证的机制,待这些验证过程完毕,结果无误后,参数才会进入到正式的业务处理中。而数据验证又分为两种,一种是无业务关联的规则性验证,一种是根据现有数据进行的联动性数据验证(简单来说,参数的合理性,需要查数据库)。而Hibernate-Validator则适合做无业务关联的规则性验证,而这类验证的代码大多是可复用的。

JSR 303和JSR 349
       简单来说,就是Java规定了一套关于验证器的接口。开始的版本是Bean Validation 1.0(JSR-303),然后是Bean Validation 1.1(JSR-349),目前最新版本是Bean Validation 2.0(JSR-380),大概是这样。

       从上可以看出Bean Validation并不是一项技术而是一种规范,需要对其实现。hibernate团队提供了参考实现,Hibernate validator 5是Bean Validation 1.1的实现,Hibernate Validator 6.0是Bean Validation 2.0规范的参考实现。新特性可以到官网查看,笔者最喜欢的两个特性是:跨参数验证(比如密码和确认密码的验证)和在消息中使用EL表达式,其他的还有如方法参数/返回值验证、CDI和依赖注入、分组转换等。对于方法参数/返回值验证,大家可以参阅《hibernate官方文档)

       如果项目的框架是spring boot的话,在依赖spring-boot-starter-web 中已经包含了Hibernate-validator的依赖。Hibernate-Validator的主要使用的方式就是注解的形式,并且是“零配置”的,无需配置也可以使用。下面展示一个最简单的案例。

1. Hibernate-Validator 最基本的使用

  1. 添加一个普通的接口信息,参数是@RequestParam类型的,传入的参数是id,且id不能小于10。
    @RestController
    @RequestMapping("/example")
    @Validated
    public class ExampleController {
    
        /**
         *  用于测试
         * @param id id数不能小于10 @RequestParam类型的参数需要在Controller上增加@Validated
         * @return
         */
        @RequestMapping(value = "/info",method = RequestMethod.GET)
        public String test(@Min(value = 10, message = "id最小只能是10") @RequestParam("id")
                                       Integer id){
            return "恭喜你拿到参数了";
        }
    }
  1. 在全局异常拦截中添加验证异常的处理
/**
 * 统一异常处理类
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        StringBuilder errorInfo = new StringBuilder();
        BindingResult bindingResult = exception.getBindingResult();
        for(int i = 0; i < bindingResult.getFieldErrors().size(); i++){
            if(i > 0){
                errorInfo.append(",");
            }
            FieldError fieldError = bindingResult.getFieldErrors().get(i);
            errorInfo.append(fieldError.getField()).append(" :").append(fieldError.getDefaultMessage());
        }

        //返回BaseResponse
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg(errorInfo.toString());
        response.setCode(DefaultErrorCode.error);
        return response;
    }


    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleConstraintViolationException(ConstraintViolationException exception) {
        StringBuilder errorInfo = new StringBuilder();
        String errorMessage ;

        Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
        for (ConstraintViolation<?> item : violations) {
            errorInfo.append(item.getMessage()).append(",");
        }
        errorMessage = errorInfo.toString().substring(0, errorInfo.toString().length()-1);

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg(errorMessage);
        response.setCode(DefaultErrorCode.error);
        return response;
    }



    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleDefaultException(Exception exception) {

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("其他错误");
        response.setCode(DefaultErrorCode.error);
        return response;
    }
}

如果是方法中参数使用@Min校验字段,则类上需要添加@Validated。如果是使用@Valid校验对象,则不需要类上添加@Validated

2.内置的校验注解

首先列举一下Hibernate-Validator所有的内置验证注解。

  • 常用的

注解

使用

@NotNull

被注释的元素(任何元素)必须不为 null, 集合为空也是可以的。没啥实际意义

@NotEmpty

用来校验字符串、集合、map、数组不能为null或空
(字符串传入空格也不可以)(集合需至少包含一个元素)

@NotBlank

只用来校验字符串不能为null,空格也是被允许的 。校验字符串推荐使用@NotEmpty

-

@Size(max=, min=)

指定的字符串、集合、map、数组长度必须在指定的max和min内
允许元素为null,字符串允许为空格

@Length(min=,max=)

只用来校验字符串,长度必须在指定的max和min内 允许元素为null

@Range(min=,max=)

用来校验数字或字符串的大小必须在指定的min和max内
字符串会转成数字进行比较,如果不是数字校验不通过
允许元素为null

@Min()

校验数字(包括integer short long int 等)的最小值,不支持小数即double和float
允许元素为null

@Max()

校验数字(包括integer short long int 等)的最小值,不支持小数即double和float
允许元素为null

-

@Pattern()

正则表达式匹配,可用来校验年月日格式,是否包含特殊字符(regexp = "^[a-zA-Z0-9\u4e00-\u9fa5

除了@Empty要求字符串不能全是空格,其他的字符串校验都是允许空格的。
message是可以引用常量的,但是如@Size里max不允许引用对象常量,基本类型常量是可以的。
注意大部分规则校验都是允许参数为null,即当不存在这个值时,就不进行校验了

  • 不常用的
@Null 被注释的元素必须为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Email 被注释的元素必须是电子邮箱地址

3. 分组校验、顺序校验、级联校验

       同一个校验规则,不可能适用于所有的业务场景,对每一个业务场景去编写一个校验规则,又显得特别冗余。实际上我们可以用到Hibernate-Validator的分组功能。

添加一个名为Save 、Update 的接口(接口内容是空的)

public @interface Save {
}

public @interface Update {
}

需要分组校验的DTO

  /**
 * 注解@GroupSequence指定分组校验的顺序,即先校验Save分组的,如果不通过就不会去做后面分组的校验了
 */
@Data
@ApiModel("用户添加修改对象")
@GroupSequence({Save.class, Update.class, UserDto.class})
public class UserDto {

    @NotEmpty(message = DefaultErrorCode.ARGUMENTS_MISSING, groups = Update.class)
    @ApiModelProperty(notes = "用户id", example = "2441634686")
    private String id;

    @NotEmpty(message = DefaultErrorCode.ARGUMENTS_MISSING, groups = Save.class)
    @Size(min = 1, max = RestfulConstants.NAME_MAX_LENGTH, message = CountGroupErrorCode.USER_NAME_IS_ILLEGAL)
    @Pattern(regexp = ValidatorConstant.LEGAL_CHARACTER, message = CountGroupErrorCode.USER_NAME_IS_ILLEGAL)
    @ApiModelProperty(notes = "用户姓名", example = "张飞")
    private String name;

    @NotNull(message = DefaultErrorCode.ARGUMENTS_MISSING)
    @Min(value = 0, message = DefaultErrorCode.ARGUMENTS_MISSING, groups = Save.class)
    @ApiModelProperty(notes = "年龄", example = "12")
    private Integer age;

    @ApiModelProperty(notes = "手机号", example = "18108195635")
    @Pattern(regexp = ValidatorConstant.MOBILE)
    private String phone;

    @ApiModelProperty(notes = "出生日期,格式如2018-08-08", example = "2018-08-08")
    private LocalDate birthday;

    @EnumCheck(enumClass = SexEnum.class, message = CountGroupErrorCode.USER_SEX_ILLEGAL)
    @ApiModelProperty(notes = "性别,1-男,2-女,3-未知", example = "2")
    private Integer sex;

    /**
     * 级联校验只需要添加@Valid
     * 注解@ConvertGroup用于分组的转换,只能和@Valid一起使用。(一般用不到)
     */
    @Size(max = RestfulConstants.DIRECTION_MAX_NUMBER, message = CountGroupErrorCode.DIRECTION_NUMBER_IS_ILLEGAL)
    @ApiModelProperty(notes = "包含的方向列表")
    @Valid
    //@ConvertGroup(from = Search.class, to = Update.class)
    private List<DirectionDto> list;

}

校验的接口

    /**
     * 这里的@Validated({Save.class, Default.class}) 其中Default.class是校验注解默认的分组,
     * (也就说明自定义校验注解时要加上)
     */
    @PostMapping(value = "/add")
    @ApiOperation(value = "添加用户")
    public BaseResponse addUser(@Validated({Save.class, Default.class}) @RequestBody UserDto addDto) {
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("添加成功");
        return response;
    }

    @PostMapping(value = "/update")
    @ApiOperation(value = "修改用户")
    public BaseResponse updatedUser(@Validated({Update.class, Default.class}) @RequestBody UserDto updateDto) {
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("修改成功");
        return response;
    }

       使用分组能极大的复用需要验证的类信息。而不是按业务重复编写冗余的类。其中@GroupSequence提供组序列的形式进行顺序式校验,即先校验@Save分组的,如果校验不通过就不进行后续的校验多了。我认为顺序化的校验,场景更多的是在业务处理类,例如联动的属性验证,值的有效性很大程度上不能从代码的枚举或常量类中来校验。

4. 自定义校验注解(枚举)、组合校验注解

       上面这些注解能适应我们绝大多数的验证场景,但是为了应对更多的可能性,我们可以自定义注解来满足验证的需求。

       我们一定会用到这么一个业务场景,vo中的属性必须符合枚举类中的枚举。Hibernate-Validator中还没有关于枚举的验证规则,那么,我们则需要自定义一个枚举的验证注解。

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = EnumCheckValidator.class)
    public @interface EnumCheck {
        /**
         * 是否必填 默认是必填的
         * @return
         */
        boolean required() default true;
        /**
         * 验证失败的消息
         * @return
         */
        String message() default "枚举的验证失败";
        /**
         * 分组的内容
         * @return
         */
        Class<?>[] groups() default {};
    
        /**
         * 错误验证的级别
         * @return
         */
        Class<? extends Payload>[] payload() default {};
    
        /**
         * 枚举的Class
         * @return
         */
        Class<? extends Enum<?>> enumClass();
    
        /**
         * 枚举中的验证方法
         * @return
         */
        String enumMethod() default "validation";
    }

注解的校验类


public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {

    private static final Logger logger = LoggerFactory.getLogger(EnumCheckValidator.class);

    private EnumCheck enumCheck;

    @Override
    public void initialize(EnumCheck enumCheck) {
        this.enumCheck =enumCheck;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        // 注解表明为必选项 则不允许为空,否则可以为空
        if (value == null) {
            return !this.enumCheck.required();
        }

        Boolean result = Boolean.FALSE;
        Class<?> valueClass = value.getClass();
        try {
            //通过反射执行枚举类中validation方法
            Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
            result = (Boolean)method.invoke(null, value);
            if(result == null){
                return false;
            }
        } catch (Exception e) {
            logger.error("custom EnumCheckValidator error", e);
        }
        return result;
    }
}

编写枚举类

    public enum  Sex{
        MAN("男",1),WOMAN("女",2);
    
        private String label;
        private Integer value;
    
        public String getLabel() {
            return label;
        }
    
        public void setLabel(String label) {
            this.label = label;
        }
    
        public Integer getValue() {
            return value;
        }
    
        public void setValue(Integer value) {
            this.value = value;
        }
    
        Sex(String label, int value) {
            this.label = label;
            this.value = value;
        }
    
        /**
         * 判断值是否满足枚举中的value
         * @param value
         * @return
         */
        public static boolean validation(Integer value){
            for(Sex s:Sex.values()){
                if(Objects.equals(s.getValue(),value)){
                    return true;
                }
            }
            return false;
        }
    }

使用方式

    @EnumCheck(message = "只能选男:1或女:2",enumClass = Sex.class)
    private Integer sex;

我们甚至可以在自定义注解中做更加灵活的处理,甚至把与数据库的数据校验的也写成自定义注解,来进行数据验证的调用。

也可以使用组合校验注解。

5. validate如何集成进springboot的国际化处理

       项目开发中,validate返回的错误信息我们也想和springboot处理国际化一样抛出国际化处理过的内容,但是validator和messages的消息配置文件不一致,且处理器看起来也是不一样的,那么接下来分析一下他们的不同和联系。

       首先看validator,其中有一个处理消息的概念:interpolator,即“篡改”,也就是对于消息可进行最终输出前的处理,如占位符替换等,否则将直接输出配置的内容。需要具体的配置加载:org.hibernate.validator.spi.resourceloading.ResourceBundleLocator,作用就是根据国际化环境(en或zh等)加载相应的资源加载器,具体有以下几个:
org.hibernate.validator.resourceloading.AggregateResourceBundleLocator
org.hibernate.validator.resourceloading.CachingResourceBundleLocator
org.hibernate.validator.resourceloading.DelegatingResourceBundleLocator
org.hibernate.validator.resourceloading.PlatformResourceBundleLocator
org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator
       以上几个类(非加粗)实现是基于装饰者模式的,基础的是 PlatformResourceBundleLocator,原理通过name去classpath路径加载对应环境的资源文件,然后从内容里获取key的值,另外几个从名字上也可以看出加强了什么:Aggregate聚合,cache缓存,delegate代理。
可以看到,其实最终起作用的是:java.util.ResourceBundle,即本地资源文件。再看加粗类,它是基于messages中MessageSource的,而这正是和message联系的桥梁。

       然后我们来看专门处理国际化消息的org.springframework.context.MessageSource,它下边的实现有很多,基本是各种不同来源,有基于本地内存的(map)、java.util.ResourceBundle等,与上面validate发生联系的就是基于java.util.ResourceBundle的org.springframework.context.support.ResourceBundleMessageSource,这也是我们经常使用的。

so,到此,两者可以联系起来了,但是两者的默认资源名称是不同的。validator是ValidationMessages,message是messages。
所以,messages可以是validator的某个实现,也就是他俩是有交集的,当且仅当均使用同一个MessageSource,validator使用MessageSourceResourceBundleLocator时,可以通过messageSource进行统一控制。好了,那这样的话,我们统一也就很简单了。

最后,配置起来其实很简单(下文的前提是已经做好国际化的配置使用):

    @Autowired
    private MessageSource messageSource;
    
    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setMessageInterpolator(new MessageInterpolatorFactory().getObject());
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;
    }

以上即可把validate的错误信息加入到国际化的处理中,其中messageSource使用的是默认的,也可以使用自定义的。


原网址: 访问
创建于: 2021-08-24 20:39:52
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论