2023-09-12  阅读(1)
原文作者:一直不懂 原文地址: https://blog.csdn.net/shenchaohao12321/article/details/100163991

1、什么是Bean Validation

JSR-303(JSR是Java Specification Requests的缩写,意思是Java 规范提案) 是JAVA EE 6 中的一项子规范,叫做Bean Validation,Hibernate Validator 是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

meta-data comment version
@Null 对象,为空 BeanValidation1.0
@NotNull 对象,不为空 BeanValidation1.0
@AssertTrue 布尔,为True BeanValidation1.0
@AssertFalse 布尔,为False BeanValidation1.0
@Min(value) 数字,最小为value BeanValidation1.0
@Max(value) 数字,最大为value BeanValidation1.0
@DecimalMin(value) 数字,最小为value BeanValidation1.0
@DecimalMax(value) 数字,最大为value BeanValidation1.0
@Size(max,min) min<=value<=max BeanValidation1.0
@Digits(integer,fraction) 数字,某个范围内 BeanValidation1.0
@Past 日期,过去的日期 BeanValidation1.0
@Future 日期,将来的日期 BeanValidation1.0
@Pattern(value) 字符串,正则校验 BeanValidation1.0
@Email 字符串,邮箱类型 BeanValidation2.0
@NotEmpty 集合,不为空 BeanValidation2.0
@NotBlank 字符串,不为空字符串 BeanValidation2.0
@Positive 数字,正数 BeanValidation2.0
@PositiveOrZero 数字,正数或0 BeanValidation2.0
@Negative 数字,负数 BeanValidation2.0
@NegativeOrZero 数字,负数或0 BeanValidation2.0
@PastOrPresent 过去或者现在 BeanValidation2.0
@FutureOrPresent 将来或者现在 BeanValidation2.0

2、使用Hibernate Validator编程式校验

2.1、简单的实体对象校验

maven依赖:

    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>2.0.0.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.0.1.Final</version>
    </dependency>

一个简单的JavaBean:

    public class User {
        private String name;
        private String gender;
        @Positive
        private int age;
        private List<@Email String> emails;
    
        // getter and setter 
        // ...
    }

使用Validator校验:

    User user = new User();
    user.setName("seven");
    user.setGender("man");
    user.setAge(-1);
    user.setEmails(Arrays.asList("sevenlin@gmail.com", "sevenlin.com"));
    
    Set<ConstraintViolation<User>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(user);
    
    List<String> message
        = result.stream().map(v -> v.getPropertyPath() + " " + v.getMessage() + ": " + v.getInvalidValue())
            .collect(Collectors.toList());
    
    message.forEach(System.out::println);

校验结果:

    emails[1].<list element> must be a well-formed email address: sevenlin.com
    age must be greater than 0: -1

代码讲解:

Validation类是Bean Validation的入口点,buildDefaultValidatorFactory()方法基于默认的Bean Validation提供程序构建并返回ValidatorFactory实例。使用默认验证提供程序解析程序逻辑解析提供程序列表。代码上等同于Validation.byDefaultProvider().configure().buildValidatorFactory()。

以上代码根据java spi查找ValidationProvider的实现类,如果类路径加入了hibernate-validator,则使用HibernateValidator,关于HibernateValidator细节暂不探讨。

之后调用该ValidatorFactory.getValidator()返回一个校验器实例,使用这个校验器的validate方法对目标对象的属性进行校验,返回一个ConstraintViolation集合。ConstraintViolation用于描述约束违规。 此对象公开约束违规上下文以及描述违规的消息。

2.2、分组校验

如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。

首先需要在constraint注解上指定groups属性,这个属性是一个class对象数组,再调用javax.validation.Validator接口的validate方法的时候将第二个参数groups传入class数组元素之一就可以针对这个这个group的校验规则生效。

    public class User {
        private String name;
        private String gender;
    
        @Positive
        @Min(value = 18,groups = {Adult.class})
        private int age;
        private List<@Email String> emails;
    
        // getter and setter 
        // ...
        
        // 一个分组标记
        public interface Adult{}
    }

这样当Validation.buildDefaultValidatorFactory().getValidator().validate(user,User.Adult.class);只会对groups=Adult.class的constraint注解生效。

2.3、自定义校验

业务需求总是比框架提供的这些简单校验要复杂的多,我们可以自定义校验来满足我们的需求。

2.3.1、编写自定义校验注解

我们尝试添加一个“字符串不能包含空格”的限制。

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @Constraint(validatedBy = {CannotHaveBlankValidator.class})<1>
    public @interface CannotHaveBlank {
    
        //默认错误消息
        String message() default "不能包含空格";
    
        //分组
        Class<?>[] groups() default {};
    
        //负载
        Class<? extends Payload>[] payload() default {};
    
        //指定多个时使用
        @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
        @Retention(RUNTIME)
        @Documented
        @interface List {
            CannotHaveBlank[] value();
        }
    
    }

我们不需要关注太多东西,例如payload,List ,groups,都可以忽略。

<1> 自定义注解中指定了这个注解真正的验证者类。

2.3.2、编写真正的校验者类

    public class CannotHaveBlankValidator implements <1> ConstraintValidator<CannotHaveBlank, String> {
    
        @Override
        public void initialize(CannotHaveBlank constraintAnnotation) {
        }
        @Override
        public boolean isValid(String value, ConstraintValidatorContext context <2>) {
            //null时不进行校验
            if (value != null && value.contains(" ")) {
                <3>
                //获取默认提示信息
                String defaultConstraintMessageTemplate = context.getDefaultConstraintMessageTemplate();
                System.out.println("default message :" + defaultConstraintMessageTemplate);
                //禁用默认提示信息
                context.disableDefaultConstraintViolation();
                //设置提示语
                context.buildConstraintViolationWithTemplate("can not contains blank").addConstraintViolation();
                return false;
            }
            return true;
        }
    }

<1> 所有的验证者都需要实现ConstraintValidator接口,它的接口也很形象,包含一个初始化事件方法,和一个判断是否合法的方法。

    public interface ConstraintValidator<A extends Annotation, T> {
    
        void initialize(A constraintAnnotation);
    
        boolean isValid(T value, ConstraintValidatorContext context);
    }

<2> ConstraintValidatorContext 这个上下文包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。

<3> 一些典型校验操作,或许可以对你产生启示作用。

值得注意的一点是,自定义注解可以用在METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER之上,ConstraintValidator的第二个泛型参数T,是需要被校验的类型。

3、Spring Validation

Spring框架为开发者提供了也提供了Validator接口,可用于验证对象。注意Spring Validation的Validator与Bean Validation 的Validator接口是两个接口。Spring的Validator通过使用Errors对象来工作,以便在验证时,验证器可以向Errors对象报告验证失败。

Spring 3开始为其验证支持引入了几项增强功能。 首先,完全支持JSR-303 Bean Validation API。 其次,当以编程方式使用时,Spring的DataBinder可以验证对象以及绑定它们。 第三,Spring MVC支持声明性地验证@Controller输入。

3.1、Validator接口

该接口完全脱离任何基础设施或上下文; 也就是说,它不会仅仅验证Web层,数据访问层或任何层中的对象。 因此,它适用于应用程序的任何层。

    public interface Validator {
    
       //此Validator可以验证提供的clazz的实例吗?
       boolean supports(Class<?> clazz);
    
       //验证提供的目标对象,该对象必须是supports(Class)方法返回true的。
       //提供的错误实例可用于报告任何结果验证错误。
       void validate(Object target, Errors errors);
    
    }

3.2、使用LocalValidatorFactoryBean编程式校验

Spring对validation全面支持JSR-303、JSR-349的标准,并且封装了LocalValidatorFactoryBean作为validator的实现。这个类同时实现了Spring Validation和Bean Validation的Validator接口,代替上述的从工厂方法中获取的hibernate validator。

    public static class ValidPerson {
    
       @NotNull
       private String name;
    
       @Valid
       private ValidAddress address = new ValidAddress();
    
       @Valid
       private List<ValidAddress> addressList = new LinkedList<>();
    
       @Valid
       private Set<ValidAddress> addressSet = new LinkedHashSet<>();
    }
    
    public static class ValidAddress {
    
       @NotNull
       private String street;
    }

@javax.validation.Valid标记用于验证级联的属性,方法参数或方法返回类型。在验证属性,方法参数或方法返回类型时,将验证在对象及其属性上定义的约束。此行为以递归方式应用。

    @Test
    public void testSimpleValidation() {
       LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
       validator.afterPropertiesSet();
    
       ValidPerson person = new ValidPerson();
       //这里使用的是Bean Validation而不是Spring Validation的接口
       Set<ConstraintViolation<ValidPerson>> result = validator.validate(person);
       assertEquals(2, result.size());
       for (ConstraintViolation<ValidPerson> cv : result) {
          String path = cv.getPropertyPath().toString();
          if ("name".equals(path) || "address.street".equals(path)) {
             assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NotNull);
          }
          else {
             fail("Invalid constraint violation with path '" + path + "'");
          }
       }
    }

3.3、LocalValidatorFactoryBean源码分析

3.3.1、类结构图

202309122024192701.png

3.3.2、JSR-303 Validator转化Spring Validator

SpringValidatorAdapter获取JSR-303 javax.validator.Validator并将其公开为Spring org.springframework.validation.Validator同时还公开原始JSR-303 Validator接口本身的适配器。

在SpringValidatorAdapter构造方法可以传入一个javax.validation.Validator赋值给变量targetValidator,当实现javax.validator.Validator接口方法时直接调用targetValidator的同名方法,而实现org.springframework.validation.Validator接口方法也会通过targetValidator对象间接实现功能,如下方法:

    @Override
    public void validate(Object target, Errors errors) {
       if (this.targetValidator != null) {
          processConstraintViolations(this.targetValidator.validate(target), errors);
       }
    }

processConstraintViolations方法处理给定的JSR-303 ConstraintViolations,向提供的Spring Errors对象添加相应的错误。

    protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) {
       for (ConstraintViolation<Object> violation : violations) {
          String field = determineField(violation);
          FieldError fieldError = errors.getFieldError(field);
          if (fieldError == null || !fieldError.isBindingFailure()) {
             try {
                ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
                String errorCode = determineErrorCode(cd);
                Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd);
                if (errors instanceof BindingResult) {
                   // Can do custom FieldError registration with invalid value from ConstraintViolation,
                   // as necessary for Hibernate Validator compatibility (non-indexed set path in field)
                   BindingResult bindingResult = (BindingResult) errors;
                   String nestedField = bindingResult.getNestedPath() + field;
                   if ("".equals(nestedField)) {
                      String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
                      bindingResult.addError(new ObjectError(
                            errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()));
                   }
                   else {
                      Object rejectedValue = getRejectedValue(field, violation, bindingResult);
                      String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
                      bindingResult.addError(new FieldError(
                            errors.getObjectName(), nestedField, rejectedValue, false,
                            errorCodes, errorArgs, violation.getMessage()));
                   }
                }
                else {
                   // got no BindingResult - can only do standard rejectValue call
                   // with automatic extraction of the current field value
                   errors.rejectValue(field, errorCode, errorArgs, violation.getMessage());
                }
             }
             catch (NotReadablePropertyException ex) {
                throw new IllegalStateException("JSR-303 validated property '" + field +
                      "' does not have a corresponding accessor for Spring data binding - " +
                      "check your DataBinder's configuration (bean property versus direct field access)", ex);
             }
          }
       }
    }

3.3.3、LocalValidatorFactoryBean

虽然LocalValidatorFactoryBean继承了SpringValidatorAdapter,但是没有继承它带参的构造方法,默认targetValidator变量为空,对targetValidator变量赋值的操作放在了afterPropertiesSet()方法中。

202309122024197782.png

上图是afterPropertiesSet()方法最后三行,上面都是对configuration的配置。

3.4、Spring MVC使用Validator

默认情况下,如果类路径上存在Bean Validation(例如,Hibernate Validator),则LocalValidatorFactoryBean将注册为全局Validator,以便与控制器方法参数一起使用@Valid和@Validated。

3.4.1、校验参数实体

创建需要被校验的实体类

    public class Foo {
    
        @NotBlank
        private String name;
    
        @Min(18)
        private Integer age;
    
        @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误")
        @NotBlank(message = "手机号码不能为空")
        private String phone;
    
        @Email(message = "邮箱格式错误")
        private String email;
    
        //... getter setter
    
    }

在@Controller中校验数据
springmvc为我们提供了自动封装表单参数的功能,一个添加了参数校验的典型controller如下所示。

    @Controller
    public class FooController {
    
        @RequestMapping("/foo")
        public String foo(@Validated Foo foo <1>, BindingResult bindingResult <2>) {
            if(bindingResult.hasErrors()){
                for (FieldError fieldError : bindingResult.getFieldErrors()) {
                    //...
                }
                return "fail";
            }
            return "success";
        }
    
    }

值得注意的地方:

<1> 参数Foo前需要加上@Validated注解,表明需要spring对其进行校验,而校验的信息会存放到其后的BindingResult中。注意,必须相邻,如果有多个参数需要校验,形式可以如下。foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult);即一个校验类对应一个校验结果。

<2> 校验结果会被自动填充,在controller中可以根据业务逻辑来决定具体的操作,如跳转到错误页面。

3.4.2、基于方法校验

    @RestController
    @Validated <1>
    public class BarController {
    
        @RequestMapping("/bar")
        public @NotBlank <2> String bar(@Min(18) Integer age <3>) {
            System.out.println("age : " + age);
            return "";
        }
    
        @ExceptionHandler(ConstraintViolationException.class)
        public Map handleConstraintViolationException(ConstraintViolationException cve){
            Set<ConstraintViolation<?>> cves = cve.getConstraintViolations();<4>
            for (ConstraintViolation<?> constraintViolation : cves) {
                System.out.println(constraintViolation.getMessage());
            }
            Map map = new HashMap();
            map.put("errorCode",500);
            return map;
        }
    
    }

<1> 为类添加@Validated注解

<2> <3> 校验方法的返回值和入参

<4> 添加一个异常处理器,可以获得没有通过校验的属性相关信息

3.4.3、分组校验

如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。未成年人是不能喝酒的,而在其他场景下我们不做特殊的限制,这个需求如何体现同一个实体,不同的校验规则呢?

改写注解,添加分组:

    Class Foo{
    
        @Min(value = 18,groups = {Adult.class})
        private Integer age;
    
        public interface Adult{}
    
        public interface Minor{}
    }

这样表明,只有在Adult分组下,18岁的限制才会起作用。

Controller层改写:

    @RequestMapping("/drink")
    public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) {
        if(bindingResult.hasErrors()){
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                //...
            }
            return "fail";
        }
        return "success";
    }
    
    @RequestMapping("/live")
    public String live(@Validated Foo foo, BindingResult bindingResult) {
        if(bindingResult.hasErrors()){
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                //...
            }
            return "fail";
        }
        return "success";
    }

drink方法限定需要进行Adult校验,而live方法则不做限制。

3.5、Spring MVC注解原理解析

《Spring MVC参数值的绑定》中分析过,Spring MVC @Controller方法参数会使用HandlerMethodArgumentResolver进行HttpServletRequest参数到实体类的装换也就是参数绑定的过程,其中有一个HandlerMethodArgumentResolver就是ModelAttributeMethodProcessor,解析@ModelAttribute带注释的方法参数,并处理来自@ModelAttribute注释方法的返回值。创建后,通过数据绑定到Servlet请求参数来填充属性。 如果参数使用@javax.validation.Valid或Spring自己的@org.springframework.validation.annotation.Validated注释,则可以应用验证。

从这段文档描述可以知道ModelAttributeMethodProcessor除了可以完成Servlet请求参数填充实体类对象的属性,如果实体对象被@javax.validation.Valid或@org.springframework.validation.annotation.Validated注释,还会对其进行数据校验。

3.5.1、校验原理

什么类型的参数会校验@Validated?来看ModelAttributeMethodProcessor的supportsParameter方法就知道了。

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
       return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
             (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
    }

annotationNotRequired是ModelAttributeMethodProcessor的构造参数,在RequestMappingHandlerAdapter中annotationNotRequired参数是true和false的ModelAttributeMethodProcessor(ServletModelAttributeMethodProcessor继承于ModelAttributeMethodProcessor)都有。

202309122024202063.png

我们看ModelAttributeMethodProcessor的resolveArgument方法。此方法中一部分是参数绑定的过程不是我们分析的重点,下面看参数校验的部分。

    WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    if (binder.getTarget() != null) {
       if (!mavContainer.isBindingDisabled(name)) {
          bindRequestParameters(binder, webRequest);
       }
       validateIfApplicable(binder, parameter);
       if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
          throw new BindException(binder.getBindingResult());
       }
    }
    // Value type adaptation, also covering java.util.Optional
    if (!parameter.getParameterType().isInstance(attribute)) {
       attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
    }
    bindingResult = binder.getBindingResult();

上述代码首先通过binderFactory创建一个WebDataBinder对象将webRequest的参数值绑定到attribute上,然后调用validateIfApplicable方法,参数校验的过程就发生在这个方法中。

    protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
       for (Annotation ann : parameter.getParameterAnnotations()) {
          //检查@javax.validation.Valid,Spring的@Validated以及名称以“Valid”开头的自定义注释。
          Object[] validationHints = determineValidationHints(ann);
          if (validationHints != null) {
             //这里使用DataBinder完成属性校验
             binder.validate(validationHints);
             break;
          }
       }
    }
    @Nullable
    private Object[] determineValidationHints(Annotation ann) {
       Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
       if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
          Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
          if (hints == null) {
             return new Object[0];
          }
          return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
       }
       return null;
    }

我们可以看到validateIfApplicable方法内部使用DataBinder的validate方法完成校验。

    public void validate() {
       Object target = getTarget();
       Assert.state(target != null, "No target to validate");
       BindingResult bindingResult = getBindingResult();
       // Call each validator with the same binding result
       for (Validator validator : getValidators()) {
          validator.validate(target, bindingResult);
       }
    }

这个方法调用getValidators()获取DataBinder内所有的Validator对实体参数进行校验,校验错误信息存放到BindingResult中。

3.5.2、Validator的注册

那么下面有一个疑问,DataBinder的Validator是何时被注入的。在《Spring MVC设计原理》介绍过,RequestMappingHandlerAdapter处理请求时构建创建DataBinder的ServletRequestDataBinderFactory对象的构造参数传入了一个WebBindingInitializer对象,在创建DataBinder的时候会使用WebBindingInitializer来初始化这个DataBinder。

202309122024209404.png

下面是WebBindingInitializer接口的实现ConfigurableWebBindingInitializer,使用自身持有的Validator设置到DataBinder上。

202309122024214785.png

那么上面的问题就转移到了Spring MVC何时实例化的WebBindingInitializer?

《Spring MVC设计原理》介绍过,在开启注解@EnableWebMvc后,为Spring容器注册了一个bean——DelegatingWebMvcConfiguration,它继承于WebMvcConfigurationSupport,内部定义了一个@Bean方法来向Spring注册RequestMappingHandlerAdapter。

202309122024221066.png

getConfigurableWebBindingInitializer方法就是创建上面说的ConfigurableWebBindingInitializer对象的。

202309122024228617.png

mvcValidator()方法返回一个Validator实例,这个实例是OptionalValidatorFactoryBean。

202309122024234098.png

OptionalValidatorFactoryBean继承于LocalValidatorFactoryBean。

参考

https://www.jianshu.com/p/2a495bf5504e

https://blog.csdn.net/u013815546/article/details/77248003


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文