Java 数据校验自动化(Bean Validation)

是什么

后端数据校验(JSR303/JSR-349/JSR-380、javax validation、hibernate validation、spring validation)

后端数据校验如:请求参数不能为null、数值至少为5、email参数符合邮箱地址规则等,通常涉及到上述几种工具,其区别:

  • JSR303/JSR-349/JSR-380、javax validation:都是JSR定义的标准,只定义了规范,不提供实现。三者分别为1.0(2009推出)、1.1(2013推出)、2.0(2019推出)版本。他们规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,位于javax.validation.constraints包下。关于几个版本的演进对比可参阅此 文章
  • hibernate validation是对该规范的实现(不要将hibernate和数据库orm框架联系在一起),他提供了相应的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等,他们位于org.hibernate.validator.constraints包下。除了Hibernate Validation外,还有Apache BVal等实现,但前者用得最多,成为了事实上的标准实现(JSR-380规范制定的负责人就职于Hibernate,水到渠成)。
  • spring对hibernate validation进行了二次封装以方便使用,其在springmvc模块中添加了自动校验并将校验信息封装进了特定的类中、还支持指定groups以进行分组校验。以此可见,下面说到的分组是Spring validation提供的功能

可见,在Spring中使用validation时实际起作用的是Hibernate框架提供的实现。但是,我们在使用时最好用规范,这样可以在不改变代码的情况下选用其他具体实现。

注:上述用于校验的注解称为约束注解,专名为"Constraint",为行文方便,后文用该词。

 

使用

maven依赖

        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>
View Code

 

框架提供的Constraint

javax Validation v2.0规范中定义了22个常用的标记(在 javax.validation.constraints 包下),详见 Hibernate Validation Constraints

包括:@NotNull、@NotBlank、@Email、@Pattern、@AssertTrue、@Min、@DecimalMin、@Negative、@NegativeOrZero、@Size、@Past、@Future、@FutureOrPresent等 。

Hibernate提供了其他注解(在 org.hibernate.validator.constraints 包下),详见 Jakarta Bean Validation Constraints

包括:@CreditCardNumber、@Currency、@DurationMax、@EAN、@ISBN、@Length、@CodePointLength、LuhnCheck、@Mod10Check、@Normalized、@Range、@UniqueElements、@URL 等。

注:@Email、@Size等是在所修饰的参数有值的情况下才起作用的,若参数未传(即参数为null)则不会其作用。

Constraint的使用

在使用上,带有Constraint的Domain Model 通常是一处定义,多处(展示层、业务逻辑层、数据访问层等)使用的,而不用为每层分别定义。

Validator is intended to be used to implement multi-layered data validation, where constraints are expressed in a single place (the annotated domain model) and checked in various different layers of the application.

 

 

使用示例

@AssertTrue:用于修饰方法或属性,要求方法名或属性名必须以 is 开头

//用于修饰方法或属性,要求方法名或属性名必须以 is 开头

//用于方法
@AssertTrue(message = "the value of grant type parameter must be 'clientcredentials'")//校验失败时默认报'grantType只能为true',可以通过message指定自定义消息    
private boolean isGrantTypeRight() {
        return getGrant_type().equals(GrantTypeEnum.clientcredentials);
    }


//用于属性
@AssertTrue    
private boolean isCourseDisabled;
View Code

 

Declaring Constraints

在Bean中的字段上加上@NotNull等约束注解(注解可以放在字段声明上,也可以放在字段的get方法上,在一个类内最好统一),或直接对方法参数或方法返回类型前加上约束注解。

实际上,根据注解作用目标,Constraint分为Bean Cnostrain、Method Constraint两大类。往细看,分为如下几种类型:

Bean Constraints:(详情可参阅 官方文档-Declaring Bean Constraints

field constraints:字段约束,如 @NotNull private String name ,内部会调用Field#get方法去获取字段值来校验。

property constraints:属性约束,如: private String name; @NotNull public String getName(){return name;} ,内部会调用getXXX得到值进行校验。

container element constraints:容器元素约束,如  class Data{ private List<@NotNull @Valid Person> parts = new ArrayList<>();} ,约束元素值非null且会进一步检查每个Person内部字段是否符合constraint。注意此情况要求容器变量须是在一个类里面才会生效。

class constraints:类约束,通常用于类的多个数据间的联合关联验证。如 @ValidPassengerCount public class Car { private int seatCount; private List<Person> passengers; } 约束passenger数不比seat数多; @ScriptAssert(lang = "javascript", alias = "_", script = "_.maxStuNum >= _.studentNames.length") @Data class Room { @Positive private int maxStuNum; @NotNull private List<String> studentNames; } ; 

Method Constraints: (详情可参阅 官方文档-Declaring Method Constraints),这里的Method包括 普通的方法、构造方法,"Validates parameters and return values of methods and constructors"。

parameter constraints:如  public void test(@NotNull String courseId) {...} ; 

return value constraints:如 public @NotNull String getName(); 

 注:

上述Constraint只对实例的字段、属性或方法起作用,对static者的不起作用

几种Constraint之间不是互相排斥的,可以组合使用。

Method Constraints在使用上繁琐些,在实际项目中用得较少;Bean Constraints几乎能满足日常开发中的绝大部分需求,故后文主要介绍Bean Constraints。

  

Constrain Inheritance

指Constraint Declaring的继承,即Constraint的作用在 supertype-subtype 间(父类或父接口 与 子类)的继承。对于Bean Constraints和Method Constraints,继承规则有差异。

Bean Constraints:(详情可参阅 官方文档-Bean Constraints Inheritance

Inheritable:可继承,即父类字段如果被约束注解修饰,则子类的该字段也默认会有该注解;

aggregationable:可聚合,即子类该字段自己的约束注解也会生效,即使父类中该字段也有注解约束。

Method Constraints:(详情可参阅 官方文档-Method Constraints Inheritance

对于普通方法:

parameter constraints:普通方法:只要子类的方法override或implement了父类或父接口的对应方法,父、子类的该方法上的parameter constraints就都是不允许的,validate时会抛ConstraintDeclarationException。示例:

//The Jakarta Bean Validation specification implements the first rule by disallowing parameter constraints on methods which override or implement a method declared in a supertype (superclass or interface).


//如下示例中,Vehicle、RacingCar的drive方法中的constrain都是无效的
public interface Vehicle {

    void drive(@Max(75) int speedInMph);
}

public interface Car {

    void drive(int speedInMph);
}

public class RacingCar implements Car, Vehicle {

    @Override
    public void drive(@Max(55) int speedInMph) {
        //...
    }
}
View Code

return value constraints:同 Bean Constraints,可继承可聚合。

对于构造器方法:构造器不存在override supertype constructors一说,故对于构造器方法的 parameter constraints、return value constraints ,只有自身的constraint declaring会生效

"when validating the parameters or the return value of a constructor invocation only the constraints declared on the constructor itself apply, but never any constraints declared on supertype constructors."

 

可见,存在override或implement时,subtype 和 supertype 间的constraints的关系:

Bean Constraints:前者继承后者的并把自己的也聚合在一起。

Parameter Constraints:对于普通方法,两者的都illegal;对于构造方法,只有前者自己的有效。

Return Value Constraints:对于普通方法,同Bean Constraints;对于构造方法,只有前者自己的有效。

 

 

Validating Constraints

Bean Constraints的启用

通常是针对Controller Handler(即Controller中接收http request的方法)启用参数校验,当然也可以对自己创建的对象启用参数校验。

1 自动校验:可通过@Validated(  org.springframework.validation.annotation.Validated 包下 )或@Valid(  javax.validation.constraints.Valid  包下)完成。

若是对请求体的值(@RequestBody 参数)做验证,则在Controller请求方法的Bean参数前加上@Validated即可(也可用@Valid,但通常用@Validated)。示例:

    @PostMapping("/courses")
    public ApiBaseResp<Boolean> importCourses(@Validated @RequestBody ImportCoursesDto importCoursesDto);
View Code

注:

上述方法中,若请求体参数类型是List、Set等,则会发现不生效。解决:在类上加@Validated、并在List元素前加@Valid(即上面介绍的container element constraint)如 public void resetEduStage(@RequestBody List<@Valid AddOrUpdateEdustageDTO> req) 。

若方法参数不是一个复杂的自定义对象,而是String等简单类型,如  addClass( @Validated @NotEmpty String name) ,此时@NotEmpty等是生效不了的,即使加上了@Validated或@Valid。解决:将@Validated移到类上(@Valid放在类上也无效)。

若是对非请求体值(如@RequestParam 参数)的验证,则将@Validated放在方法所在的类上,示例:

@Validated
@Data
@Configuration
@ConfigurationProperties(prefix = "sensestudy.security.jwt")
public class JwtSettings {

    @NotBlank
    private String tokenIssuer;

    @NotBlank
    private String tokenSigningKey;

    @NotNull
    private Integer tokenExpirationTimeMinutes;

    @NotNull
    private Integer refreshTokenExpireTimeMinutes;

    /***/
    @NotBlank
    private String domainForCookie = "*";

    @Valid
    private Cookie cookie=new Cookie();

}
@Data
class Cookie {
    @NotBlank
    private String domain;
}
View Code

总结:3+1种

在body:

是java bean

不是java bean:是容器、是简单类型

不在body

自动校验是由框架来完成的,其内部原理实际上就是借助下面的Method Constraints 的 ExecutableValidator 来完成的。

2 手动校验:(详情可参阅 官方文档-Validating Bean Constraints

可通过  Validation.buildDefaultValidatorFactory().getValidator()  获取一个Validator来手动启用校验,此法是利用Validator的 validate、validateProperty、validatteValue 方法完成校验。当然可对Validator进行各种个性化配置来实现个性化的validate逻辑。示例:

public class Car {

    @NotNull
    private String manufacturer;

    @NotNull
    @Size(min = 2, max = 14)
    private String licensePlate;

    @Min(2)
    private int seatCount;

    public Car(String manufacturer, String licencePlate, int seatCount) {
        this.manufacturer = manufacturer;
        this.licensePlate = licencePlate;
        this.seatCount = seatCount;
    }

    //getters and setters ...
}


ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

Car car = new Car( null, true );

Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );

assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
View Code 

Method Constraints的启用: 

需要手动校验(详情可参阅 官方文档-Validating Method Constraints),借助 ExecutableValidator 来完成,此法是利用其 validateParameters、validateReturnValue、validateConstructorParameters、validateConstructorReturnValue 方法完成校验,也可进行个性化配置。示例:

package org.hibernate.validator.referenceguide.chapter03.validation;

public class Car {

    public Car(@NotNull String manufacturer) {
        //...
    }

    @ValidRacingCar
    public Car(String manufacturer, String team) {
        //...
    }

    public void drive(@Max(75) int speedInMph) {
        //...
    }

    @Size(min = 1)
    public List<Passenger> getPassengers() {
        //...
        return Collections.emptyList();
    }
}



ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator  executableValidator = factory.getValidator().forExecutables();



Car object = new Car( "Morris" );
Method method = Car.class.getMethod( "drive", int.class );
Object[] parameterValues = { 80 };
Set<ConstraintViolation<Car>> violations = executableValidator.validateParameters(
        object,
        method,
        parameterValues
);

assertEquals( 1, violations.size() );
Class<? extends Annotation> constraintType = violations.iterator()
        .next()
        .getConstraintDescriptor()
        .getAnnotation()
        .annotationType();
assertEquals( Max.class, constraintType );
View Code

原理——Constraint Validation Process:

Hibernate Annotation Processor对于Constraint会进行validate,其步骤是:

constraint annotations are allowed for the type of the annotated element

only non-static fields or methods are annotated with constraint annotations

only non-primitive fields or methods are annotated with @Valid

only such methods are annotated with constraint annotations which are valid JavaBeans getter methods (optionally, see below)

only such annotation types are annotated with constraint annotations which are constraint annotations themselves

definition of dynamic default group sequence with @GroupSequenceProvider is valid

annotation parameter values are meaningful and valid

method parameter constraints in inheritance hierarchies respect the inheritance rules

method return value constraints in inheritance hierarchies respect the inheritance rules
View Code

 

 

Constraint Message

(更多详情可参阅官方文档:https://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-message-interpolation.html#section-message-interpolation

message参数用于指定出错时的提示信息。以@NotNull为例,源码如下:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {

    String message() default "{javax.validation.constraints.NotNull.message}";

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

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

    /**
     * Defines several {@link NotNull} annotations on the same element.
     *
     * @see javax.validation.constraints.NotNull
     */
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        NotNull[] value();
    }
}
View Code

如果是{xxxx}的格式,则可用于本地化,如果找不到,就作为一般性描述。如果没有大括号,就是一般性的描述。

内置的constraint都有默认的{xxxx}说明,例如 {javax.validation.constraints.NotNull.message} 

未指定则会用注解的默认值,如{javax.validation.constraints.NotNull.message};若自己指定,则值(可以用EL表达式):

可以是字面值,如 "name is not present"

可以包含EL表达式,如  @Size(min=0, max=5, message="value ${validatedValue} should be between [{min},{max}]") ,message中可以引用变量:

对于注解自身有的属性的值可以通过 {属性名} 引用。

引用所传的值用 ${validatedValue} 。

message的值中可以包含 {}、${ }:

以 { } 包裹的称为 "message parameter",如 {javax.validation.constraints.NotNull.message}、{min} 。

以 ${ } 包裹的称为 "message expression",如 ${validatedValue} 。

对于message parameter、expression的解析规则如下:优先当做i18n key解析(可嵌套)->找不到则当做message parameter解析->找不到则当做EL表达式解析:

Message descriptors can contain message parameters as well as message expressions which will be resolved during interpolation. Message parameters are string literals enclosed in {}, while message expressions are string literals enclosed in ${}. The following algorithm is applied during method interpolation:

Resolve any message parameters by using them as key for the resource bundle ValidationMessages. If this bundle contains an entry for a given message parameter, that parameter will be replaced in the message with the corresponding value from the bundle. This step will be executed recursively in case the replaced value again contains message parameters. The resource bundle is expected to be provided by the application developer, e.g. by adding a file named ValidationMessages.properties to the classpath. You can also create localized error messages by providing locale specific variations of this bundle, such as ValidationMessages_en_US.properties. By default, the JVM’s default locale (Locale#getDefault()) will be used when looking up messages in the bundle.

Resolve any message parameters by using them as key for a resource bundle containing the standard error messages for the built-in constraints as defined in Appendix B of the Jakarta Bean Validation specification. In the case of Hibernate Validator, this bundle is named org.hibernate.validator.ValidationMessages. If this step triggers a replacement, step 1 is executed again, otherwise step 3 is applied.

Resolve any message parameters by replacing them with the value of the constraint annotation member of the same name. This allows to refer to attribute values of the constraint (e.g. Size#min()) in the error message (e.g. "must be at least ${min}").

Resolve any message expressions by evaluating them as expressions of the Unified Expression Language. See Section 4.1.2, “Interpolation with message expressions” to learn more about the usage of Unified EL in error messages.
Message Descriptor Resolve Algorithm

官方示例:

package org.hibernate.validator.referenceguide.chapter04.complete;

public class Car {

    @NotNull
    private String manufacturer;

    @Size(
            min = 2,
            max = 14,
            message = "The license plate '${validatedValue}' must be between {min} and {max} characters long"
    )
    private String licensePlate;

    @Min(
            value = 2,
            message = "There must be at least {value} seat${value > 1 ? 's' : ''}"
    )
    private int seatCount;

    @DecimalMax(
            value = "350",
            message = "The top speed ${formatter.format('%1$.2f', validatedValue)} is higher " +
                    "than {value}"
    )
    private double topSpeed;

    @DecimalMax(value = "100000", message = "Price must not be higher than ${value}")
    private BigDecimal price;

    public Car(
            String manufacturer,
            String licensePlate,
            int seatCount,
            double topSpeed,
            BigDecimal price) {
        this.manufacturer = manufacturer;
        this.licensePlate = licensePlate;
        this.seatCount = seatCount;
        this.topSpeed = topSpeed;
        this.price = price;
    }

    //getters and setters ...
}
View Code

更多详情可参阅本节首所示的参考链接。

其他:消息的I18n处理,见后文。

 

进阶

  • 嵌套的内部对象的验证(Cascaded Validation):在Person p有个字段List<Car> cars,若在验证p时同时要验证Car的price等字段,可以在cars前加上 @Valid 即可。这可能存在无限循环验证的情况,框架考虑到了此情况,会正确处理。不论是Bean Constraint还是Method Constraint都可用嵌套验证,后者示例:
    //对于如下两个类定义,当Garage构造器执行返回时会校验name属性非空
    ;当调用Garbage的checkCar方法时,其参数car对象的各属性会被进行约束检查
    public class Garage {
    
        @NotNull
        private String name;
    
        @Valid
        public Garage(String name) {
            this.name = name;
        }
    
        public boolean checkCar(@Valid @NotNull Car car) {
            //...
            return false;
        }
    }
    
    public class Car {
    
        @NotNull
        private String manufacturer;
    
        @NotNull
        @Size(min = 2, max = 14)
        private String licensePlate;
    
        public Car(String manufacturer, String licencePlate) {
            this.manufacturer = manufacturer;
            this.licensePlate = licencePlate;
        }
    
        //getters and setters ...
    }
    View Code
  • 分组:在Bean中可以指定字段验证所属的groups、在请求参数中可以指定只对哪种groups进行验证,只会触发相应的groups进行验证;若未指定groups则默认属于组javax.validation.groups.Default。分组的定义是可继承的。更多可参阅(分组)示例:
    //Bean中的定义
        @Min(value = 18, groups = { Adult.class ,Default.class}) // groups限制触发此约束的条件,groups中无元素则默认为Default.class
        private Integer age;
    
    
    //Controller中的验证
        @PostMapping("/foo1")
        public String foo1(@RequestBody @Validated({ Adult.class }) Foo foo1) {
            System.out.println("------- res1 -------");
            return "foo1 done.";
        }
    View Code
  • 分组顺序验证:通过@GroupSequence定义一个组SeqGroup,里面包含若干其他组,则SeqGroup可起顺序验证若干组的作用。示例:
    public interface GroupA {}
     
    public interface GroupB {}
     
    @GroupSequence( { Default.class, GroupA.class, GroupB.class })
    public interface SeqGroup {}
    View Code
  • 改变默认分组:validate时若期望未指定分组时按自定义的分组而非Default分组处理,则只要在类上加上@GroupSequence即可,会将其保护的第一个分组作为默认分组。示例:
    @GroupSequence({ RentalChecks.class, CarChecks.class, RentalCar.class })
    public class RentalCar extends Car {
        @AssertFalse(message = "The car is currently rented out", groups = RentalChecks.class)
        private boolean rented;
    
        public RentalCar(String manufacturer, String licencePlate, int seatCount) {
            super( manufacturer, licencePlate, seatCount );
        }
    
        public boolean isRented() {
            return rented;
        }
    
        public void setRented(boolean rented) {
            this.rented = rented;
        }
    }
    View Code
  • 可以不用在方法中与每个请求参数都对应一个BindingResult,而是拦截Controller异常进行处理:违背约束时会抛MethodArgumentNotValidException异常。示例:
    @Slf4j
    @ControllerAdvice
    class GlobalControllerExceptionHandler {
    
        @ResponseBody
        @ExceptionHandler(Throwable.class)
        public String handleApiBindException(Throwable e) {
            String msg = e.getLocalizedMessage();
    
            log.error("param validation error", e);
    
            if (e instanceof BindException) {
    //            return handleApiBindException((BindException) e);
            }
            if (e instanceof MethodArgumentNotValidException) {
                BindingResult bindingResult2 = ((MethodArgumentNotValidException) e).getBindingResult();
                if (bindingResult2.hasErrors()) {
                    for (FieldError fieldError : bindingResult2.getFieldErrors()) {
                        System.out.println(String.format("%s %s %s %s", fieldError.getCode(), fieldError.getField(),
                                fieldError.getDefaultMessage(), fieldError.getRejectedValue()));
                    }
    
                }
                msg = ((MethodArgumentNotValidException) e).getBindingResult().getFieldErrors().stream()
                        .map(fe -> String.format("'%s'%s", fe.getField(), fe.getDefaultMessage()))
                        .collect(Collectors.joining(","));
            }
            if (e instanceof ConstraintViolationException) {
    //            return handleApiConstraintViolationException((ConstraintViolationException) e);
            }
    
            return msg;
        }
    }
    View Code
  • 手动启用验证:通过javax.validation.ValidatorFactory获取一个Validator然后进行验证。此法支持分组但对部分约束(如@Email)不生效。该Validator可针对整个类或指定部分字段进行验证,还支持指定分组等。此法主要是利用Validator的 validate、validateProperty、validatteValue 方法。 
    1         Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
    2         validator.validate(new Foo(), Default.class).forEach(e -> {
    3             System.err.println(e.getPropertyPath() + " " + e.getMessage());//+ " " + e.getInvalidValue() 
    4         });
    View Code 
  • 自定义注解:(详见 官方文档-custom constraints),包括 simple constraint、class-level constraint、cross-parameter constraint、composite constraint 等。

    • 定义自定义注解,该注解须被@Constraint修饰以指定该自定义注解对应的注解处理器
      package com.marchon.learning.validation.custom_annotation;
      
      import java.lang.annotation.Documented;
      import java.lang.annotation.ElementType;
      import java.lang.annotation.Retention;
      import java.lang.annotation.RetentionPolicy;
      import java.lang.annotation.Target;
      
      import javax.validation.Constraint;
      import javax.validation.Payload;
      
      @Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
      @Retention(RetentionPolicy.RUNTIME)
      @Constraint(validatedBy = CheckCaseValidator.class)
      @Documented
      public @interface CheckCase {
      
          // @Constraint要求必须有以下三个方法
          String message() default "'${validatedValue}' not {caseMode} case";// "com.marchon.learning.validation.constraintts.checkcase";
      
          Class<?>[] groups() default {};
      
          Class<? extends Payload>[] payload() default {};
      
          // 以下方法为其他自定义方法
          CaseMode caseMode();
      
          public enum CaseMode {
              UPPER, LOWER
          }
      }
      CheckCase
    • 定义自定义注解的处理器,该处理器须实现ConstraintValidator接口
      package com.marchon.learning.validation.custom_annotation;
      
      import javax.validation.ConstraintValidator;
      import javax.validation.ConstraintValidatorContext;
      
      import com.marchon.learning.validation.custom_annotation.CheckCase.CaseMode;
      
      public class CheckCaseValidator implements ConstraintValidator<CheckCase, String> {//两参数分别为注解类型、注解作用目标的属性类型
      
          private CaseMode caseMode;
      
          @Override
          public void initialize(CheckCase constraintAnnotation) {
              this.caseMode = constraintAnnotation.caseMode();
          }
      
          @Override
          public boolean isValid(String value, ConstraintValidatorContext constraintContext) {
      
              if (null == value)
                  return true;
      
              if (caseMode == CaseMode.UPPER)
                  return value.equals(value.toUpperCase());
              else
                  return value.equals(value.toLowerCase());
          }
      
      
      //        if ( !isValid ) {
      //            constraintContext.disableDefaultConstraintViolation();
      //            constraintContext.buildConstraintViolationWithTemplate(                  "{org.hibernate.validator.referenceguide.chapter06." +"constraintvalidatorcontext.CheckCase.message}").addConstraintViolation();
      //        }
      
      }
      CheckCaseValidator
    • 使用示例
          @CheckCase(caseMode = CaseMode.UPPER)
          @NotBlank
          private String name;
      View Code
    • 当校验时涉及多个属性关联校验时,上述注解满足不了需求,此时可用更复杂的自定义注解,如 class-level constraint,示例:
      @Target({ TYPE, ANNOTATION_TYPE })
      @Retention(RUNTIME)
      @Constraint(validatedBy = { ValidPassengerCountValidator.class })
      @Documented
      public @interface ValidPassengerCount {
      
          String message() default "{org.hibernate.validator.referenceguide.chapter06.classlevel." +
                  "ValidPassengerCount.message}";
      
          Class<?>[] groups() default { };
      
          Class<? extends Payload>[] payload() default { };
      }
      
      public class ValidPassengerCountValidator
              implements ConstraintValidator<ValidPassengerCount, Car> {
      
          @Override
          public void initialize(ValidPassengerCount constraintAnnotation) {
          }
      
          @Override
          public boolean isValid(Car car, ConstraintValidatorContext constraintValidatorContext) {
              if ( car == null ) {
                  return true;
              }
      
              boolean isValid = car.getPassengers().size() <= car.getSeatCount();
      
              if ( !isValid ) {
                  constraintValidatorContext.disableDefaultConstraintViolation();
                  constraintValidatorContext
                          .buildConstraintViolationWithTemplate( "{my.custom.template}" )
                          .addPropertyNode( "passengers" ).addConstraintViolation();
              }
      
              return isValid;
          }
      }
      ValidPassengerCount
    • 上述自定义注解属于Bean Constraint,对于涉及到一个方法多个参数间关联校验的场景,可以用 cross-parameter constraint,示例:
      @Constraint(validatedBy = ConsistentDateParametersValidator.class)
      @Target({ METHOD, CONSTRUCTOR, ANNOTATION_TYPE })
      @Retention(RUNTIME)
      @Documented
      public @interface ConsistentDateParameters {
      
          String message() default "{org.hibernate.validator.referenceguide.chapter04." +
                  "crossparameter.ConsistentDateParameters.message}";
      
          Class<?>[] groups() default { };
      
          Class<? extends Payload>[] payload() default { };
      }
      
      
      
      @SupportedValidationTarget(ValidationTarget.PARAMETERS)//自定义Validator须有此注解
      public class ConsistentDateParametersValidator implements
              ConstraintValidator<ConsistentDateParameters, Object[]> {
      
          @Override
          public void initialize(ConsistentDateParameters constraintAnnotation) {
          }
      
          @Override
          public boolean isValid(Object[] value, ConstraintValidatorContext context) {
              if ( value.length != 2 ) {
                  throw new IllegalArgumentException( "Illegal method signature" );
              }
      
              //leave null-checking to @NotNull on individual parameters
              if ( value[0] == null || value[1] == null ) {
                  return true;
              }
      
              if ( !( value[0] instanceof Date ) || !( value[1] instanceof Date ) ) {
                  throw new IllegalArgumentException(
                          "Illegal method signature, expected two " +
                                  "parameters of type Date."
                  );
              }
      
              return ( (Date) value[0] ).before( (Date) value[1] );
          }
      }
      ConsistentDateParameters
    • 注解组合:一个属性上有多个Constraint时显得繁杂,可自定义新Constraint来表示这些注解组合:将原constaints加在自定义注解定义上即可,示例:
      @NotNull
      @Size(min = 2, max = 14)
      @CheckCase(CaseMode.UPPER)
      @Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE })
      @Retention(RUNTIME)
      @Constraint(validatedBy = { }) //可不指定注解处理器,则会用上面NotNull、CheckCase等各自的处理器处理
      @Documented
      //@ReportAsSingleViolation //可通过此注解将违背约束时上述注解的各ConstraintViolation组合为一个
      public @interface ValidLicensePlate {
      
          String message() default "{org.hibernate.validator.referenceguide.chapter06." +
                  "constraintcomposition.ValidLicensePlate.message}";
      
          Class<?>[] groups() default { };
      
          Class<? extends Payload>[] payload() default { };
      }
      
      
      
      
      public class Car {
      
          @ValidLicensePlate
          private String licensePlate;
      
          //...
      }
      ValidLicensePlate
  • 消息的i18n处理

例如:通过表格文件批量添加账号时,后端对数据进行校验,要将不符合的字段返回给前端弹窗提示给用户,文件模板有多语言版本,此时返回的字段名须是对应语言的表格列名

1 (不通用)对于自己指定消息的场景:若要进行消息国际化显示,则可通过LocalValidatorFactoryBean指定消息配置源,如:

@Configuration
public class ValidationConfig{

    @Value(value = "${spring.messages.basename}")
    private String basename;

    @Bean(name = "messageSource")
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        String[] basenames=basename.split(",");
        messageSource.setBasenames(basenames);
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }


    @Bean
    public LocalValidatorFactoryBean getValidator() {
        LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
        bean.setValidationMessageSource(messageSource());
            return bean;
    }
}
View Code

此方法的缺陷有:消息只能是字面值,当消息中涉及到EL表达式时无法解析成对应的值;消息是根据服务端JVM的默认Locale显示的,而不是根据request指定的语言。

2 (不通用)不自己指定消息的场景:实际使用中,对于注解中的message参数通常不进行指定,此时会使用框架的默认值,如上面的 {javax.validation.constraints.NotNull.message} ,且框架通常对该默认消息会提供多种主流语言的i18n表示。框架如何确定用哪种语言的消息?

框架解析message默认值的相关逻辑在 org.hibernate.validator.internal.engine.ValidationContext 中,相关代码:

private String interpolate(String messageTemplate,
            Object validatedValue,
            ConstraintDescriptor<?> descriptor,
            Map<String, Object> messageParameters,
            Map<String, Object> expressionVariables) {
        MessageInterpolatorContext context = new MessageInterpolatorContext(
                descriptor,
                validatedValue,
                getRootBeanClass(),
                messageParameters,
                expressionVariables
        );

        try {
            return validatorScopedContext.getMessageInterpolator().interpolate(
                    messageTemplate,
                    context
            );//此句为关键所在
        }
        catch (ValidationException ve) {
            throw ve;
        }
        catch (Exception e) {
            throw LOG.getExceptionOccurredDuringMessageInterpolationException( e );
        }
    }

ValidationContext#interpolate
ValidationContext#interpolate

通过 validatorScopedContext.getMessageInterpolator().interpolate( messageTemplate, context ) 可知是获取MessageInterpolator实例并调用其 interpolate(messageTemplate, context) 方法。

因此关键在 javax.validation.MessageInterpolator 接口的两个 interpolate 方法中。validatorScopedContext调的是第一个interpolate方法。该接口源码如下:

public interface MessageInterpolator {

    /**
     * Interpolates the message template based on the constraint validation context.
     * <p>
     * The locale is defaulted according to the {@code MessageInterpolator}
     * implementation. See the implementation documentation for more detail.
     *
     * @param messageTemplate the message to interpolate
     * @param context contextual information related to the interpolation
     *
     * @return interpolated error message
     */
    String interpolate(String messageTemplate, Context context);

    /**
     * Interpolates the message template based on the constraint validation context.
     * The {@code Locale} used is provided as a parameter.
     *
     * @param messageTemplate the message to interpolate
     * @param context contextual information related to the interpolation
     * @param locale the locale targeted for the message
     *
     * @return interpolated error message
     */
    String interpolate(String messageTemplate, Context context,  Locale locale);

    /**
     * Information related to the interpolation context.
     */
    interface Context {
        /**
         * @return {@link ConstraintDescriptor} corresponding to the constraint being
         * validated
         */
        ConstraintDescriptor<?> getConstraintDescriptor();

        /**
         * @return value being validated
         */
        Object getValidatedValue();

        /**
         * Returns an instance of the specified type allowing access to
         * provider-specific APIs. If the Bean Validation provider
         * implementation does not support the specified class,
         * {@link ValidationException} is thrown.
         *
         * @param type the class of the object to be returned
         * @param <T> the type of the object to be returned
         * @return an instance of the specified class
         * @throws ValidationException if the provider does not support the call
         *
         * @since 1.1
         */
        <T> T unwrap(Class<T> type);
    }
}
javax.validation.MessageInterpolator

在Hibernate中,该接口的默认实现类有 AbstractMessageInterpolator、ResourceBundleMessageInterpolator、ParameterMessageInterpolator,后两者继承于第一个。接口的两个interpolate方法在第一个实现类中做了实现。

相关代码如下: 

    @Override
    public String interpolate(String message, Context context) {
        // probably no need for caching, but it could be done by parameters since the map
        // is immutable and uniquely built per Validation definition, the comparison has to be based on == and not equals though
        String interpolatedMessage = message;
        try {
            interpolatedMessage = interpolateMessage( message, context, defaultLocale );
        }
        catch (MessageDescriptorFormatException e) {
            LOG.warn( e.getMessage() );
        }
        return interpolatedMessage;
    }

    @Override
    public String interpolate(String message, Context context, Locale locale) {
        String interpolatedMessage = message;
        try {
            interpolatedMessage = interpolateMessage( message, context, locale );
        }
        catch (ValidationException e) {
            LOG.warn( e.getMessage() );
        }
        return interpolatedMessage;
    }



    public AbstractMessageInterpolator(ResourceBundleLocator userResourceBundleLocator,
            ResourceBundleLocator contributorResourceBundleLocator,
            boolean cacheMessages) {
        defaultLocale = Locale.getDefault();//此句为关键

        if ( userResourceBundleLocator == null ) {
            this.userResourceBundleLocator = new PlatformResourceBundleLocator( USER_VALIDATION_MESSAGES );
        }
        else {
            this.userResourceBundleLocator = userResourceBundleLocator;
        }

        if ( contributorResourceBundleLocator == null ) {
            this.contributorResourceBundleLocator = new PlatformResourceBundleLocator(
                    CONTRIBUTOR_VALIDATION_MESSAGES,
                    null,
                    true
            );
        }
        else {
            this.contributorResourceBundleLocator = contributorResourceBundleLocator;
        }

        this.defaultResourceBundleLocator = new PlatformResourceBundleLocator( DEFAULT_VALIDATION_MESSAGES );

        this.cachingEnabled = cacheMessages;
        if ( cachingEnabled ) {
            this.resolvedMessages = new ConcurrentReferenceHashMap<LocalizedMessage, String>(
                    DEFAULT_INITIAL_CAPACITY,
                    DEFAULT_LOAD_FACTOR,
                    DEFAULT_CONCURRENCY_LEVEL,
                    SOFT,
                    SOFT,
                    EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
            );
            this.tokenizedParameterMessages = new ConcurrentReferenceHashMap<String, List<Token>>(
                    DEFAULT_INITIAL_CAPACITY,
                    DEFAULT_LOAD_FACTOR,
                    DEFAULT_CONCURRENCY_LEVEL,
                    SOFT,
                    SOFT,
                    EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
            );
            this.tokenizedELMessages = new ConcurrentReferenceHashMap<String, List<Token>>(
                    DEFAULT_INITIAL_CAPACITY,
                    DEFAULT_LOAD_FACTOR,
                    DEFAULT_CONCURRENCY_LEVEL,
                    SOFT,
                    SOFT,
                    EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
            );
        }
        else {
            resolvedMessages = null;
            tokenizedParameterMessages = null;
            tokenizedELMessages = null;
        }
    }
AbstractMessageInterpolator#interpolate

不管用的是ResourceBundleMessageInterpolator还是ParameterMessageInterpolator(未显式指定则默认用前者),interpolate方法逻辑都继承自AbstractMessageInterpolator。

可见,由于框架调用的是第一个interpolate方法,故始终会根据JVM的Locale来获取对应的i18n消息(源码中的 defaultLocale = Locale.getDefault() ),显然,这不符合实际需求,那么,如何能根据前端所传的语言环境获取对应的i18n消息呢?

一种可能的方案是自定义一个 MessageInterpolator 实现并改写其第一个interpolate方法即可,相关代码:

 1 /**
 2  * 注册 {@link javax.validation.Validator}
 3  * 实例到Spring容器,使得其他地方可直接通过Autowired使用,并对该Validator进行配置使得返回的{@link javax.validation.ConstraintViolation }消息自动替换成i18n的。<br>
 4  */
 5 @Configuration
 6 public class I18nConstrainValidator {
 7 
 8     @Bean
 9     public Validator validator() {
10         return Validation.byDefaultProvider().configure().messageInterpolator(new ParameterMessageInterpolator() {
11 
12             @Override
13             public String interpolate(String message, Context context) {
14                 return interpolate(message, context, Locale.getDefault());
15             }
16 
17             @Override
18             public String interpolate(String message, Context context, Locale locale) {
19 
20                 // 获取当前请求所指定的语言对应的Locale
21                 Locale requestLocale = I18nUtil.getLocaleFromCurrentRequest();
22                 if (null == requestLocale) {
23                     requestLocale = locale;
24                 }
25 
26                 return super.interpolate(message, context, requestLocale);
27             }
28 
29         }).buildValidatorFactory().getValidator();
30     }
31 
32 }
I18nConstraintValidator

此方案的缺陷:框架提供的message的语种有限,当request指定了一种框架中不存在的语种时无法得到准确的对应语种的消息而是得到JVM Locale对应的消息。分析:

AbstractMessageInterpolator的上述interpolate方法最终会调用到其  resolveMessage(String message, Locale locale) 方法,该方法的主要逻辑如下:

private String resolveMessage(String message, Locale locale) {
        String resolvedMessage = message;

        ResourceBundle userResourceBundle = userResourceBundleLocator
                .getResourceBundle( locale );

        ResourceBundle constraintContributorResourceBundle = contributorResourceBundleLocator
                .getResourceBundle( locale );

        ResourceBundle defaultResourceBundle = defaultResourceBundleLocator
                .getResourceBundle( locale );

        String userBundleResolvedMessage;
        boolean evaluatedDefaultBundleOnce = false;
        do {
            // search the user bundle recursive (step 1.1)
            userBundleResolvedMessage = interpolateBundleMessage(
                    resolvedMessage, userResourceBundle, locale, true
            );

            // search the constraint contributor bundle recursive (only if the user did not define a message)
            if ( !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) {
                userBundleResolvedMessage = interpolateBundleMessage(
                        resolvedMessage, constraintContributorResourceBundle, locale, true
                );
            }

            // exit condition - we have at least tried to validate against the default bundle and there was no
            // further replacements
            if ( evaluatedDefaultBundleOnce && !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) {
                break;
            }

            // search the default bundle non recursive (step 1.2)
            resolvedMessage = interpolateBundleMessage(
                    userBundleResolvedMessage,
                    defaultResourceBundle,
                    locale,
                    false
            );
            evaluatedDefaultBundleOnce = true;
        } while ( true );

        return resolvedMessage;
    }
AbstractMessageInterpolator#resolveMessage
    /**
     * The name of the default message bundle.
     */
    private static final String DEFAULT_VALIDATION_MESSAGES = "org.hibernate.validator.ValidationMessages";

    /**
     * The name of the user-provided message bundle as defined in the specification.
     */
    public static final String USER_VALIDATION_MESSAGES = "ValidationMessages";

    /**
     * Default name of the message bundle defined by a constraint definition contributor.
     *
     * @since 5.2
     */
    public static final String CONTRIBUTOR_VALIDATION_MESSAGES = "ContributorValidationMessages";

public AbstractMessageInterpolator(ResourceBundleLocator userResourceBundleLocator,
            ResourceBundleLocator contributorResourceBundleLocator,
            boolean cacheMessages) {
        defaultLocale = Locale.getDefault();

        if ( userResourceBundleLocator == null ) {
            this.userResourceBundleLocator = new PlatformResourceBundleLocator( USER_VALIDATION_MESSAGES );
        }
        else {
            this.userResourceBundleLocator = userResourceBundleLocator;
        }

        if ( contributorResourceBundleLocator == null ) {
            this.contributorResourceBundleLocator = new PlatformResourceBundleLocator(
                    CONTRIBUTOR_VALIDATION_MESSAGES,
                    null,
                    true
            );
        }
        else {
            this.contributorResourceBundleLocator = contributorResourceBundleLocator;
        }

        this.defaultResourceBundleLocator = new PlatformResourceBundleLocator( DEFAULT_VALIDATION_MESSAGES );

        this.cachingEnabled = cacheMessages;
        if ( cachingEnabled ) {
            this.resolvedMessages = new ConcurrentReferenceHashMap<LocalizedMessage, String>(
                    DEFAULT_INITIAL_CAPACITY,
                    DEFAULT_LOAD_FACTOR,
                    DEFAULT_CONCURRENCY_LEVEL,
                    SOFT,
                    SOFT,
                    EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
            );
            this.tokenizedParameterMessages = new ConcurrentReferenceHashMap<String, List<Token>>(
                    DEFAULT_INITIAL_CAPACITY,
                    DEFAULT_LOAD_FACTOR,
                    DEFAULT_CONCURRENCY_LEVEL,
                    SOFT,
                    SOFT,
                    EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
            );
            this.tokenizedELMessages = new ConcurrentReferenceHashMap<String, List<Token>>(
                    DEFAULT_INITIAL_CAPACITY,
                    DEFAULT_LOAD_FACTOR,
                    DEFAULT_CONCURRENCY_LEVEL,
                    SOFT,
                    SOFT,
                    EnumSet.noneOf( ConcurrentReferenceHashMap.Option.class )
            );
        }
        else {
            resolvedMessages = null;
            tokenizedParameterMessages = null;
            tokenizedELMessages = null;
        }
    }
AbstractMessageInterpolator Constructor

从AbstractMessageInterpolator的构造方法及上述代码可知,注解的默认message {javax.validation.constraints.NotNull.message} 会依次从userResourceBundle、constraintContributorResourceBundle、defaultResourceBundle中解析出对应值,而这三者分别是框架提供的对应语种的i18n文件中的内容的包装类,默认情况下前两者内容为空,故最终是由第三种Bundle中提取到值。

那为何request指定的语种不存在时会返回JVM Locale对应的消息呢?关键在于  ResourceBundle defaultResourceBundle = defaultResourceBundleLocator .getResourceBundle( locale ); 这句,跟踪进去发现其最终调用到了java.util.ResourceBundle的getBundleImpl方法,相关代码如下:

   private static ResourceBundle getBundleImpl(String baseName, Locale locale,
                                                ClassLoader loader, Control control) {
        if (locale == null || control == null) {
            throw new NullPointerException();
        }

        // We create a CacheKey here for use by this call. The base
        // name and loader will never change during the bundle loading
        // process. We have to make sure that the locale is set before
        // using it as a cache key.
        CacheKey cacheKey = new CacheKey(baseName, locale, loader);
        ResourceBundle bundle = null;

        // Quick lookup of the cache.
        BundleReference bundleRef = cacheList.get(cacheKey);
        if (bundleRef != null) {
            bundle = bundleRef.get();
            bundleRef = null;
        }

        // If this bundle and all of its parents are valid (not expired),
        // then return this bundle. If any of the bundles is expired, we
        // don't call control.needsReload here but instead drop into the
        // complete loading process below.
        if (isValidBundle(bundle) && hasValidParentChain(bundle)) {
            return bundle;
        }

        // No valid bundle was found in the cache, so we need to load the
        // resource bundle and its parents.

        boolean isKnownControl = (control == Control.INSTANCE) ||
                                   (control instanceof SingleFormatControl);
        List<String> formats = control.getFormats(baseName);
        if (!isKnownControl && !checkList(formats)) {
            throw new IllegalArgumentException("Invalid Control: getFormats");
        }

        ResourceBundle baseBundle = null;
        for (Locale targetLocale = locale;
             targetLocale != null;

             targetLocale = control.getFallbackLocale(baseName, targetLocale)//此句是关键
) {
            List<Locale> candidateLocales = control.getCandidateLocales(baseName, targetLocale);
            if (!isKnownControl && !checkList(candidateLocales)) {
                throw new IllegalArgumentException("Invalid Control: getCandidateLocales");
            }

            bundle = findBundle(cacheKey, candidateLocales, formats, 0, control, baseBundle);

            // If the loaded bundle is the base bundle and exactly for the
            // requested locale or the only candidate locale, then take the
            // bundle as the resulting one. If the loaded bundle is the base
            // bundle, it's put on hold until we finish processing all
            // fallback locales.
            if (isValidBundle(bundle)) {
                boolean isBaseBundle = Locale.ROOT.equals(bundle.locale);
                if (!isBaseBundle || bundle.locale.equals(locale)
                    || (candidateLocales.size() == 1
                        && bundle.locale.equals(candidateLocales.get(0)))) {
                    break;
                }

                // If the base bundle has been loaded, keep the reference in
                // baseBundle so that we can avoid any redundant loading in case
                // the control specify not to cache bundles.
                if (isBaseBundle && baseBundle == null) {
                    baseBundle = bundle;
                }
            }
        }

        if (bundle == null) {
            if (baseBundle == null) {
                throwMissingResourceException(baseName, locale, cacheKey.getCause());
            }
            bundle = baseBundle;
        }

        keepAlive(loader);
        return bundle;
    }


        public Locale getFallbackLocale(String baseName, Locale locale) {
            if (baseName == null) {
                throw new NullPointerException();
            }
            Locale defaultLocale = Locale.getDefault();
            return locale.equals(defaultLocale) ? null : defaultLocale;
        }
ResourceBundle#getBundleImpl #getFallbackLocale

可见,在语种不存在时会获取Locale.getDefault()对应的Locale,也即JVM的Locale。

3 (通用)终极方案:经过上述两种方案分析可以得到最终的方案(有多种最终方案,这里介绍一种),只要在第二种方案的基础上进一步提供指向你项目里i18n文件路径的UserResourceBundleLocator即可。配置如下:

/**
 * 注册 {@link javax.validation.Validator}
 * 实例到Spring容器,使得其他地方可直接通过Autowired使用,并对该Validator进行配置使得返回的{@link javax.validation.ConstraintViolation }消息自动替换成i18n的。<br>
 */
@Slf4j
@Configuration
public class ConstrainValidatorConfig {

    @Value("${spring.messages.basename}")
    private String[] baseNames;

    @Bean
    public Validator validator() {
        return Validation.byDefaultProvider().configure().messageInterpolator(new RequesLocaleAwareMessageInterpolator(

                // 提供AggregateResourceBundleLocator使得除了用框架提供的Validation ConstrainViolation
                // Message外,还可以用自己指定的或覆盖框架提供的。
                new AggregateResourceBundleLocator(Arrays.asList(baseNames))

        )).buildValidatorFactory().getValidator();
    }

    /**
     * 自定义ResourceBundleMessageInterpolator的若干方法,使得可根据request指定的语言返回对应语种的Validation
     * ConstrainViolation Message
     */
    public static class RequesLocaleAwareMessageInterpolator extends ResourceBundleMessageInterpolator {
        public RequesLocaleAwareMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) {
            super(userResourceBundleLocator);
        }

        @Override
        public String interpolate(String message, Context context) {
            return interpolate(message, context, Locale.getDefault());
        }

        @Override
        public String interpolate(String message, Context context, Locale locale) {

            // 获取当前请求所指定的语言对应的Locale
            Locale requestLocale = I18nUtil.getLocaleFromCurrentRequest();
            log.debug("locale for javax.validation.Validator resolved: {}", requestLocale);

            if (null == requestLocale) {
                requestLocale = locale;
            }

            return super.interpolate(message, context, requestLocale);
        }

//        @Override
//        public String interpolate(Context context, Locale locale, String term) {
//            
//            // 获取当前请求所指定的语言对应的Locale
//            Locale requestLocale = I18nUtil.getLocaleFromCurrentRequest();
//            if (null == requestLocale) {
//                requestLocale = locale;
//            }
//
//            return super.interpolate(context, requestLocale, term);
//        }

    }

}
ConstraintValidatorConfig

在这种方案下:

若注解的message未指定——即用的是框架默认值(如  {javax.validation.constraints.Size.message} ),则对于框架未提供的i18n语种(如 zh_CHS),在你自己项目的i18n文件里补充相应值即可(如在messages_zh_CHS.properties文件里增加  javax.validation.constraints.Size.message = 长度不能超过{max}  );

若注解的message不用默认值,而是自己指定message,如  message="{ss.constraints.Size.message.name}" ,则在你自己项目的i18n文件里补充相应值即可(如 ss.constraints.Size.message.name = 姓名的长度不能超过{max} )。此时,注解的message仍支持EL表达式实际使用中推荐用此方案,因为这种方案不仅支持EL表达式、I18n消息、还支持返回可直接弹窗显示给用户的I18n字段名。

 

附: hibernate-validator提供的i18n bundle:

  • Constraint Metadata:框架提供了API来获取Constraint元数据,例如 类上、Property上、Method上、Constructor上 是否包含有Constraint等。包括 BeanDescriptor、PropertyDescriptor、MethodDescriptor、ConstructorDescriptor、ElementDescriptor、ContainerDescriptor and ContainerElementTypeDescriptor、GroupConversionDescriptor、ConstraintDescriptor 等。示例:
    BeanDescriptor carDescriptor = validator.getConstraintsForClass( Car.class );
    
    assertTrue( carDescriptor.isBeanConstrained() );
    
    //one class-level constraint
    assertEquals( 1, carDescriptor.getConstraintDescriptors().size() );
    
    //manufacturer, licensePlate, driver
    assertEquals( 3, carDescriptor.getConstrainedProperties().size() );
    
    //property has constraint
    assertNotNull( carDescriptor.getConstraintsForProperty( "licensePlate" ) );
    
    //property is marked with @Valid
    assertNotNull( carDescriptor.getConstraintsForProperty( "driver" ) );
    
    //constraints from getter method in interface and implementation class are returned
    assertEquals(
            2,
            carDescriptor.getConstraintsForProperty( "manufacturer" )
                    .getConstraintDescriptors()
                    .size()
    );
    
    //property is not constrained
    assertNull( carDescriptor.getConstraintsForProperty( "modelName" ) );
    
    //driveAway(int), load(List<Person>, List<PieceOfLuggage>)
    assertEquals( 2, carDescriptor.getConstrainedMethods( MethodType.NON_GETTER ).size() );
    
    //driveAway(int), getManufacturer(), getDriver(), load(List<Person>, List<PieceOfLuggage>)
    assertEquals(
            4,
            carDescriptor.getConstrainedMethods( MethodType.NON_GETTER, MethodType.GETTER )
                    .size()
    );
    
    //driveAway(int)
    assertNotNull( carDescriptor.getConstraintsForMethod( "driveAway", int.class ) );
    
    //getManufacturer()
    assertNotNull( carDescriptor.getConstraintsForMethod( "getManufacturer" ) );
    
    //setManufacturer() is not constrained
    assertNull( carDescriptor.getConstraintsForMethod( "setManufacturer", String.class ) );
    
    //Car(String, String, Person, String)
    assertEquals( 1, carDescriptor.getConstrainedConstructors().size() );
    
    //Car(String, String, Person, String)
    assertNotNull(
            carDescriptor.getConstraintsForConstructor(
                    String.class,
                    String.class,
                    Person.class,
                    String.class
            )
    );
    BeanDescriptor Demo

     

 

总结

可见用的最多的是spring的 @Validated 、其次是javax的 @Valid ,其多数基本功能相似,但在注解适用位置、分组、嵌套验证支持上有区别。

注解适用位置:前者可用于修饰类型、方法、方法参数上,不能修饰成员变量;后者可用于方法、方法参数、成员变量、构造函数上。能否用于成员属性决定了是否支持嵌套验证。

分组:前者支持,后者不支持。

嵌套验证:前者不支持,后者支持。

 

 

参考资料:

Bean Validation——BAT的乌托邦

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#preface (Hibernate Validation官方文档,实际上上面几个参考资料及上文中的挺多示例都是官方文档中的内容)

 

原理

Spring会在启动时通过AOP对使用@Validated或@Valid的类或其子类的对象生成一个代理对象。在代理对象中调用目标hanlder方法前后会分别进行参数、返回值的JSR303校验。

1 切面创建的相关逻辑在MethodValidationPostProcessor,相关源码:

 1 package org.springframework.validation.beanvalidation;
 2  
 3 @SuppressWarnings("serial")
 4 public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
 5         implements InitializingBean {
 6 
 7     private Class<? extends Annotation> validatedAnnotationType = Validated.class;
 8 
 9     @Nullable
10     private Validator validator;
11 
12 
13     /**
14      * Set the 'validated' annotation type.
15      * The default validated annotation type is the {@link Validated} annotation.
16      * <p>This setter property exists so that developers can provide their own
17      * (non-Spring-specific) annotation type to indicate that a class is supposed
18      * to be validated in the sense of applying method validation.
19      * @param validatedAnnotationType the desired annotation type
20      */
21     public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
22         Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
23         this.validatedAnnotationType = validatedAnnotationType;
24     }
25 
26     /**
27      * Set the JSR-303 Validator to delegate to for validating methods.
28      * <p>Default is the default ValidatorFactory's default Validator.
29      */
30     public void setValidator(Validator validator) {
31         // Unwrap to the native Validator with forExecutables support
32         if (validator instanceof LocalValidatorFactoryBean) {
33             this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
34         }
35         else if (validator instanceof SpringValidatorAdapter) {
36             this.validator = validator.unwrap(Validator.class);
37         }
38         else {
39             this.validator = validator;
40         }
41     }
42 
43     /**
44      * Set the JSR-303 ValidatorFactory to delegate to for validating methods,
45      * using its default Validator.
46      * <p>Default is the default ValidatorFactory's default Validator.
47      * @see javax.validation.ValidatorFactory#getValidator()
48      */
49     public void setValidatorFactory(ValidatorFactory validatorFactory) {
50         this.validator = validatorFactory.getValidator();
51     }
52 
53 
54     @Override
55     public void afterPropertiesSet() {
56         Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
57         this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
58     }
59 
60     /**
61      * Create AOP advice for method validation purposes, to be applied
62      * with a pointcut for the specified 'validated' annotation.
63      * @param validator the JSR-303 Validator to delegate to
64      * @return the interceptor to use (typically, but not necessarily,
65      * a {@link MethodValidationInterceptor} or subclass thereof)
66      * @since 4.2
67      */
68     protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
69         return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
70     }
71 
72 }
MethodValidationPostProcessor

MethodValidationPostProcessor实现了接口InitializingBean,该bean自身初始化时会创建一个DefaultPointcutAdvisor用于向符合条件的对象添加进行方法验证的AOP advise。相关源码:

1     @Override
2     public void afterPropertiesSet() {
3         Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
4         this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
5     }
MethodValidationPostProcessor#afterPropertiesSet

MethodValidationPostProcessor实现了接口BeanPostProcessor定义的方法postProcessAfterInitialization(从父类继承),该方法会检查每个bean的创建(在该bean初始化之后),如果检测到该bean符合条件,会向其增加上述AOP advise。相关源码:

 1     @Override
 2     public Object postProcessAfterInitialization(Object bean, String beanName) {
 3         if (this.advisor == null || bean instanceof AopInfrastructureBean) {
 4             // Ignore AOP infrastructure such as scoped proxies.
 5             return bean;
 6         }
 7 
 8         if (bean instanceof Advised) {
 9             Advised advised = (Advised) bean;
10             if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
11                 // Add our local Advisor to the existing proxy's Advisor chain...
12                 if (this.beforeExistingAdvisors) {
13                     advised.addAdvisor(0, this.advisor);
14                 }
15                 else {
16                     advised.addAdvisor(this.advisor);
17                 }
18                 return bean;
19             }
20         }
21 
22         if (isEligible(bean, beanName)) {
23             ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
24             if (!proxyFactory.isProxyTargetClass()) {
25                 evaluateProxyInterfaces(bean.getClass(), proxyFactory);
26             }
27             proxyFactory.addAdvisor(this.advisor);
28             customizeProxyFactory(proxyFactory);
29             return proxyFactory.getProxy(getProxyClassLoader());
30         }
31 
32         // No proxy needed.
33         return bean;
34     }
AbstractAdvisingBeanPostProcessor#postProcessAfterInitialization

MethodValidationPostProcessor是被ValidationAutoConfiguration自动配置到IoC容器的。相关源码:

 1 package org.springframework.boot.autoconfigure.validation;
 2  
 3 /**
 4  * EnableAutoConfiguration Auto-configuration to configure the validation
 5  * infrastructure.
 6  *
 7  * @author Stephane Nicoll
 8  * @author Madhura Bhave
 9  * @since 1.5.0
10  */
11 @Configuration
12 @ConditionalOnClass(ExecutableValidator.class)
13 @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
14 @Import(PrimaryDefaultValidatorPostProcessor.class)
15 public class ValidationAutoConfiguration {
16 
17     // 向容器注册一个 bean LocalValidatorFactoryBean defaultValidator,    
18     // 仅在容器中不存在类型为 Validator 的 bean时才注册该bean定义
19     // 这是Spring 框架各部分缺省使用的 validator , 基于对象属性上的验证注解验证对象属性值
20     @Bean    
21     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
22     @ConditionalOnMissingBean(Validator.class)
23     public static LocalValidatorFactoryBean defaultValidator() {
24         LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
25         MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
26         factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
27         return factoryBean;
28     }
29 
30     // 向容器注册一个 bean MethodValidationPostProcessor methodValidationPostProcessor
31     // 这是一个 BeanPostProcessor, 它基于容器中存在的 bean Validator构建一个 MethodValidationInterceptor,
32     // 这是一个方法调用拦截器,然后该 BeanPostProcessor 会为那些使用了注解 @Validated 的 bean
33     // 创建代理对象,并使用所创建的 MethodValidationInterceptor 包裹该 bean,从而能够在方法调用时
34     // 对 bean 的方法参数进行验证
35     @Bean
36     @ConditionalOnMissingBean
37     public static MethodValidationPostProcessor methodValidationPostProcessor(
38             Environment environment, @Lazy Validator validator) {
39         MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
40         boolean proxyTargetClass = environment
41                 .getProperty("spring.aop.proxy-target-class", Boolean.class, true);
42         processor.setProxyTargetClass(proxyTargetClass);
43         processor.setValidator(validator);
44         return processor;
45     }
46 
47 }
ValidationAutoConfiguration

2 切面中进行参数验证、返回值验证的相关逻辑在MethodValidationInterceptor,相关源码:

 1     @Override
 2     @SuppressWarnings("unchecked")
 3     public Object invoke(MethodInvocation invocation) throws Throwable {
 4         // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
 5         if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
 6             return invocation.proceed();
 7         }
 8 
 9         Class<?>[] groups = determineValidationGroups(invocation);
10 
11         // Standard Bean Validation 1.1 API
12         ExecutableValidator execVal = this.validator.forExecutables();
13         Method methodToValidate = invocation.getMethod();
14         Set<ConstraintViolation<Object>> result;
15 
16         try {
17             result = execVal.validateParameters(
18                     invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
19         }
20         catch (IllegalArgumentException ex) {
21             // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
22             // Let's try to find the bridged method on the implementation class...
23             methodToValidate = BridgeMethodResolver.findBridgedMethod(
24                     ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
25             result = execVal.validateParameters(
26                     invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
27         }
28         if (!result.isEmpty()) {
29             throw new ConstraintViolationException(result);
30         }
31 
32         Object returnValue = invocation.proceed();
33 
34         result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
35         if (!result.isEmpty()) {
36             throw new ConstraintViolationException(result);
37         }
38 
39         return returnValue;
40     }
MethodValidationInterceptor

注意:这里的Interceptor不是SpringMVC中拦截器的概念,而是AOP概念,只不过名字上类似而已。

interface org.aopalliance.aop.Advice

interface org.aopalliance.intercept.Interceptor

interface org.aopalliance.intercept.MethodInterceptor

class org.springframework.validation.beanvalidation.MethodValidationInterceptor

class org.springframework.validation.beanvalidation.MethodValidationPostProcessor

  

参考资料:https://blog.csdn.net/andy_zhang2007/article/details/99713481

 

posted @ 2015-10-12 17:24  March On  阅读(3257)  评论(1编辑  收藏  举报
top last
Welcome user from
(since 2020.6.1)