Perform failure analysis of NoUniqueBeanDefinitionException

This commit introduces a new failure analyser for
NoUniqueBeanDefinitionException. The analyser provides details of the
consumer whose dependency could not be satisfied and the names and
sources of the non-unique beans.

This analysis requires access to the BeanFactory, so FailureAnalyzers
has been updated to support BeanFactory injection via an analyzer
implementing BeanFactoryAware.

Closes gh-5299
pull/5300/head
Andy Wilkinson 9 years ago
parent 6fde504a63
commit 8d2421938b

@ -819,7 +819,7 @@ public class SpringApplication {
listeners.finished(context, exception); listeners.finished(context, exception);
} }
finally { finally {
reportFailure(exception); reportFailure(exception, context);
if (context != null) { if (context != null) {
context.close(); context.close();
} }
@ -831,9 +831,11 @@ public class SpringApplication {
ReflectionUtils.rethrowRuntimeException(exception); ReflectionUtils.rethrowRuntimeException(exception);
} }
private void reportFailure(Throwable failure) { private void reportFailure(Throwable failure,
ConfigurableApplicationContext context) {
try { try {
if (FailureAnalyzers.analyzeAndReport(failure, getClass().getClassLoader())) { if (FailureAnalyzers.analyzeAndReport(failure, getClass().getClassLoader(),
context)) {
registerLoggedException(failure); registerLoggedException(failure);
return; return;
} }

@ -18,11 +18,19 @@ package org.springframework.boot.diagnostics;
import java.util.List; 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; import org.springframework.core.io.support.SpringFactoriesLoader;
/** /**
* Utility to trigger {@link FailureAnalyzer} and {@link FailureAnalysisReporter} * Utility to trigger {@link FailureAnalyzer} and {@link FailureAnalysisReporter}
* instances loaded from {@code spring.factories}. * instances loaded from {@code spring.factories}.
* <p>
* 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 Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
@ -33,18 +41,20 @@ public final class FailureAnalyzers {
private FailureAnalyzers() { private FailureAnalyzers() {
} }
public static boolean analyzeAndReport(Throwable failure, ClassLoader classLoader) { public static boolean analyzeAndReport(Throwable failure, ClassLoader classLoader,
ConfigurableApplicationContext context) {
List<FailureAnalyzer> analyzers = SpringFactoriesLoader List<FailureAnalyzer> analyzers = SpringFactoriesLoader
.loadFactories(FailureAnalyzer.class, classLoader); .loadFactories(FailureAnalyzer.class, classLoader);
List<FailureAnalysisReporter> reporters = SpringFactoriesLoader List<FailureAnalysisReporter> reporters = SpringFactoriesLoader
.loadFactories(FailureAnalysisReporter.class, classLoader); .loadFactories(FailureAnalysisReporter.class, classLoader);
FailureAnalysis analysis = analyze(failure, analyzers); FailureAnalysis analysis = analyze(failure, analyzers, context);
return report(analysis, reporters); return report(analysis, reporters);
} }
private static FailureAnalysis analyze(Throwable failure, private static FailureAnalysis analyze(Throwable failure,
List<FailureAnalyzer> analyzers) { List<FailureAnalyzer> analyzers, ConfigurableApplicationContext context) {
for (FailureAnalyzer analyzer : analyzers) { for (FailureAnalyzer analyzer : analyzers) {
prepareAnalyzer(context, analyzer);
FailureAnalysis analysis = analyzer.analyze(failure); FailureAnalysis analysis = analyzer.analyze(failure);
if (analysis != null) { if (analysis != null) {
return analysis; return analysis;
@ -53,6 +63,13 @@ public final class FailureAnalyzers {
return null; 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, private static boolean report(FailureAnalysis analysis,
List<FailureAnalysisReporter> reporters) { List<FailureAnalysisReporter> reporters) {
if (analysis == null || reporters.isEmpty()) { if (analysis == null || reporters.isEmpty()) {

@ -28,7 +28,7 @@ import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.util.StringUtils; 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}. * {@link BeanCurrentlyInCreationException}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson

@ -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<NoUniqueBeanDefinitionException>
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;
}
}

@ -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 {
}
}

@ -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 {
}

@ -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 {
}

@ -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 {
}

@ -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) {
}
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.boot.diagnostics.analyzer.nounique.TestBeanConsumer"/>
</beans>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="org.springframework.boot.diagnostics.analyzer.nounique.TestBean" name="xmlTestBean"/>
</beans>
Loading…
Cancel
Save