Spring Boot「10」Propety 验证

147 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

从前面的文章中,我们了解到激活@ConfigurationProperties的三种方式:

  • 在 Application 类上使用@EnableConfigurationProperties,指定要激活的@ConfigurationProperties
  • 借助 Spring 的 ComponentScan,使用@Component标注要激活的@ConfigurationProperties
  • 使用@ConfigurationPropertiesScan指定要激活的@ConfigurationProperties类所在包

最终目的是,向 Spring 容器中加入一个要激活的@ConfigurationProperties类的 Bean(后面我们称之为 ConfigurationPropertiesBean)。 ConfigurationPropertiesBean 中的属性来自于 Environment 中的 Property,它们可以是外部配置文件、环境变量、系统变量、命令行参数等定义的; 将 Environment 中 Property 赋值到 ConfigurationPropertiesBean 属性的过程称之为绑定(binding)。

01-@ConfigurationProperties 属性验证

JSR-303 定义了 Bean validation 规范,其中包含了诸多注解用来描述约束,例如@NotNull/@Size(min,max)等。 (这里不再一一学习这些注解,JSR 303 - Bean Validation 介绍及最佳实践这篇文章中有比较详细的介绍,等需要时可以参考)。 Hibernate validator 实现了 JSR-303,并扩展出了几个额外的约束注解,例如,@NotEmpty标注的字符串属性必须非空;@Email标注的字符串必须满足邮箱格式等; 更多约束注解可以参考 Hibernate validator 的官方文档Built-in constraints

得益于 Spring Boot 丰富的 starter,在项目中引入 Bean Validation 非常的简单,只需在项目 pom.xml 中添加如下依赖:

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

在标注@ConfigurationProperties的类属性上添加约束注解,例如:

@ConfigurationProperties(prefix = "external.carProperties.info")
public class Car {
    @NotNull
    private String manufacturer;

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

    @Min(2)
    private Integer seatCount;
}

上述示例中定义的约束为:manufacturer 不为空,licensePlate 不为空、且长度在2-4之间,seatCount 的值至少要为2;

然后,我们在外部配置中增加对应的配置信息,并通过@PropertySource将外部配置注册到 Environment 中:

external.carProperties.info.manufacturer=Morris
external.carProperties.info.license-plate=D
external.carProperties.info.seat_count=3

最后,为了对 Car 属性绑定时进行验证,需要在其类上标注@Validated,然后我们运行应用将得到如下的输出:

2022-10-20 15:39:38.791 ERROR 24368 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'external.carProperties.info' to self.samson.example.property.CarProperties failed:

    Property: external.carProperties.info.licensePlate
    Value: "D"
    Origin: "external.carProperties.info.license-plate" from property source "class path resource [properties/carProperties.properties]"
    Reason: 个数必须在2和14之间

原因是我们在 properties 文件中配置的external.carProperties.info.license-plate=D在绑定到 Car 对象时,发现并不满足长度在2-14之间的约束,应用启动失败。

02-自定义约束注解

虽然 JSR-303、Hibernate Validator 和 Spring 已经提供了许多的约束注解,但有时业务开发有特定、具体的需求,需要自定义的约束规则。 接下来我们将看一下如何自定义约束注解。 本节中我们继续沿用上面的示例 CarProperties。 例如我们需要保证 licensePlate 中至少要包含一个数字(这是一个臆想的需求,并不一定这样要求,只是作为演示),我们将如何定义呢? 主要分为三个步骤:

  1. 定义一个注解@MustHasNumber,必须包含 message、groups 和 payload 属性。而且注解上包含了元注解@Constraint,用来指定验证逻辑的实现类:
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MustHasNumberValidatorImpl.class)
public @interface MustHasNumber {
    String message() default "must has number";

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

}
  1. 实现 ConstraintValidator 接口,定义自己的验证规则。验证逻辑在 isValid 方法中:
public class MustHasNumberValidatorImpl implements ConstraintValidator<MustHasNumber, String> {

    @Override
    public void initialize(MustHasNumber constraintAnnotation) {}

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        for (int i = 0; i < value.length(); ++i) {
            if (value.charAt(i) >= '0' && value.charAt(i) <= '9') {
                /** 只要值中包含数字即可 */
                return true;
            }
        }
        return false;
    }
}
  1. 使用注解标注属性。
@NotNull
@Size(min = 2, max = 14)
@MustHasNumber
private String licensePlate;

如果我们在 classpath:/properties/car.properties 中的 validation.car.info.license-plate=Daaa,运行程序后,我们将得到如下错误:

***************************
APPLICATION FAILED TO START
***************************

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'validation.car.info' to self.samson.example.property.CarProperties failed:

    Property: validation.car.info.licensePlate
    Value: "Daaa"
    Origin: "validation.car.info.license-plate" from property source "class path resource [properties/car.properties]"
    Reason: must has number

可以看到,提示的错误原因 Reason 正是我们在@MustHasNumber 中定义的 message 的默认值。

03-手动验证

前面学习的内容都是依赖于 Spring Boot 框架的,如果不使用 Spring 框架,我们又如何使用 Bean Validation 呢? 从前面了解到,Hibernate Validator 实现了 JSR-303,接下来我们就从单元测试的角度来看一下如何仅使用 Hibernate Validator 来进行对象属性验证。

首先,我们需要一个 Validator,可以通过 ValidatorFactory 创建:

private static Validator validator;

@BeforeAll
public static void setUpValidator() {
    final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
    validator = validatorFactory.getValidator();
}

然后,通过 Validator#validate() 方法,可以验证某个对象是否满足其类上标注的约束。 同样使用上节中的 CarProperties 类:

CarProperties carProperties = new CarProperties( null, "DD-AB-123", 4 );

final Set<ConstraintViolation<CarProperties>> constraintViolations  = validator.validate(carProperties);
assertThat(constraintViolations).size().isEqualTo(1);
assertThat(constraintViolations.iterator().next().getMessage()).isEqualTo("不能为null");

04-总结

今天我们学习了属性验证的两种方式,通过 Spring Boot 自动进行验证和使用 Hibernate Validator 手动验证。 并且在自动验证中,我们知道了如何自定义约束注解并使用它们。 在项目中,属性验证是非常有必要的一项技术,它能够省去大量的 if-else 编码,使得代码更清晰、也更精简。