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 @@ + + + + + +