diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java index 0924a67112..a027a900ed 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java @@ -92,7 +92,9 @@ public interface BindHandler { } /** - * Called when binding finishes, regardless of whether the property was bound or not. + * Called when binding finishes with either bound or unbound result. This method will + * not be called when binding failed, even if a handler rurns a result from + * {@link #onFailure}. * @param name the name of the element being bound * @param target the item being bound * @param context the bind context diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java index d9f93d6e82..73064e04d4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java @@ -65,6 +65,17 @@ public class ValidationBindHandler extends AbstractBindHandler { return super.onSuccess(name, target, context, result); } + @Override + public Object onFailure(ConfigurationPropertyName name, Bindable target, BindContext context, Exception error) + throws Exception { + Object result = super.onFailure(name, target, context, error); + if (result != null) { + this.exceptions.clear(); + } + validate(name, target, context, result); + return result; + } + @Override public void onFinish(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) throws Exception { @@ -72,14 +83,6 @@ public class ValidationBindHandler extends AbstractBindHandler { super.onFinish(name, target, context, result); } - @Override - public Object onFailure(ConfigurationPropertyName name, Bindable target, BindContext context, Exception error) - throws Exception { - Object result = super.onFailure(name, target, context, error); - validate(name, target, context, null); - return result; - } - private void validate(ConfigurationPropertyName name, Bindable target, BindContext context, Object result) { Object validationTarget = getValidationTarget(target, context, result); Class validationType = target.getBoxedType().resolve(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java index f2b338f845..cbf1ce996d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java @@ -167,7 +167,7 @@ class ValidationBindHandlerTests { @Test void bindShouldValidateIfOtherHandlersInChainIgnoreError() { - TestHandler testHandler = new TestHandler(); + TestHandler testHandler = new TestHandler(null); this.handler = new ValidationBindHandler(testHandler, this.validator); this.sources.add(new MockConfigurationPropertySource("foo", "hello")); ExampleValidatedBean bean = new ExampleValidatedBean(); @@ -177,6 +177,21 @@ class ValidationBindHandlerTests { .withCauseInstanceOf(BindValidationException.class); } + @Test + void bindShouldValidateIfOtherHandlersInChainReplaceErrorWithResult() { + TestHandler testHandler = new TestHandler(new ExampleValidatedBeanSubclass()); + this.handler = new ValidationBindHandler(testHandler, this.validator); + this.sources.add(new MockConfigurationPropertySource("foo", "hello")); + this.sources.add(new MockConfigurationPropertySource("foo.age", "bad")); + this.sources.add(new MockConfigurationPropertySource("foo.years", "99")); + ExampleValidatedBean bean = new ExampleValidatedBean(); + assertThatExceptionOfType(BindException.class) + .isThrownBy(() -> this.binder.bind("foo", + Bindable.of(ExampleValidatedBean.class).withExistingValue(bean), this.handler)) + .withCauseInstanceOf(BindValidationException.class) + .satisfies((ex) -> assertThat(ex.getCause()).hasMessageContaining("years")); + } + private BindValidationException bindAndExpectValidationError(Runnable action) { try { action.run(); @@ -219,6 +234,25 @@ class ValidationBindHandlerTests { } + public static class ExampleValidatedBeanSubclass extends ExampleValidatedBean { + + @Min(100) + private int years; + + ExampleValidatedBeanSubclass() { + setAge(20); + } + + public int getYears() { + return this.years; + } + + public void setYears(int years) { + this.years = years; + } + + } + @Validated public static class ExampleValidatedWithNestedBean { @@ -286,10 +320,16 @@ class ValidationBindHandlerTests { static class TestHandler extends AbstractBindHandler { + private Object result; + + TestHandler(Object result) { + this.result = result; + } + @Override public Object onFailure(ConfigurationPropertyName name, Bindable target, BindContext context, Exception error) throws Exception { - return null; + return this.result; } }