From e5a253e68951dcf975b9844b3b39afac2222b503 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 7 Apr 2016 12:14:22 +0100 Subject: [PATCH] Improve diagnostics when OnBeanCondition type deduction fails When @ConditionalOnBean or @ConditionalOnMissingBean are used on a @Bean method, they will, in the absence of any other configuration, attempt to deduce the bean's type by examining the method's return type. This deduction can fail. See gh-4841, gh-4934, and gh-5624 for some examples of possible failure causes. Previously, this failure was only logged as a debug message leaving the user with a misleading message suggesting that the @ConditionalOnBean or @ConditionalOnMissingBean annotation was not configured correctly. This commit improves the diagnostics by mention the possibility of type deduction in the exception message and including the exception that caused deduction to fail as the cause. Closes gh-4934 --- .../condition/OnBeanCondition.java | 52 ++++++--- ...eanConditionTypeDeductionFailureTests.java | 100 ++++++++++++++++++ 2 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnBeanConditionTypeDeductionFailureTests.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java index 49d0ce2ffb..800f146f95 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java @@ -267,18 +267,32 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit collect(attributes, "annotation", this.annotations); collect(attributes, "ignored", this.ignoredTypes); collect(attributes, "ignoredType", this.ignoredTypes); - if (this.types.isEmpty() && this.names.isEmpty()) { - addDeducedBeanType(context, metadata, this.types); - } this.strategy = (SearchStrategy) metadata .getAnnotationAttributes(annotationType.getName()).get("search"); - validate(); + BeanTypeDeductionException deductionException = null; + try { + if (this.types.isEmpty() && this.names.isEmpty()) { + addDeducedBeanType(context, metadata, this.types); + } + } + catch (BeanTypeDeductionException ex) { + deductionException = ex; + } + validate(deductionException); } - protected void validate() { - Assert.isTrue(hasAtLeastOne(this.types, this.names, this.annotations), - annotationName() + " annotations must " - + "specify at least one bean (type, name or annotation)"); + protected void validate(BeanTypeDeductionException ex) { + if (!hasAtLeastOne(this.types, this.names, this.annotations)) { + String message = annotationName() + + " did not specify a bean using type, name or annotation"; + if (ex == null) { + throw new IllegalStateException(message); + } + else { + throw new IllegalStateException(message + " and the attempt to deduce" + + " the bean's type failed", ex); + } + } } private boolean hasAtLeastOne(List... lists) { @@ -337,12 +351,9 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit }); } catch (Throwable ex) { - // swallow exception and continue - if (logger.isDebugEnabled()) { - logger.debug("Unable to deduce bean type for " - + methodMetadata.getDeclaringClassName() + "." - + methodMetadata.getMethodName(), ex); - } + throw new BeanTypeDeductionException( + methodMetadata.getDeclaringClassName(), + methodMetadata.getMethodName(), ex); } } @@ -404,11 +415,20 @@ class OnBeanCondition extends SpringBootCondition implements ConfigurationCondit } @Override - protected void validate() { + protected void validate(BeanTypeDeductionException ex) { Assert.isTrue(getTypes().size() == 1, annotationName() + " annotations must " + "specify only one type (got " + getTypes() + ")"); - } } + static final class BeanTypeDeductionException extends RuntimeException { + + private BeanTypeDeductionException(String className, String beanMethodName, + Throwable cause) { + super("Failed to deduce bean type for " + className + "." + beanMethodName, + cause); + } + + } + } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnBeanConditionTypeDeductionFailureTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnBeanConditionTypeDeductionFailureTests.java new file mode 100644 index 0000000000..13d5434a59 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/OnBeanConditionTypeDeductionFailureTests.java @@ -0,0 +1,100 @@ +/* + * 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.condition; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.autoconfigure.condition.OnBeanCondition.BeanTypeDeductionException; +import org.springframework.boot.testutil.ClassPathExclusions; +import org.springframework.boot.testutil.FilteredClassPathRunner; +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.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests for {@link OnBeanCondition} when deduction of the bean's type fails + * + * @author Andy Wilkinson + */ +@RunWith(FilteredClassPathRunner.class) +@ClassPathExclusions("jackson-core-*.jar") +public class OnBeanConditionTypeDeductionFailureTests { + + @Test + public void conditionalOnMissingBeanWithDeducedTypeThatIsPartiallyMissingFromClassPath() { + try { + new AnnotationConfigApplicationContext(ImportingConfiguration.class).close(); + fail("Context refresh was successful"); + } + catch (Exception ex) { + ex.printStackTrace(); + Throwable beanTypeDeductionException = findBeanTypeDeductionException(ex); + assertThat(beanTypeDeductionException) + .hasMessage("Failed to deduce bean type for " + + OnMissingBeanConfiguration.class.getName() + + ".objectMapper"); + assertThat(beanTypeDeductionException) + .hasCauseInstanceOf(NoClassDefFoundError.class); + } + } + + private Throwable findBeanTypeDeductionException(Throwable ex) { + Throwable candidate = ex; + while (candidate != null) { + if (candidate instanceof BeanTypeDeductionException) { + return candidate; + } + candidate = candidate.getCause(); + } + return null; + } + + @Configuration + @Import(OnMissingBeanImportSelector.class) + static class ImportingConfiguration { + + } + + @Configuration + static class OnMissingBeanConfiguration { + + @Bean + @ConditionalOnMissingBean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + } + + static class OnMissingBeanImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + return new String[] { OnMissingBeanConfiguration.class.getName() }; + } + + } + +}