From b450fece2e592618a6ab2287039c9d2b60307435 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 24 Aug 2016 08:57:22 +0200 Subject: [PATCH] Add NoSuchBeanDefinitionException failure analyzer Add a `FailureAnalyzer` that handles the case where the context does not start because no candidate bean was found for an `InjectionPoint`. The implementation inspects the auto-configuration report for beans that are candidate and output the condition(s) that lead to such beans to be discarded on startup. If a whole auto-configuration class is disabled (or excluded), its beans are inspected and candidates are extracted in a similar way. This works for both injection by type and by name. Closes gh-6612 --- .../NoSuchBeanDefinitionFailureAnalyzer.java | 281 +++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + ...uchBeanDefinitionFailureAnalyzerTests.java | 333 ++++++++++++++++++ .../AbstractInjectionFailureAnalyzer.java | 119 +++++++ ...NoUniqueBeanDefinitionFailureAnalyzer.java | 119 ++----- 5 files changed, 756 insertions(+), 97 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java new file mode 100644 index 0000000000..fac0ee9fce --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzer.java @@ -0,0 +1,281 @@ +/* + * 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.autoconfigure.diagnostics.analyzer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.analyzer.AbstractInjectionFailureAnalyzer; +import org.springframework.context.annotation.Bean; +import org.springframework.core.type.MethodMetadata; +import org.springframework.core.type.classreading.CachingMetadataReaderFactory; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * An {@link AbstractInjectionFailureAnalyzer} that performs analysis of failures caused + * by a {@link NoSuchBeanDefinitionException}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +class NoSuchBeanDefinitionFailureAnalyzer + extends AbstractInjectionFailureAnalyzer + implements BeanFactoryAware { + + private ConfigurableListableBeanFactory beanFactory; + + private MetadataReaderFactory metadataReaderFactory; + + private ConditionEvaluationReport report; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory); + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + this.metadataReaderFactory = new CachingMetadataReaderFactory( + this.beanFactory.getBeanClassLoader()); + // Get early as won't be accessible once context has failed to start + this.report = ConditionEvaluationReport.get(this.beanFactory); + } + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, + NoSuchBeanDefinitionException cause, String description) { + if (cause.getNumberOfBeansFound() != 0) { + return null; + } + List autoConfigurationResults = getAutoConfigurationResults( + cause); + StringBuilder message = new StringBuilder(); + message.append(String.format("%s required %s that could not be found.%n", + description == null ? "A component" : description, + getBeanDescription(cause))); + if (!autoConfigurationResults.isEmpty()) { + for (AutoConfigurationResult provider : autoConfigurationResults) { + message.append(String.format("\t- %s%n", provider)); + } + } + String action = String.format("Consider %s %s in your configuration.", + (!autoConfigurationResults.isEmpty() + ? "revisiting the conditions above or defining" : "defining"), + getBeanDescription(cause)); + return new FailureAnalysis(message.toString(), action, cause); + } + + private String getBeanDescription(NoSuchBeanDefinitionException cause) { + if (cause.getBeanType() != null) { + return "a bean of type '" + cause.getBeanType().getName() + "'"; + } + return "a bean named '" + cause.getBeanName() + "'"; + } + + private List getAutoConfigurationResults( + NoSuchBeanDefinitionException cause) { + List results = new ArrayList(); + collectReportedConditionOutcomes(cause, results); + collectExcludedAutoConfiguration(cause, results); + return results; + } + + private void collectReportedConditionOutcomes(NoSuchBeanDefinitionException cause, + List results) { + for (Map.Entry entry : this.report + .getConditionAndOutcomesBySource().entrySet()) { + Source source = new Source(entry.getKey()); + ConditionAndOutcomes conditionAndOutcomes = entry.getValue(); + if (!conditionAndOutcomes.isFullMatch()) { + BeanMethods methods = new BeanMethods(source, cause); + for (ConditionAndOutcome conditionAndOutcome : conditionAndOutcomes) { + if (!conditionAndOutcome.getOutcome().isMatch()) { + for (MethodMetadata method : methods) { + results.add(new AutoConfigurationResult(method, + conditionAndOutcome.getOutcome(), source.isMethod())); + } + } + } + } + } + } + + private void collectExcludedAutoConfiguration(NoSuchBeanDefinitionException cause, + List results) { + for (String excludedClass : this.report.getExclusions()) { + Source source = new Source(excludedClass); + BeanMethods methods = new BeanMethods(source, cause); + for (MethodMetadata method : methods) { + String message = String.format("auto-configuration '%s' was excluded", + ClassUtils.getShortName(excludedClass)); + results.add(new AutoConfigurationResult(method, + new ConditionOutcome(false, message), false)); + } + } + } + + private class Source { + + private final String className; + + private final String methodName; + + Source(String source) { + String[] tokens = source.split("#"); + this.className = (tokens.length > 1 ? tokens[0] : source); + this.methodName = (tokens.length == 2 ? tokens[1] : null); + } + + public String getClassName() { + return this.className; + } + + public String getMethodName() { + return this.methodName; + } + + public boolean isMethod() { + return this.methodName != null; + } + + } + + private class BeanMethods implements Iterable { + + private final List methods; + + BeanMethods(Source source, NoSuchBeanDefinitionException cause) { + this.methods = findBeanMethods(source, cause); + } + + private List findBeanMethods(Source source, + NoSuchBeanDefinitionException cause) { + try { + MetadataReader classMetadata = NoSuchBeanDefinitionFailureAnalyzer.this.metadataReaderFactory + .getMetadataReader(source.getClassName()); + Set candidates = classMetadata.getAnnotationMetadata() + .getAnnotatedMethods(Bean.class.getName()); + List result = new ArrayList(); + for (MethodMetadata candidate : candidates) { + if (isMatch(candidate, source, cause)) { + result.add(candidate); + } + } + return Collections.unmodifiableList(result); + } + catch (Exception ex) { + return Collections.emptyList(); + } + } + + private boolean isMatch(MethodMetadata candidate, Source source, + NoSuchBeanDefinitionException cause) { + if (source.getMethodName() != null + && !source.getMethodName().equals(candidate.getMethodName())) { + return false; + } + String name = cause.getBeanName(); + Class type = cause.getBeanType(); + return ((name != null && hasName(candidate, name)) + || (type != null && hasType(candidate, type))); + } + + private boolean hasName(MethodMetadata methodMetadata, String name) { + Map attributes = methodMetadata + .getAnnotationAttributes(Bean.class.getName()); + String[] candidates = (attributes == null ? null + : (String[]) attributes.get("name")); + if (candidates != null) { + for (String candidate : candidates) { + if (candidate.equals(name)) { + return true; + } + } + return false; + } + return methodMetadata.getMethodName().equals(name); + } + + private boolean hasType(MethodMetadata candidate, Class type) { + String returnTypeName = candidate.getReturnTypeName(); + if (type.getName().equals(returnTypeName)) { + return true; + } + try { + Class returnType = ClassUtils.forName(returnTypeName, + NoSuchBeanDefinitionFailureAnalyzer.this.beanFactory + .getBeanClassLoader()); + return type.isAssignableFrom(returnType); + } + catch (Throwable ex) { + return false; + } + } + + @Override + public Iterator iterator() { + return this.methods.iterator(); + } + + } + + private class AutoConfigurationResult { + + private final MethodMetadata methodMetadata; + + private final ConditionOutcome conditionOutcome; + + private final boolean methodEvaluated; + + AutoConfigurationResult(MethodMetadata methodMetadata, + ConditionOutcome conditionOutcome, boolean methodEvaluated) { + this.methodMetadata = methodMetadata; + this.conditionOutcome = conditionOutcome; + this.methodEvaluated = methodEvaluated; + } + + @Override + public String toString() { + if (this.methodEvaluated) { + return String.format("Bean method '%s' in '%s' not loaded because %s", + this.methodMetadata.getMethodName(), + ClassUtils.getShortName( + this.methodMetadata.getDeclaringClassName()), + this.conditionOutcome.getMessage()); + } + return String.format("Bean method '%s' not loaded because %s", + this.methodMetadata.getMethodName(), + this.conditionOutcome.getMessage()); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 38b9d1d5ac..62f5a8b2df 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -104,6 +104,7 @@ org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration # Failure analyzers org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java new file mode 100644 index 0000000000..3e27ce0cc5 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/diagnostics/analyzer/NoSuchBeanDefinitionFailureAnalyzerTests.java @@ -0,0 +1,333 @@ +/* + * 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.autoconfigure.diagnostics.analyzer; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoUniqueBeanDefinitionException; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter; +import org.springframework.boot.test.util.EnvironmentTestUtils; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NoSuchBeanDefinitionFailureAnalyzer}. + * + * @author Stephane Nicoll + */ +public class NoSuchBeanDefinitionFailureAnalyzerTests { + + private final NoSuchBeanDefinitionFailureAnalyzer analyzer = new NoSuchBeanDefinitionFailureAnalyzer(); + + @Test + public void failureAnalysisForMultipleBeans() { + FailureAnalysis analysis = analyzeFailure( + new NoUniqueBeanDefinitionException(String.class, 2, "Test")); + assertThat(analysis).isNull(); + } + + @Test + public void failureAnalysisForNoMatchType() { + FailureAnalysis analysis = analyzeFailure(createFailure(StringHandler.class)); + assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0, + String.class); + assertThat(analysis.getDescription()).doesNotContain( + "No matching auto-configuration has been found for this type."); + assertThat(analysis.getAction()).startsWith(String.format( + "Consider defining a bean of type '%s' in your configuration.", + String.class.getName())); + } + + @Test + public void failureAnalysisForMissingPropertyExactType() { + FailureAnalysis analysis = analyzeFailure( + createFailure(StringPropertyTypeConfiguration.class)); + assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0, + String.class); + assertBeanMethodDisabled(analysis, + "did not find property 'spring.string.enabled'", + TestPropertyAutoConfiguration.class, "string"); + assertActionMissingType(analysis, String.class); + } + + @Test + public void failureAnalysisForMissingPropertySubType() { + FailureAnalysis analysis = analyzeFailure( + createFailure(IntegerPropertyTypeConfiguration.class)); + assertThat(analysis).isNotNull(); + assertDescriptionConstructorMissingType(analysis, NumberHandler.class, 0, + Number.class); + assertBeanMethodDisabled(analysis, + "did not find property 'spring.integer.enabled'", + TestPropertyAutoConfiguration.class, "integer"); + assertActionMissingType(analysis, Number.class); + } + + @Test + public void failureAnalysisForMissingClassOnAutoConfigurationType() { + FailureAnalysis analysis = analyzeFailure( + createFailure(MissingClassOnAutoConfigurationConfiguration.class)); + assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0, + String.class); + assertClassDisabled(analysis, "did not find required class 'com.example.FooBar'", + "string"); + assertActionMissingType(analysis, String.class); + } + + @Test + public void failureAnalysisForExcludedAutoConfigurationType() { + FatalBeanException failure = createFailure(StringHandler.class); + addExclusions(this.analyzer, TestPropertyAutoConfiguration.class); + FailureAnalysis analysis = analyzeFailure(failure); + assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0, + String.class); + String configClass = ClassUtils + .getShortName(TestPropertyAutoConfiguration.class.getName()); + assertClassDisabled(analysis, + String.format("auto-configuration '%s' was excluded", configClass), + "string"); + assertActionMissingType(analysis, String.class); + } + + @Test + public void failureAnalysisForSeveralConditionsType() { + FailureAnalysis analysis = analyzeFailure( + createFailure(SeveralAutoConfigurationTypeConfiguration.class)); + assertDescriptionConstructorMissingType(analysis, StringHandler.class, 0, + String.class); + assertBeanMethodDisabled(analysis, + "did not find property 'spring.string.enabled'", + TestPropertyAutoConfiguration.class, "string"); + assertClassDisabled(analysis, "did not find required class 'com.example.FooBar'", + "string"); + assertActionMissingType(analysis, String.class); + } + + @Test + public void failureAnalysisForNoMatchName() { + FailureAnalysis analysis = analyzeFailure(createFailure(StringNameHandler.class)); + assertThat(analysis.getDescription()).startsWith(String.format( + "Constructor in %s required a bean named '%s' that could not be found", + StringNameHandler.class.getName(), "test-string")); + assertThat(analysis.getDescription().contains( + "No matching auto-configuration has been found for this bean name.")); + assertThat(analysis.getAction()).startsWith(String.format( + "Consider defining a bean named '%s' in your configuration.", + "test-string")); + } + + @Test + public void failureAnalysisForMissingBeanName() { + FailureAnalysis analysis = analyzeFailure( + createFailure(StringMissingBeanNameConfiguration.class)); + assertThat(analysis.getDescription()).startsWith(String.format( + "Constructor in %s required a bean named '%s' that could not be found", + StringNameHandler.class.getName(), "test-string")); + assertBeanMethodDisabled(analysis, + "@ConditionalOnBean (types: java.lang.Integer; SearchStrategy: all) did not find any beans", + TestMissingBeanAutoConfiguration.class, "string"); + assertActionMissingName(analysis, "test-string"); + } + + private void assertDescriptionConstructorMissingType(FailureAnalysis analysis, + Class component, int index, Class type) { + String expected = String.format( + "Parameter %s of constructor in %s required a bean of " + + "type '%s' that could not be found.", + index, component.getName(), type.getName()); + assertThat(analysis.getDescription()).startsWith(expected); + } + + private void assertActionMissingType(FailureAnalysis analysis, Class type) { + assertThat(analysis.getAction()).startsWith(String + .format("Consider revisiting the conditions above or defining a bean of type '%s' " + + "in your configuration.", type.getName())); + } + + private void assertActionMissingName(FailureAnalysis analysis, String name) { + assertThat(analysis.getAction()).startsWith(String + .format("Consider revisiting the conditions above or defining a bean named '%s' " + + "in your configuration.", name)); + } + + private void assertBeanMethodDisabled(FailureAnalysis analysis, String description, + Class target, String methodName) { + String expected = String.format("Bean method '%s' in '%s' not loaded because", + methodName, ClassUtils.getShortName(target), description); + assertThat(analysis.getDescription()).contains(expected); + assertThat(analysis.getDescription()).contains(description); + } + + private void assertClassDisabled(FailureAnalysis analysis, String description, + String methodName) { + String expected = String.format("Bean method '%s' not loaded because", methodName, + description); + assertThat(analysis.getDescription()).contains(expected); + assertThat(analysis.getDescription()).contains(description); + } + + private static void addExclusions(NoSuchBeanDefinitionFailureAnalyzer analyzer, + Class... classes) { + ConditionEvaluationReport report = (ConditionEvaluationReport) new DirectFieldAccessor( + analyzer).getPropertyValue("report"); + List exclusions = new ArrayList(report.getExclusions()); + for (Class c : classes) { + exclusions.add(c.getName()); + } + report.recordExclusions(exclusions); + } + + private FatalBeanException createFailure(Class config, String... environment) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + this.analyzer.setBeanFactory(context.getBeanFactory()); + EnvironmentTestUtils.addEnvironment(context, environment); + context.register(config); + try { + context.refresh(); + return null; + } + catch (FatalBeanException ex) { + return ex; + } + finally { + context.close(); + } + } + + private FailureAnalysis analyzeFailure(Exception failure) { + FailureAnalysis analysis = this.analyzer.analyze(failure); + if (analysis != null) { + new LoggingFailureAnalysisReporter().report(analysis); + } + return analysis; + } + + @Configuration + @ImportAutoConfiguration(TestPropertyAutoConfiguration.class) + @Import(StringHandler.class) + protected static class StringPropertyTypeConfiguration { + + } + + @Configuration + @ImportAutoConfiguration(TestPropertyAutoConfiguration.class) + @Import(NumberHandler.class) + protected static class IntegerPropertyTypeConfiguration { + + } + + @Configuration + @ImportAutoConfiguration(TestTypeClassAutoConfiguration.class) + @Import(StringHandler.class) + protected static class MissingClassOnAutoConfigurationConfiguration { + + } + + @Configuration + @ImportAutoConfiguration({ TestPropertyAutoConfiguration.class, + TestTypeClassAutoConfiguration.class }) + @Import(StringHandler.class) + protected static class SeveralAutoConfigurationTypeConfiguration { + + } + + @Configuration + @ImportAutoConfiguration(TestMissingBeanAutoConfiguration.class) + @Import(StringNameHandler.class) + protected static class StringMissingBeanNameConfiguration { + + } + + @Configuration + public static class TestPropertyAutoConfiguration { + + @ConditionalOnProperty("spring.string.enabled") + @Bean + public String string() { + return "Test"; + } + + @ConditionalOnProperty("spring.integer.enabled") + @Bean + public Integer integer() { + return 42; + } + + } + + @Configuration + @ConditionalOnClass(name = "com.example.FooBar") + public static class TestTypeClassAutoConfiguration { + + @Bean + public String string() { + return "Test"; + } + + } + + @Configuration + public static class TestMissingBeanAutoConfiguration { + + @ConditionalOnBean(Integer.class) + @Bean(name = "test-string") + public String string() { + return "Test"; + } + + } + + protected static class StringHandler { + + public StringHandler(String foo) { + } + + } + + protected static class NumberHandler { + + public NumberHandler(Number foo) { + } + + } + + protected static class StringNameHandler { + + public StringNameHandler(BeanFactory beanFactory) { + beanFactory.getBean("test-string"); + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java new file mode 100644 index 0000000000..f0ddc236d3 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/AbstractInjectionFailureAnalyzer.java @@ -0,0 +1,119 @@ +/* + * 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.BeanInstantiationException; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.diagnostics.FailureAnalyzer; +import org.springframework.util.ClassUtils; + +/** + * Abstract base class for a {@link FailureAnalyzer} that handles some kind of injection + * failure. + * + * @param the type of exception to analyze + * @author Andy Wilkinson + * @author Stephane Nicoll + * @since 1.4.1 + */ +public abstract class AbstractInjectionFailureAnalyzer + extends AbstractFailureAnalyzer { + + @Override + protected final FailureAnalysis analyze(Throwable rootFailure, T cause) { + return analyze(rootFailure, cause, getDescription(rootFailure)); + } + + private String getDescription(Throwable rootFailure) { + UnsatisfiedDependencyException unsatisfiedDependency = findMostNestedCause( + rootFailure, UnsatisfiedDependencyException.class); + if (unsatisfiedDependency != null) { + return getDescription(unsatisfiedDependency); + } + BeanInstantiationException beanInstantiationException = findMostNestedCause( + rootFailure, BeanInstantiationException.class); + if (beanInstantiationException != null) { + return getDescription(beanInstantiationException); + } + return null; + } + + @SuppressWarnings("unchecked") + private C findMostNestedCause(Throwable root, Class type) { + Throwable candidate = root; + C result = null; + while (candidate != null) { + if (type.isAssignableFrom(candidate.getClass())) { + result = (C) candidate; + } + candidate = candidate.getCause(); + } + return result; + } + + private String getDescription(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 getDescription(BeanInstantiationException ex) { + if (ex.getConstructingMethod() != null) { + return String.format("Method %s in %s", ex.getConstructingMethod().getName(), + ex.getConstructingMethod().getDeclaringClass().getName()); + } + if (ex.getConstructor() != null) { + return String.format("Constructor in %s", ClassUtils + .getUserClass(ex.getConstructor().getDeclaringClass()).getName()); + } + return ex.getBeanClass().getName(); + } + + /** + * Returns an analysis of the given {@code failure}, or {@code null} if no analysis + * was possible. + * @param rootFailure the root failure passed to the analyzer + * @param cause the actual found cause + * @param description the description of the injection point or {@code null} + * @return the analysis or {@code null} + */ + protected abstract FailureAnalysis analyze(Throwable rootFailure, T cause, + String description); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java index 61cbfdcd40..210f0d5537 100644 --- a/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java +++ b/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/NoUniqueBeanDefinitionFailureAnalyzer.java @@ -16,30 +16,25 @@ package org.springframework.boot.diagnostics.analyzer; -import org.springframework.beans.BeanInstantiationException; 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.ClassUtils; import org.springframework.util.StringUtils; /** - * An {@link AbstractFailureAnalyzer} that performs analysis of failures caused by a - * {@link NoUniqueBeanDefinitionException}. + * An {@link AbstractInjectionFailureAnalyzer} that performs analysis of failures caused + * by a {@link NoUniqueBeanDefinitionException}. * * @author Andy Wilkinson */ class NoUniqueBeanDefinitionFailureAnalyzer - extends AbstractFailureAnalyzer + extends AbstractInjectionFailureAnalyzer implements BeanFactoryAware { private ConfigurableBeanFactory beanFactory; @@ -52,9 +47,8 @@ class NoUniqueBeanDefinitionFailureAnalyzer @Override protected FailureAnalysis analyze(Throwable rootFailure, - NoUniqueBeanDefinitionException cause) { - String consumerDescription = getConsumerDescription(rootFailure); - if (consumerDescription == null) { + NoUniqueBeanDefinitionException cause, String description) { + if (description == null) { return null; } String[] beanNames = extractBeanNames(cause); @@ -63,26 +57,9 @@ class NoUniqueBeanDefinitionFailureAnalyzer } StringBuilder message = new StringBuilder(); message.append(String.format("%s required a single bean, but %d were found:%n", - consumerDescription, beanNames.length)); + description, beanNames.length)); for (String beanName : beanNames) { - 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 programmatically registered singleton", beanName)); - } - + buildMessage(message, beanName); } return new FailureAnalysis(message.toString(), "Consider marking one of the beans as @Primary, updating the consumer to" @@ -91,78 +68,26 @@ class NoUniqueBeanDefinitionFailureAnalyzer cause); } - private String getConsumerDescription(Throwable ex) { - UnsatisfiedDependencyException unsatisfiedDependency = findUnsatisfiedDependencyException( - ex); - if (unsatisfiedDependency != null) { - return getConsumerDescription(unsatisfiedDependency); + private void buildMessage(StringBuilder message, String beanName) { + try { + BeanDefinition definition = this.beanFactory + .getMergedBeanDefinition(beanName); + message.append(getDefinitionDescription(beanName, definition)); } - BeanInstantiationException beanInstantiationException = findBeanInstantiationException( - ex); - if (beanInstantiationException != null) { - return getConsumerDescription(beanInstantiationException); + catch (NoSuchBeanDefinitionException ex) { + message.append(String + .format("\t- %s: a programmatically registered singleton", beanName)); } - return null; - } - - private UnsatisfiedDependencyException findUnsatisfiedDependencyException( - Throwable root) { - return findMostNestedCause(root, UnsatisfiedDependencyException.class); } - private BeanInstantiationException findBeanInstantiationException(Throwable root) { - return findMostNestedCause(root, BeanInstantiationException.class); - } - - @SuppressWarnings("unchecked") - private T findMostNestedCause(Throwable root, - Class causeType) { - Throwable candidate = root; - T mostNestedMatch = null; - while (candidate != null) { - if (causeType.isAssignableFrom(candidate.getClass())) { - mostNestedMatch = (T) 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 getConsumerDescription(BeanInstantiationException ex) { - if (ex.getConstructingMethod() != null) { - return String.format("Method %s in %s", ex.getConstructingMethod().getName(), - ex.getConstructingMethod().getDeclaringClass().getName()); - } - if (ex.getConstructor() != null) { - return String.format("Constructor in %s", ClassUtils - .getUserClass(ex.getConstructor().getDeclaringClass()).getName()); + private String getDefinitionDescription(String beanName, BeanDefinition definition) { + if (StringUtils.hasText(definition.getFactoryMethodName())) { + return String.format("\t- %s: defined by method '%s' in %s%n", beanName, + definition.getFactoryMethodName(), + definition.getResourceDescription()); } - return ex.getBeanClass().getName(); + return String.format("\t- %s: defined in %s%n", beanName, + definition.getResourceDescription()); } private String[] extractBeanNames(NoUniqueBeanDefinitionException cause) {