diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java index 384dc790736..f2888d25d46 100644 --- a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java @@ -16,10 +16,15 @@ package org.springframework.boot.diagnostics.analyzer; +import org.springframework.boot.context.properties.bind.BindException; +import org.springframework.boot.context.properties.bind.UnboundConfigurationPropertiesException; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.boot.context.properties.bind.validation.ValidationErrors; +import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; import org.springframework.boot.diagnostics.FailureAnalysis; -import org.springframework.util.CollectionUtils; -import org.springframework.validation.BindException; +import org.springframework.boot.origin.Origin; +import org.springframework.util.StringUtils; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; @@ -34,22 +39,83 @@ class BindFailureAnalyzer extends AbstractFailureAnalyzer { @Override protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) { - if (CollectionUtils.isEmpty(cause.getAllErrors())) { + if (cause.getCause() instanceof BindValidationException) { + return analyzeBindValidationException(cause, + (BindValidationException) cause.getCause()); + } + else if (cause.getCause() instanceof UnboundConfigurationPropertiesException) { + return analyzeUnboundConfigurationPropertiesException(cause, + (UnboundConfigurationPropertiesException) cause.getCause()); + } + return analyzeGenericBindException(cause); + } + + private FailureAnalysis analyzeBindValidationException(BindException cause, + BindValidationException validationException) { + ValidationErrors errors = validationException.getValidationErrors(); + if (!errors.hasErrors()) { return null; } StringBuilder description = new StringBuilder( String.format("Binding to target %s failed:%n", cause.getTarget())); - for (ObjectError error : cause.getAllErrors()) { + for (ObjectError error : errors) { if (error instanceof FieldError) { - FieldError fieldError = (FieldError) error; - description.append(String.format("%n Property: %s", - cause.getObjectName() + "." + fieldError.getField())); - description.append( - String.format("%n Value: %s", fieldError.getRejectedValue())); + appendFieldError(description, (FieldError) error); } description.append( String.format("%n Reason: %s%n", error.getDefaultMessage())); } + return getFailureAnalysis(description, cause); + } + + private void appendFieldError(StringBuilder description, FieldError error) { + Origin origin = Origin.from(error); + description.append(String.format("%n Property: %s", + error.getObjectName() + "." + error.getField())); + description.append(String.format("%n Value: %s", error.getRejectedValue())); + if (origin != null) { + description.append(String.format("%n Origin: %s", origin)); + } + } + + private FailureAnalysis analyzeUnboundConfigurationPropertiesException( + BindException cause, UnboundConfigurationPropertiesException exception) { + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", cause.getTarget())); + for (ConfigurationProperty property : exception.getUnboundProperties()) { + buildDescription(description, property); + description.append(String.format("%n Reason: %s", exception.getMessage())); + } + return getFailureAnalysis(description, cause); + } + + private FailureAnalysis analyzeGenericBindException(BindException cause) { + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", cause.getTarget())); + ConfigurationProperty property = cause.getProperty(); + buildDescription(description, property); + description.append(String.format("%n Reason: %s", getMessage(cause))); + return getFailureAnalysis(description, cause); + } + + private void buildDescription(StringBuilder description, + ConfigurationProperty property) { + if (property != null) { + description.append(String.format("%n Property: %s", property.getName())); + description.append(String.format("%n Value: %s", property.getValue())); + description.append(String.format("%n Origin: %s", property.getOrigin())); + } + } + + private String getMessage(BindException cause) { + if (cause.getCause() != null + && StringUtils.hasText(cause.getCause().getMessage())) { + return cause.getCause().getMessage(); + } + return cause.getMessage(); + } + + private FailureAnalysis getFailureAnalysis(Object description, BindException cause) { return new FailureAnalysis(description.toString(), "Update your application's configuration", cause); } diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java index 4e00c9ac983..978f4f5b7e6 100644 --- a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java @@ -16,7 +16,10 @@ package org.springframework.boot.diagnostics.analyzer; +import java.util.HashMap; +import java.util.List; import java.util.Locale; +import java.util.Map; import javax.validation.Valid; import javax.validation.constraints.Min; @@ -32,6 +35,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.diagnostics.FailureAnalysis; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.env.MapPropertySource; +import org.springframework.core.env.MutablePropertySources; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; @@ -76,20 +81,60 @@ public class BindFailureAnalyzerTests { .contains("Reason: This object could not be bound."); } + @Test + public void bindExceptionWithOriginDueToValidationFailure() throws Exception { + FailureAnalysis analysis = performAnalysis( + FieldValidationFailureConfiguration.class, "test.foo.value=4"); + assertThat(analysis.getDescription()) + .contains("Origin: \"test.foo.value\" from property source \"test\""); + } + + @Test + public void bindExceptionDueToUnboundElements() throws Exception { + FailureAnalysis analysis = performAnalysis( + UnboundElementsFailureConfiguration.class, "test.foo.listValue[0]=hello", + "test.foo.listValue[2]=world"); + assertThat(analysis.getDescription()).contains(failure("test.foo.listvalue[2]", + "world", "\"test.foo.listValue[2]\" from property source \"test\"", + "The elements [test.foo.listvalue[2]] were left unbound.")); + } + + @Test + public void bindExceptionDueToOtherFailure() throws Exception { + FailureAnalysis analysis = performAnalysis(GenericFailureConfiguration.class, + "test.foo.value=${BAR}"); + assertThat(analysis.getDescription()).contains(failure("test.foo.value", "${BAR}", + "\"test.foo.value\" from property source \"test\"", + "Could not resolve placeholder 'BAR' in value \"${BAR}\"")); + } + private static String failure(String property, String value, String reason) { return String.format("Property: %s%n Value: %s%n Reason: %s", property, value, reason); } - private FailureAnalysis performAnalysis(Class configuration) { - BeanCreationException failure = createFailure(configuration); + private static String failure(String property, String value, String origin, + String reason) { + return String.format( + "Property: %s%n Value: %s%n Origin: %s%n Reason: %s", property, + value, origin, reason); + } + + private FailureAnalysis performAnalysis(Class configuration, + String... environment) { + BeanCreationException failure = createFailure(configuration, environment); assertThat(failure).isNotNull(); return new BindFailureAnalyzer().analyze(failure); } - private BeanCreationException createFailure(Class configuration) { + private BeanCreationException createFailure(Class configuration, + String... environment) { try { - new AnnotationConfigApplicationContext(configuration).close(); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + addEnvironment(context, environment); + context.register(configuration); + context.refresh(); + context.close(); return null; } catch (BeanCreationException ex) { @@ -97,6 +142,19 @@ public class BindFailureAnalyzerTests { } } + private void addEnvironment(AnnotationConfigApplicationContext context, + String[] environment) { + MutablePropertySources sources = context.getEnvironment().getPropertySources(); + Map map = new HashMap<>(); + for (String pair : environment) { + int index = pair.indexOf("="); + String key = pair.substring(0, index > 0 ? index : pair.length()); + String value = index > 0 ? pair.substring(index + 1) : ""; + map.put(key.trim(), value.trim()); + } + sources.addFirst(new MapPropertySource("test", map)); + } + @EnableConfigurationProperties(FieldValidationFailureProperties.class) static class FieldValidationFailureConfiguration { @@ -107,6 +165,16 @@ public class BindFailureAnalyzerTests { } + @EnableConfigurationProperties(UnboundElementsFailureProperties.class) + static class UnboundElementsFailureConfiguration { + + } + + @EnableConfigurationProperties(GenericFailureProperties.class) + static class GenericFailureConfiguration { + + } + @ConfigurationProperties("test.foo") @Validated static class FieldValidationFailureProperties { @@ -162,6 +230,7 @@ public class BindFailureAnalyzerTests { } @ConfigurationProperties("foo.bar") + @Validated static class ObjectErrorFailureProperties implements Validator { @Override @@ -176,4 +245,33 @@ public class BindFailureAnalyzerTests { } + @ConfigurationProperties("test.foo") + static class UnboundElementsFailureProperties { + + private List listValue; + + public List getListValue() { + return this.listValue; + } + + public void setListValue(List listValue) { + this.listValue = listValue; + } + } + + @ConfigurationProperties("test.foo") + static class GenericFailureProperties { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + } + }