diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java
index 5cc03c0431..e07bd354e5 100644
--- a/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java
+++ b/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java
@@ -819,7 +819,7 @@ public class SpringApplication {
listeners.finished(context, exception);
}
finally {
- reportFailure(exception);
+ reportFailure(exception, context);
if (context != null) {
context.close();
}
@@ -831,9 +831,11 @@ public class SpringApplication {
ReflectionUtils.rethrowRuntimeException(exception);
}
- private void reportFailure(Throwable failure) {
+ private void reportFailure(Throwable failure,
+ ConfigurableApplicationContext context) {
try {
- if (FailureAnalyzers.analyzeAndReport(failure, getClass().getClassLoader())) {
+ if (FailureAnalyzers.analyzeAndReport(failure, getClass().getClassLoader(),
+ context)) {
registerLoggedException(failure);
return;
}
diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java
index dcdf49ca21..e4a2e04a14 100644
--- a/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java
+++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java
@@ -18,11 +18,19 @@ package org.springframework.boot.diagnostics;
import java.util.List;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Utility to trigger {@link FailureAnalyzer} and {@link FailureAnalysisReporter}
* instances loaded from {@code spring.factories}.
+ *
+ * A {@code FailureAnalyzer} that requires access to the {@link BeanFactory} in order to
+ * perform its analysis can implement {@code BeanFactoryAware} to have the
+ * {@code BeanFactory} injected prior to {@link FailureAnalyzer#analyze(Throwable)} being
+ * called.
*
* @author Andy Wilkinson
* @author Phillip Webb
@@ -33,18 +41,20 @@ public final class FailureAnalyzers {
private FailureAnalyzers() {
}
- public static boolean analyzeAndReport(Throwable failure, ClassLoader classLoader) {
+ public static boolean analyzeAndReport(Throwable failure, ClassLoader classLoader,
+ ConfigurableApplicationContext context) {
List analyzers = SpringFactoriesLoader
.loadFactories(FailureAnalyzer.class, classLoader);
List reporters = SpringFactoriesLoader
.loadFactories(FailureAnalysisReporter.class, classLoader);
- FailureAnalysis analysis = analyze(failure, analyzers);
+ FailureAnalysis analysis = analyze(failure, analyzers, context);
return report(analysis, reporters);
}
private static FailureAnalysis analyze(Throwable failure,
- List analyzers) {
+ List analyzers, ConfigurableApplicationContext context) {
for (FailureAnalyzer analyzer : analyzers) {
+ prepareAnalyzer(context, analyzer);
FailureAnalysis analysis = analyzer.analyze(failure);
if (analysis != null) {
return analysis;
@@ -53,6 +63,13 @@ public final class FailureAnalyzers {
return null;
}
+ private static void prepareAnalyzer(ConfigurableApplicationContext context,
+ FailureAnalyzer analyzer) {
+ if (analyzer instanceof BeanFactoryAware) {
+ ((BeanFactoryAware) analyzer).setBeanFactory(context.getBeanFactory());
+ }
+ }
+
private static boolean report(FailureAnalysis analysis,
List reporters) {
if (analysis == null || reporters.isEmpty()) {
diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java
index 0877739dbf..b5008c5826 100644
--- a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java
+++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BeanCurrentlyInCreationFailureAnalyzer.java
@@ -28,7 +28,7 @@ import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.util.StringUtils;
/**
- * A {@link AbstractFailureAnalyzer} the performs analysis of failures caused by a
+ * An {@link AbstractFailureAnalyzer} the performs analysis of failures caused by a
* {@link BeanCurrentlyInCreationException}.
*
* @author Andy Wilkinson
diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionExceptionFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionExceptionFailureAnalyzer.java
new file mode 100644
index 0000000000..811eebc8e9
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionExceptionFailureAnalyzer.java
@@ -0,0 +1,140 @@
+/*
+ * 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.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.InjectionPoint;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
+import org.springframework.beans.factory.UnsatisfiedDependencyException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
+import org.springframework.boot.diagnostics.FailureAnalysis;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * An {@link AbstractFailureAnalyzer} the performs analysis of failures caused by a
+ * {@link NoUniqueBeanDefinitionException}.
+ *
+ * @author Andy Wilkinson
+ */
+class NoUniqueBeanDefinitionExceptionFailureAnalyzer
+ extends AbstractFailureAnalyzer
+ implements BeanFactoryAware {
+
+ private ConfigurableBeanFactory beanFactory;
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ Assert.isInstanceOf(ConfigurableBeanFactory.class, beanFactory);
+ this.beanFactory = (ConfigurableBeanFactory) beanFactory;
+ }
+
+ @Override
+ protected FailureAnalysis analyze(Throwable rootFailure,
+ NoUniqueBeanDefinitionException cause) {
+ UnsatisfiedDependencyException unsatisfiedDependency = findUnsatisfiedDependencyException(
+ rootFailure);
+ if (unsatisfiedDependency == null) {
+ return null;
+ }
+ String[] beanNames = extractBeanNames(cause);
+ if (beanNames == null) {
+ return null;
+ }
+ StringBuilder message = new StringBuilder();
+ message.append(String.format("%s required a single bean, but %d were found:%n",
+ getConsumerDescription(unsatisfiedDependency), beanNames.length));
+ for (String beanName : beanNames) {
+ unsatisfiedDependency.getInjectionPoint();
+ try {
+ BeanDefinition beanDefinition = this.beanFactory
+ .getMergedBeanDefinition(beanName);
+ if (StringUtils.hasText(beanDefinition.getFactoryMethodName())) {
+ message.append(String.format("\t- %s: defined by method '%s' in %s%n",
+ beanName, beanDefinition.getFactoryMethodName(),
+ beanDefinition.getResourceDescription()));
+ }
+ else {
+ message.append(String.format("\t- %s: defined in %s%n", beanName,
+ beanDefinition.getResourceDescription()));
+ }
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ message.append(String.format(
+ "\t- %s: a programtically registered singleton", beanName));
+ }
+
+ }
+ return new FailureAnalysis(message.toString(),
+ "Consider marking one of the beans as @Primary, updating the consumer to"
+ + " accept multiple beans, or using @Qualifer to identify the"
+ + " bean that should be consumed",
+ cause);
+ }
+
+ private UnsatisfiedDependencyException findUnsatisfiedDependencyException(
+ Throwable root) {
+ Throwable candidate = root;
+ UnsatisfiedDependencyException mostNestedMatch = null;
+ while (candidate != null) {
+ if (candidate instanceof UnsatisfiedDependencyException) {
+ mostNestedMatch = (UnsatisfiedDependencyException) candidate;
+ }
+ candidate = candidate.getCause();
+ }
+ return mostNestedMatch;
+ }
+
+ private String getConsumerDescription(UnsatisfiedDependencyException ex) {
+ InjectionPoint injectionPoint = ex.getInjectionPoint();
+ if (injectionPoint != null) {
+ if (injectionPoint.getField() != null) {
+ return String.format("Field '%s' in %s",
+ injectionPoint.getField().getName(),
+ injectionPoint.getField().getDeclaringClass().getName());
+ }
+ if (injectionPoint.getMethodParameter() != null) {
+ if (injectionPoint.getMethodParameter().getConstructor() != null) {
+ return String.format("Parameter %d of constructor in %s",
+ injectionPoint.getMethodParameter().getParameterIndex(),
+ injectionPoint.getMethodParameter().getDeclaringClass()
+ .getName());
+ }
+ return String.format("Parameter %d of method '%s' in %s",
+ injectionPoint.getMethodParameter().getParameterIndex(),
+ injectionPoint.getMethodParameter().getMethod().getName(),
+ injectionPoint.getMethodParameter().getDeclaringClass()
+ .getName());
+ }
+ }
+ return ex.getResourceDescription();
+ }
+
+ private String[] extractBeanNames(NoUniqueBeanDefinitionException cause) {
+ if (cause.getMessage().indexOf("but found") > -1) {
+ return StringUtils.commaDelimitedListToStringArray(cause.getMessage()
+ .substring(cause.getMessage().lastIndexOf(":") + 1).trim());
+ }
+ return null;
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionExceptionFailureAnalyzerTests.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionExceptionFailureAnalyzerTests.java
new file mode 100644
index 0000000000..24941f2c39
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionExceptionFailureAnalyzerTests.java
@@ -0,0 +1,163 @@
+/*
+ * 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.junit.Test;
+
+import org.springframework.beans.factory.UnsatisfiedDependencyException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.diagnostics.FailureAnalysis;
+import org.springframework.boot.diagnostics.analyzer.nounique.TestBean;
+import org.springframework.boot.diagnostics.analyzer.nounique.TestBeanConsumer;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ImportResource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link NoUniqueBeanDefinitionExceptionFailureAnalyzer}.
+ *
+ * @author Andy Wilkinson
+ */
+public class NoUniqueBeanDefinitionExceptionFailureAnalyzerTests {
+
+ private final NoUniqueBeanDefinitionExceptionFailureAnalyzer analyzer = new NoUniqueBeanDefinitionExceptionFailureAnalyzer();
+
+ @Test
+ public void failureAnalysisForFieldConsumer() {
+ FailureAnalysis failureAnalysis = analyzeFailure(
+ createFailure(FieldConsumer.class));
+ System.out.println(failureAnalysis.getDescription());
+ assertThat(failureAnalysis.getDescription())
+ .startsWith("Field 'testBean' in " + FieldConsumer.class.getName()
+ + " required a single bean, but 6 were found:");
+ assertFoundBeans(failureAnalysis);
+ }
+
+ @Test
+ public void failureAnalysisForMethodConsumer() {
+ FailureAnalysis failureAnalysis = analyzeFailure(
+ createFailure(MethodConsumer.class));
+ System.out.println(failureAnalysis.getDescription());
+ assertThat(failureAnalysis.getDescription()).startsWith(
+ "Parameter 0 of method 'consumer' in " + MethodConsumer.class.getName()
+ + " required a single bean, but 6 were found:");
+ assertFoundBeans(failureAnalysis);
+ }
+
+ @Test
+ public void failureAnalysisForXmlConsumer() {
+ FailureAnalysis failureAnalysis = analyzeFailure(
+ createFailure(XmlConsumer.class));
+ System.out.println(failureAnalysis.getDescription());
+ assertThat(failureAnalysis.getDescription()).startsWith(
+ "Parameter 0 of constructor in " + TestBeanConsumer.class.getName()
+ + " required a single bean, but 6 were found:");
+ assertFoundBeans(failureAnalysis);
+ }
+
+ private UnsatisfiedDependencyException createFailure(Class> consumer) {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.register(DuplicateBeansProducer.class, consumer);
+ context.setParent(new AnnotationConfigApplicationContext(ParentProducer.class));
+ try {
+ context.refresh();
+ return null;
+ }
+ catch (UnsatisfiedDependencyException ex) {
+ this.analyzer.setBeanFactory(context.getBeanFactory());
+ return ex;
+ }
+ finally {
+ context.close();
+ }
+ }
+
+ private FailureAnalysis analyzeFailure(UnsatisfiedDependencyException failure) {
+ return this.analyzer.analyze(failure);
+ }
+
+ private void assertFoundBeans(FailureAnalysis analysis) {
+ assertThat(analysis.getDescription())
+ .contains("beanOne: defined by method 'beanOne' in "
+ + DuplicateBeansProducer.class.getName());
+ assertThat(analysis.getDescription())
+ .contains("beanTwo: defined by method 'beanTwo' in "
+ + DuplicateBeansProducer.class.getName());
+ assertThat(analysis.getDescription())
+ .contains("beanThree: defined by method 'beanThree' in "
+ + ParentProducer.class.getName());
+ assertThat(analysis.getDescription()).contains("barTestBean");
+ assertThat(analysis.getDescription()).contains("fooTestBean");
+ assertThat(analysis.getDescription()).contains("xmlTestBean");
+ }
+
+ @Configuration
+ @ComponentScan(basePackageClasses = TestBean.class)
+ @ImportResource("/org/springframework/boot/diagnostics/analyzer/nounique/producer.xml")
+ static class DuplicateBeansProducer {
+
+ @Bean
+ TestBean beanOne() {
+ return new TestBean();
+ }
+
+ @Bean
+ TestBean beanTwo() {
+ return new TestBean();
+ }
+
+ }
+
+ static class ParentProducer {
+
+ @Bean
+ TestBean beanThree() {
+ return new TestBean();
+ }
+
+ }
+
+ @Configuration
+ static class FieldConsumer {
+
+ @SuppressWarnings("unused")
+ @Autowired
+ private TestBean testBean;
+
+ }
+
+ @Configuration
+ static class MethodConsumer {
+
+ @Bean
+ String consumer(TestBean testBean) {
+ return "foo";
+ }
+
+ }
+
+ @Configuration
+ @ImportResource("/org/springframework/boot/diagnostics/analyzer/nounique/consumer.xml")
+ static class XmlConsumer {
+
+ }
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/BarTestBean.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/BarTestBean.java
new file mode 100644
index 0000000000..a11433bfdd
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/BarTestBean.java
@@ -0,0 +1,24 @@
+/*
+ * 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.nounique;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class BarTestBean extends TestBean {
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/FooTestBean.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/FooTestBean.java
new file mode 100644
index 0000000000..8097a95b1c
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/FooTestBean.java
@@ -0,0 +1,24 @@
+/*
+ * 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.nounique;
+
+import org.springframework.stereotype.Component;
+
+@Component
+public class FooTestBean extends TestBean {
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/TestBean.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/TestBean.java
new file mode 100644
index 0000000000..bfcefe49cb
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/TestBean.java
@@ -0,0 +1,21 @@
+/*
+ * 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.nounique;
+
+public class TestBean {
+
+}
diff --git a/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/TestBeanConsumer.java b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/TestBeanConsumer.java
new file mode 100644
index 0000000000..0c01a3b1d4
--- /dev/null
+++ b/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/nounique/TestBeanConsumer.java
@@ -0,0 +1,25 @@
+/*
+ * 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.nounique;
+
+public class TestBeanConsumer {
+
+ TestBeanConsumer(TestBean testBean) {
+
+ }
+
+}
diff --git a/spring-boot/src/test/resources/org/springframework/boot/diagnostics/analyzer/nounique/consumer.xml b/spring-boot/src/test/resources/org/springframework/boot/diagnostics/analyzer/nounique/consumer.xml
new file mode 100644
index 0000000000..9440e1af13
--- /dev/null
+++ b/spring-boot/src/test/resources/org/springframework/boot/diagnostics/analyzer/nounique/consumer.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/spring-boot/src/test/resources/org/springframework/boot/diagnostics/analyzer/nounique/producer.xml b/spring-boot/src/test/resources/org/springframework/boot/diagnostics/analyzer/nounique/producer.xml
new file mode 100644
index 0000000000..3b28fb030d
--- /dev/null
+++ b/spring-boot/src/test/resources/org/springframework/boot/diagnostics/analyzer/nounique/producer.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+