参数校验:Bean Validation - Hibernate Validator - Spring Validation

蒋啸
2023-12-01

Bean Validation - Hibernate Validator - Spring Validation

1 Bean Validation

Bean Validation 现名为 Jakarta Bean Validation,官网为: https://beanvalidation.org/

Jakarta Bean Validation 是一套 Java EE 规范,提供了以下功能:

  • 通过注解表达对对象模型的约束
  • 可自定义约束
  • 提供 API 来校验对象和对象图(Object Graph)
  • 提供 API 来校验方法和构造器的参数和返回值
  • 报告一组违规(即校验不通过的信息,经过本地化)
  • 在 Java SE 上运行,并集成在 Jakarta EE 9 和 10 中
  • lets you express constraints on object models via annotations
  • lets you write custom constraints in an extensible way
  • provides the APIs to validate objects and object graphs
  • provides the APIs to validate parameters and return values of methods and constructors
  • reports the set of violations (localized)
  • runs on Java SE and is integrated in Jakarta EE 9 and 10

Jakarta Bean Validation 版本历史:

  1. Bean Validation 1.0 (JSR 303) 是 Java 对象验证标准的第一个版本,发布于 2009 年,是 Java EE 6 的一部分,可以与 Java SE 一同使用。
  2. Bean Validation 1.1 (JSR 349) 于 2013 年完成,是 Java EE 7 的一部分,可以与 Java SE 一同使用。
  3. Bean Validation 2.0 (JSR 380) 于 2017 年 8 月完成,是 Java EE 8 的一部分,可以与 Java SE 一同使用。
  4. Jakarta Bean Validation 2.0 于 2019 年 8 月发布。它是 Jakarta EE 8 的一部分,可以与 Java SE 一同使用。Jakarta Bean Validation 2.0 与 Bean Validation 2.0 除了 GAV(GroupId 、ArtifactId 、Version) 以外没有任何不同,现在的 GAV 是:jakarta.validation:jakarta.validation-api

2 Hibernate Validator

Hibernate Validator 是对 Bean Validation 的实现,并进行了扩展。其官网为:https://hibernate.org/validator/

官方文档地址为:https://hibernate.org/validator/documentation/

2.1 引入依赖

<!-- Jakarta Bean Validation-->
<dependency>
  <groupId>jakarta.validation</groupId>
  <artifactId>jakarta.validation-api</artifactId>
  <version>2.0.2</version>
</dependency>

<!-- Hibernate Validator,引入此依赖后,无需引入 Bean Validation -->
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.2.0.Final</version>
</dependency>

<!-- el解释器,用于处理消息模板中的 el 表达式  -->
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-el</artifactId>
    <version>9.0.52</version>
</dependency>

2.2 Validator

校验需要 Validator(类) 与 Constraint(注解) 配合才能实现。Constraint 以注解的形式标注在需要被校验的结构,表示此结构需满足的条件,不同的 Constraint 对应的不同的 Validator(一般命名规则为:注解xx对应xxValidator),Validator 进行真实的校验工作。内置的 Constraint 与 Validator 是由 ConstraintHelper 确定处理的。

package com.xumenghao.model;
import lombok.Data;
import javax.validation.constraints.NotNull;

@Data
public class User {
    @NotNull
    private String name;
}
package com.xumenghao.util;
import com.xumenghao.model.User;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ValidatorUtil {
    // 此接口线程安全
    private static Validator validator;
    static {
        // 获取 ValidatorFactory ,再通过其获得 Validator
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    public static List<String> valid(User user){
        // 寻找对应的 Validator 对其属性进行校验,若是通过,返回的 Set 为空
        Set<ConstraintViolation<User>> validateInfo = validator.validate(user);
        return validateInfo.stream().map(v -> "属性:" + v.getPropertyPath()
                                        + ",属性值:" + v.getInvalidValue()
                                        + ",提示信息:" + v.getMessage()).collect(Collectors.toList());
    }
}

使用 Validator 时,只是声明了接口,具体的实现(Hibernate Validator)是通过 SPI 机制找到的。

package com.xumenghao;
import com.xumenghao.model.User;
import com.xumenghao.util.ValidatorUtil;
import java.util.List;

public class App {
    public static void main(String[] args) {
        User user = new User();
        List<String> valid = ValidatorUtil.valid(user);
        System.out.println(valid);
    }
}

// 输出:
// [属性:name,属性值:null,提示信息:不能为null]

2.3 Constraint

注意:约束是可以重复的,但不能矛盾。

2.3.1 Bean Validation Constraints

非空校验
约束作用支持数据类型
@NotNull标注的结构必须不为 null任何类型
@Null标注的结构必须为 null任何类型
数值校验
约束作用
@DecimalMax(value=, inclusive=)如果 inclusive 为 false,标注的结构必须小于 value;如果 inclusive 为 true,标注的结构必须小于等于 value
@DecimalMin(value=, inclusive=)如果 inclusive 为 false,标注的结构必须大于 value;如果 inclusive 为 true,标注的结构必须大于等于 value
@Digits(integer=, fraction=)标注的结构必须有整数部分位数上限为integer、小数部分位数上限为fraction

上述3个注解支持的数据类型皆为:

  • BigDecimal
  • BigInteger
  • CharSequence
  • byte、short、int、long 及对应的包装类
  • any sub-type of Number and javax.money.MonetaryAmount (if the JSR 354 API and an implementation is on the class path)
约束作用
@Max(value=)标注的结构必须小于等于value
@Min(value=)标注的结构必须大于等于value
@Negative标注的结构必须小于0
@NegativeOrZero标注的结构必须小于等于0
@Positive标注的结构必须大于0
@PositiveOrZero标注的结构必须大于等于0

上述6个注解支持的数据类型皆为:

  • BigDecimal
  • BigInteger
  • byte、short、int、long 及对应的包装类
  • any sub-type of CharSequence (the numeric value represented by the character sequence
    is evaluated)
  • any sub-type of Number and javax.money.MonetaryAmount
布尔校验
约束作用支持数据类型
@AssertFalse标注的结构必须为 falseBoolean, boolean
@AssertTrue标注的结构必须为 trueBoolean, boolean
长度校验
约束作用支持数据类型
@NotBlank标注的字符序列必须非 null 且长度大于 0(经过trim)CharSequence
@NotEmpty标注的结构必须非null且非长度大于0(不经过trim)CharSequence、Collection、Map、arrays
@Size(min=, max=)标注的结构的长度(元素个数)必须介于[min,max]CharSequence、Collection、Map、arrays
时间校验
约束作用
@Future标注的日期必须是未来
@FutureOrPresent标注的日期必须是未来或现在
@Past标注的日期必须是过去
@PastOrPresent标注的日期必须是过去或现在

上述6个注解支持的数据类型皆为:

  • java.util.Date
  • java.util.Calendar
  • java.time.Instant
  • java.time.LocalDate
  • java.time.LocalDateTime
  • java.time.LocalTime
  • java.time.MonthDay
  • java.time.OffsetDateTime
  • java.time.OffsetTime
  • java.time.Year
  • java.time.YearMonth, java.time.ZonedDateTime
  • java.time.chrono.HijrahDate
  • java.time.chrono.JapaneseDate
  • java.time.chrono.MinguoDate
  • java.time.chrono.ThaiBuddhistDate;
  • additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant
正则校验
约束作用支持数据类型
@Email标注的字符序列必须为有效的电子邮箱地址。可选参数 regexpflags可以指定电子邮件必须匹配的附加正则表达式(包括正则表达式标志)CharSequence
@Pattern(regex=, flags=)检查标注的字符串是否与给定 flag 匹配的正则表达式regex 匹配CharSequence

2.3.2 Hibernate Validator Additional constraints

常用的扩展约束:

约束作用支持数据类型
@Length(min=, max=)标注的字符序列长度在[min,max]之间CharSequence
@Range(min=, max=)标注的结构的值在[min,max]之间BigDecimal,BigInteger,CharSequence,byte、short、 int、 long 及其包装类
@URL(protocol=, host=, port=, regexp=, flags=)检查标注的字符序列是否是一个有效的URL(根据RFC2396),如果protocolhostport有指定值,则相应URL片段需要完全匹配。CharSequence

2.3.3 @Valid

@Valid 是 Bean Validation 提供的注解,可用于方法、属性、构造器、参数、任何使用类型的语句
作用:

  • 在属性、方法参数或方法返回值使用此注解进行级联校验。
  • 当属性、方法参数或方法返回值被校验的时候,被 Constraint 标记的对象及其属性也会被校验。
  • 此行为是递归的。

Marks a property, method parameter or method return type for validation cascading.
Constraints defined on the object and its properties are be validated when the property, method parameter or method return type is validated.
This behavior is applied recursively.

当一个属性、方法参数或者返回值为一个 Bean ,此 Bean 的属性也有约束,希望对此 Bean 的属性进行校验,则需要 @Valid 对属性、方法参数或者返回值进行标注。

@Data
public class User {
    @NotBlank
    private String name;
    @Valid
    @NotNull
    private Car car;
}
@Data
public class Car {
    @NotBlank
    private String brand;
    @NotBlank
    private String type;
    @Min(10000)
    private Double price;
}
public class App {
    public static void main(String[] args) {
        User user = new User();
        user.setName("Jack");
        user.setCar(new Car());
        List<String> valid = ValidatorUtil.valid(user);
        System.out.println(valid);
    }
}
// 对于上述测试
// 未使用 @Valid ,输出:[]
// 使用了 @Valid ,输出:[属性:car.brand,属性值:null,提示信息:不能为空, 属性:car.type,属性值:null,提示信息:不能为空]

2.4 消息模板

可以通过约束的 message方法参数指定此校验不通过时输出的信息,还可以使用 EL 表达式。

@Data
public class Car {
    @NotBlank
    private String brand;
    @NotBlank
    private String type;
    @Min(value = 10000, message = "价格要高于 ${value} !")
    private Double price;
}

2.5 分组校验

可通过约束的 groups方法参数进行分组,约束只有在指定的组下才生效,不指定时默认属于 javax.validation.groups.Default组。

@Data
public class User {
    // 接口作为分组标识
    public interface Add{};
    public interface Update{};
    @Null(groups = {Add.class})
    @NotNull(groups = {Update.class})
    private Long id;
    
    @NotBlank
    private String name;
    
    @Valid
    @NotNull
    private Car car;
    
    @InSet({"男","女"})
    private String gender;
}
public static List<String> valid(User user, Class<?> group){
        Set<ConstraintViolation<User>> validateInfo = validator.validate(user, group, Default.class);
        return validateInfo.stream().map(v -> "属性:" + v.getPropertyPath()
                                        + ",属性值:" + v.getInvalidValue()
                                        + ",提示信息:" + v.getMessage()).collect(Collectors.toList());
    }
public class App {
    public static void main(String[] args) {
        User user = new User();
        user.setId(123L);
        user.setName("Jack");
        Car car = new Car();
        car.setBrand("BMW");
        car.setType("SUV");
        car.setPrice(20000000.0);
        user.setCar(car);
        user.setGender("男");
        List<String> valid = ValidatorUtil.valid(user, User.Add.class);
        System.out.println(valid);
    }
}
// 当分组为 Add.class 时,输出:[属性:id,属性值:123,提示信息:必须为null]
// 当分组为 Update.class 时,输出:[]

2.6 自定义约束

@Documented
@Constraint(validatedBy = {InSetValidator.class})  // 指明处理此约束的Validator
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface InSet {
    String message() default "当前值不在允许范围内";

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

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

    String[] value();
}
package com.xumenghao.validator;

import com.xumenghao.constraint.InSet;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class InSetValidator implements ConstraintValidator<InSet, String> {
    private String[] strings;
    @Override
    public void initialize(InSet constraintAnnotation) {
        // 获取注解的 value
        strings = constraintAnnotation.value();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        // String s 为被注解标注的属性
        // 如果被标注的值为空,则不校验,直接返回
        if (s == null){
            return true;
        }
        for(String str : strings){
            if (str.equals(s)){
                return true;
            }
        }
        return false;
    }
}
@Data
public class User {
    @NotBlank
    private String name;
    @Valid
    @NotNull
    private Car car;
    @InSet({"男","女"})
    private String gender;
}

2.8 快速失败模式

快速失败(fail fast)模式:第一次校验不通过就不再校验后面的。

// 设置快速失败模式
fastValidator = Validation.byProvider(HibernateValidator.class)
    .configure().failFast(true)
    .buildValidatorFactory().getValidator();

2.9 非 Bean 入参校验

情景:上述都是对自定义的 Bean 进行校验。如果方法参数是 Bean,因为 Bean 中的属性已被约束标注,可以通过直接将 Bean (实例对象)传入 ValidatorUtil 的 valid 方法进行校验。但如果方法参数并不是 Bean,是String、Interger 等类型,该如何校验?
方式一:方法参数前使用约束注解,再通过反射获取相关信息,传给校验方法。

public class ValidatorUtil {
    private final static Validator VALIDATOR;
    private final static ExecutableValidator EXECUTABLE_VALIDATOR;
    static {
        VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
        EXECUTABLE_VALIDATOR = VALIDATOR.forExecutables();
    }

    public static <T> List<String> valid(T object, Method method, Object[] parameterValues, Class<?>... groups){
        Set<ConstraintViolation<T>> validateInfo = EXECUTABLE_VALIDATOR.validateParameters(object, method, parameterValues, groups);
        return validateInfo.stream().map(v -> "属性:" + v.getPropertyPath()
                                    + ",属性值:" + v.getInvalidValue()
                                    + ",提示信息:" + v.getMessage()).collect(Collectors.toList());
    }
}
public class UserService {
    public List<String> getByName(@NotNull String name){
        StackTraceElement st = Thread.currentThread().getStackTrace()[1];
        String methodName = st.getMethodName();
        Method method = null;
        try {
            method = this.getClass().getDeclaredMethod(methodName, String.class);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return ValidatorUtil.valid(this, method, new Object[]{name});
    }
}

方式二:AOP
可以看出,上述方式十分复杂,还不如传统使用 if-else 的校验方法。
可以使用 AOP 编程思想进行非 Bean 入参校验,具体见

3 Spring Validation

Spring Validation 从两个方向提供了校验功能:

  1. 提供接口 org.springframework.validation.Validator
  2. 支持 Bean Validation(使用 Hibernate Validator)

官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation

3.1 使用 Spring Validator 进行校验

Spring 提供了 org.springframework.validation.Validator 接口,可以通过实现此接口对特定的 Bean 进行校验。
Spring 还提供了ValidationUtils 工具类,里面提供了通用的校验方法。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.3.20</version>
</dependency>
@Data
public class User {
    private Long id;
    private String name;
}
package com.xumenghao.validator;
import com.xumenghao.model.User;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class UserValidator implements Validator {
    
    /**
     * 用来检查被校验的对象是否支持,此 Validator 只支持 User
     * @param clazz 对象的类实例
     * @return true 表示支持,反之不支持
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return User.class.equals(clazz);
    }
    
    /**
     * 用来校验的方法
     * @param target 被校验的对象
     * @param errors 用来封装校验的错误,如果通过校验,则空
     */
    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name empty");
        User user  = (User) target;
        if(user.getId() < 0){
            errors.reject("id","negative value");
        }
    }
}
public class App {
    public static void main(String[] args) {
        // 准备 User 对象
        User user = new User();
        user.setName("");
        user.setId(-10L);
        // 通过 DataBinder 将 User 对象与 UserValidator 绑定到一起
        DataBinder dataBinder = new DataBinder(user);
        dataBinder.setValidator(new UserValidator());
        // 通过 DataBinder 调用 UserValidator 对 User 进行校验
        dataBinder.validate();
        // 获得校验结果并输出
        BindingResult bindingResult = dataBinder.getBindingResult();
        List<ObjectError> allErrors = bindingResult.getAllErrors();
        System.out.println(allErrors);
    }
}

3.2 使用 Bean Validation (Hibernate Validator)进行校验

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>5.3.20</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
  <version>6.2.0.Final</version>
</dependency>

3.2.1 校验 Bean

使用 Bean Validation 需要 javax.validation.ValidatorFactoryjavax.validation.Validator,Spring默认有一个实现类 LocalValidatorFactoryBean 实现了上述接口,并且也实现了org.springframework.validation.Validator 接口。可以将 LocalValidatorFactoryBean 注入 IOC。

@Configuration
public class AppConfig {
    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean(){
        return new LocalValidatorFactoryBean();
    }
}
使用 javax.validation.Validator

与前文 2.2 使用方式完全相同,只不过因为 Spring 的存在,可以不用手动创建。

import javax.validation.Validator;

@Service
public class MyService {
    
    @Autowired
    private Validator validator;
    
    public  boolean validator(Person person){
        Set<ConstraintViolation<Person>> sets =  validator.validate(person);
        return sets.isEmpty();
    }
    
}
使用 org.springframework.validation.Validator
import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;

   public boolean validaPersonByValidator(Person person) {
      BindException bindException = new BindException(person, person.getName());
      validator.validate(person, bindException);
      return bindException.hasErrors();
   }
}

3.2.3 校验方法参数

使用 MethodValidationPostProcessor@Validated 注解配合进行方法参数的校验。
通过注入 MethodValidationPostProcessor将 Bean Validation 支持的方法验证功能整合到 Spring 中。

import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
@ComponentScan(basePackages = {"com.xumenghao"})
public class AppConfig {

    @Bean
    public MethodValidationPostProcessor validationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

为了使得 Spring 驱动的方法验证生效,需要在目标类上使用@Validated注解,目标类中的方法参数如果有 Bean Validation 的注解并标注,则会自动被校验,如果校验不通过,则会抛出 ConstraintViolationException

@Data
public class User {
    public interface Add{};
    public interface Update{};

    @NotNull(groups = Update.class)
    @Null(groups = Add.class)
    private Long id;

    @NotBlank
    private String name;
}
@Service
@Validated
public class UserService {
    public void printUser(@NotNull @Valid User user){
        System.out.println(user);
    }
}
public class App {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserService userService = context.getBean(UserService.class);
        User user = new User();
        userService.printUser(user);
    }
}

3.2.4 @Validated

@Validated 注解是由 Spring 提供的,除了上述用于方法参数校验外,其功能与 Bean Validation 的 @Valid 相同:级联验证,但是 @Validated支持分组校验。实例见 3.2.5。

3.2.5 Spring MVC Validation

默认情况下,如果 Bean Validation 在 classPath 上存在(例如 Hibernate Validator),则将 LocalValidatorFactoryBean 注册为用于在 Controller 方法参数上 @Valid@Validated的全局校验器。

对于 Spring MVC 下的 Controller 中的方法参数:

  1. 如果方法入参为 Bean,参数前使用了 @Valid@Validated 注解,则会自动对 Bean 进行校验(其余层不支持),即使用 LocalValidatorFactoryBean,无需在类上标注 @Validated;如果校验不通过会抛出 BindException
  2. 方法入参无论是 Bean 还是非 Bean,如果使用约束注解,比如 @Null@NotNull,则是进行参数校验,即使用 ConstraintViolationException,需要在类上标注 @Validated;如果校验不通过会抛出 ConstraintViolationException
@RestController
@Validated
public class UserController {
    @Autowired
    UserService userService;

    @GetMapping("/getByName")
    public String getByName(@NotBlank String name){
        return name + ": ok";
    }

    @GetMapping("/addUser")
    public String addUser(@Validated({User.Add.class, Default.class}) User user){
        userService.printUser(user);
        return "成功!";
    }
}

3.3 整合 SpringBoot

可以引入org.springframework.boot:spring-boot-starter-validation 会将
org.hibernate.validator:hibernate-validatororg.springframework.boot:spring-boot-starter引入并进行自动配置,如果已存在
org.springframework.boot:spring-boot-starter-web,可以只引入 org.hibernate.validator:hibernate-validator(有些版本只需要引入 stater-web 即可)。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({ExecutableValidator.class})
@ConditionalOnResource(
    resources = {"classpath:META-INF/services/javax.validation.spi.ValidationProvider"}
)
@Import({PrimaryDefaultValidatorPostProcessor.class})
public class ValidationAutoConfiguration {
    public ValidationAutoConfiguration() {
    }

    @Bean
    @Role(2)
    @ConditionalOnMissingBean({Validator.class})
    public static LocalValidatorFactoryBean defaultValidator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        return factoryBean;
    }

    @Bean
    @ConditionalOnMissingBean
    public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
        FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(excludeFilters.orderedStream());
        boolean proxyTargetClass = (Boolean)environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
        processor.setProxyTargetClass(proxyTargetClass);
        processor.setValidator(validator);
        return processor;
    }
}

3.4 设置快速失败模式

@Configuration(proxyBeanMethods = false)
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean localValidatorFactoryBean(){
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        factoryBean.getValidationPropertyMap()
                .put(BaseHibernateValidatorConfiguration.FAIL_FAST, Boolean.TRUE.toString());
        return factoryBean;
    }
}

3.5 统一异常管理

结合统一异常管理处理校验未通过时的异常,使得代码更加整洁。
未完待续…


笔者才疏学浅,若有纰漏,欢迎指证!

 类似资料: