从前面的文章中,我们了解到激活@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 中至少要包含一个数字(这是一个臆想的需求,并不一定这样要求,只是作为演示),我们将如何定义呢?主要分为三个步骤:
定义一个注解@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 {};
}
复制代码
实现 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;
}
}
复制代码
使用注解标注属性。
@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 编码,使得代码更清晰、也更精简。
评论