写点什么

Spring Boot「10」Propety 验证

作者:Samson
  • 2022-10-21
    上海
  • 本文字数:3226 字

    阅读完需:约 11 分钟

Spring Boot「10」Propety 验证

从前面的文章中,我们了解到激活@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=Morrisexternal.carProperties.info.license-plate=Dexternal.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)@MustHasNumberprivate 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;
@BeforeAllpublic 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 编码,使得代码更清晰、也更精简。

发布于: 刚刚阅读数: 4
用户头像

Samson

关注

还未添加个人签名 2019-07-22 加入

InfoQ签约作者 | 阿里云社区签约作者

评论

发布
暂无评论
Spring Boot「10」Propety 验证_Java_Samson_InfoQ写作社区