diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index 86b60f7c4e39..f2b95ebd8e91 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -51,6 +51,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.env.PropertySources; import org.springframework.util.Assert; +import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -134,6 +135,7 @@ private IgnoreTopLevelConverterNotFoundBindHandler getHandler() { : new IgnoreTopLevelConverterNotFoundBindHandler(); } + @SuppressWarnings("unchecked") private List getValidators(Bindable target) { List validators = new ArrayList<>(3); if (this.configurationPropertiesValidator != null) { @@ -142,8 +144,13 @@ private List getValidators(Bindable target) { if (this.jsr303Present && target.getAnnotation(Validated.class) != null) { validators.add(getJsr303Validator()); } - if (target.getValue() != null && target.getValue().get() instanceof Validator validator) { - validators.add(validator); + if (target.getValue() != null) { + if (target.getValue().get() instanceof Validator validator) { + validators.add(validator); + } + } + else if (Validator.class.isAssignableFrom(target.getType().resolve())) { + validators.add(new SelfValidatingConstructorBoundBindableValidator((Bindable) target)); } return validators; } @@ -250,4 +257,28 @@ public ConfigurationPropertiesBinder getObject() throws Exception { } + /** + * A {@code Validator} for a constructor-bound {@code Bindable} where the type being + * bound is itself a {@code Validator} implementation. + */ + static class SelfValidatingConstructorBoundBindableValidator implements Validator { + + private final Bindable bindable; + + SelfValidatingConstructorBoundBindableValidator(Bindable bindable) { + this.bindable = bindable; + } + + @Override + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(this.bindable.getType().resolve()); + } + + @Override + public void validate(Object target, Errors errors) { + ((Validator) target).validate(target, errors); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index b071174cc7d5..a0683316bbc1 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -731,6 +731,16 @@ void loadWhenConfigurationPropertiesIsAlsoValidatorShouldApplyValidator() { }); } + @Test + void loadWhenConstructorBoundConfigurationPropertiesIsAlsoValidatorShouldApplyValidator() { + assertThatExceptionOfType(Exception.class) + .isThrownBy(() -> load(ValidatorConstructorBoundPropertiesConfiguration.class)) + .satisfies((ex) -> { + assertThat(ex).hasCauseInstanceOf(BindException.class); + assertThat(ex.getCause()).hasCauseExactlyInstanceOf(BindValidationException.class); + }); + } + @Test void loadWhenConfigurationPropertiesWithValidDefaultValuesShouldNotFail() { AnnotationConfigApplicationContext context = load(ValidatorPropertiesWithDefaultValues.class); @@ -2060,6 +2070,36 @@ void setFoo(String foo) { } + @EnableConfigurationProperties(ValidatorConstructorBoundProperties.class) + static class ValidatorConstructorBoundPropertiesConfiguration { + + } + + @ConfigurationProperties + static class ValidatorConstructorBoundProperties implements Validator { + + private final String foo; + + ValidatorConstructorBoundProperties(String foo) { + this.foo = foo; + } + + @Override + public boolean supports(Class type) { + return type == ValidatorConstructorBoundProperties.class; + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "foo", "TEST1"); + } + + String getFoo() { + return this.foo; + } + + } + @EnableConfigurationProperties @ConfigurationProperties(prefix = "test") static class WithSetterThatThrowsValidationExceptionProperties {