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 new file mode 100644 index 0000000000..a846077aaf --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.diagnostics.analyzer; + +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.validation.FieldError; + +/** + * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a + * {@link BindException}. + * + * @author Andy Wilkinson + */ +class BindFailureAnalyzer extends AbstractFailureAnalyzer { + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) { + if (CollectionUtils.isEmpty(cause.getFieldErrors())) { + return null; + } + StringBuilder description = new StringBuilder( + String.format("Binding to target %s failed:%n", cause.getTarget())); + for (FieldError fieldError : cause.getFieldErrors()) { + description.append(String.format("%n Property: %s", + cause.getObjectName() + "." + fieldError.getField())); + description.append( + String.format("%n Value: %s", fieldError.getRejectedValue())); + description.append( + String.format("%n Reason: %s%n", fieldError.getDefaultMessage())); + } + return new FailureAnalysis(description.toString(), + "Update your application's configuration", cause); + } + +} diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index ca7babae93..a9310150ce 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -34,9 +34,10 @@ org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor # Failure Analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\ +org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\ org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\ -org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer, +org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer # FailureAnalysisReporters org.springframework.boot.diagnostics.FailureAnalysisReporter=\ diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java new file mode 100644 index 0000000000..9940f2a864 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.diagnostics; + +import javax.annotation.PostConstruct; + +import org.junit.Rule; +import org.junit.Test; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.context.embedded.PortInUseException; +import org.springframework.boot.testutil.InternalOutputCapture; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests for {@link FailureAnalyzers} + * + * @author Andy Wilkinson + */ +public class FailureAnalyzersTests { + + @Rule + public InternalOutputCapture outputCapture = new InternalOutputCapture(); + + @Test + public void analysisIsPerformed() { + try { + new SpringApplicationBuilder(TestConfiguration.class).web(false).run(); + fail("Application started successfully"); + } + catch (Exception ex) { + assertThat(this.outputCapture.toString()) + .contains("APPLICATION FAILED TO START"); + } + } + + @Configuration + static class TestConfiguration { + + @PostConstruct + public void fail() { + throw new PortInUseException(8080); + } + + } + +} 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 new file mode 100644 index 0000000000..328978898c --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.diagnostics.analyzer; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import org.junit.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BindFailureAnalyzer}. + * + * @author Andy Wilkinson + */ +public class BindFailureAnalyzerTests { + + @Test + public void bindExceptionDueToValidationFailure() { + FailureAnalysis analysis = performAnalysis(ValidationFailureConfiguration.class); + assertThat(analysis.getDescription()) + .contains(failure("test.foo.foo", "null", "may not be null")); + assertThat(analysis.getDescription()) + .contains(failure("test.foo.value", "0", "at least five")); + assertThat(analysis.getDescription()) + .contains(failure("test.foo.nested.bar", "null", "may not be null")); + } + + 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); + assertThat(failure).isNotNull(); + return new BindFailureAnalyzer().analyze(failure); + } + + private BeanCreationException createFailure(Class configuration) { + try { + new AnnotationConfigApplicationContext(configuration).close(); + return null; + } + catch (BeanCreationException ex) { + return ex; + } + } + + @EnableConfigurationProperties(ValidationFailureProperties.class) + static class ValidationFailureConfiguration { + + } + + @ConfigurationProperties("test.foo") + static class ValidationFailureProperties { + + @NotNull + private String foo; + + @Min(value = 5, message = "at least five") + private int value; + + @Valid + private Nested nested = new Nested(); + + public String getFoo() { + return this.foo; + } + + public void setFoo(String foo) { + this.foo = foo; + } + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + + public Nested getNested() { + return this.nested; + } + + public void setNested(Nested nested) { + this.nested = nested; + } + + static class Nested { + + @NotNull + private String bar; + + public String getBar() { + return this.bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + } + + } + +}