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-5299pull/5300/head
parent
6fde504a63
commit
8d2421938b
@ -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…
Reference in New Issue