From 9f858e759c2bd69018b54e378b2dc982dcebea2c Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 23 Oct 2018 17:49:01 -0700 Subject: [PATCH] Support parameterizedContainer in bean conditions Add a `parameterizedContainer` attribute to `ConditionalOnBean` and `ConditionalOnMissingBean` which can be used to support generic types when checking for the presence of beans. Closes gh-14940 --- .../condition/BeanTypeRegistry.java | 224 ++++++----- .../condition/ConditionalOnBean.java | 10 + .../condition/ConditionalOnMissingBean.java | 10 + .../condition/OnBeanCondition.java | 360 ++++++++++++------ .../condition/ConditionalOnBeanTests.java | 219 +++++++++-- .../ConditionalOnMissingBeanTests.java | 215 +++++++++-- .../condition/TestParameterizedContainer.java | 28 ++ 7 files changed, 799 insertions(+), 267 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/TestParameterizedContainer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/BeanTypeRegistry.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/BeanTypeRegistry.java index 526288fd64..f7e67e300a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/BeanTypeRegistry.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/BeanTypeRegistry.java @@ -75,7 +75,7 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { private final DefaultListableBeanFactory beanFactory; - private final Map> beanTypes = new HashMap<>(); + private final Map beanTypes = new HashMap<>(); private final Map beanDefinitions = new HashMap<>(); @@ -89,16 +89,20 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { * the case of {@link FactoryBean FactoryBeans}. Will include singletons but will not * cause early bean initialization. * @param type the class or interface to match (must not be {@code null}) + * @param typeExtractor function used to extract the actual type * @return the names of beans (or objects created by FactoryBeans) matching the given * object type (including subclasses), or an empty set if none */ - public Set getNamesForType(Class type) { + public Set getNamesForType(Class type, TypeExtractor typeExtractor) { updateTypesIfNecessary(); - return this.beanTypes.entrySet().stream() - .filter((entry) -> entry.getValue() != null - && type.isAssignableFrom(entry.getValue())) - .map(Map.Entry::getKey) - .collect(Collectors.toCollection(LinkedHashSet::new)); + return this.beanTypes.entrySet().stream().filter((entry) -> { + Class beanType = extractType(entry.getValue(), typeExtractor); + return beanType != null && type.isAssignableFrom(beanType); + }).map(Map.Entry::getKey).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + private Class extractType(ResolvableType type, TypeExtractor extractor) { + return (type != null) ? extractor.getBeanType(type) : null; } /** @@ -114,7 +118,7 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { updateTypesIfNecessary(); return this.beanTypes.entrySet().stream() .filter((entry) -> entry.getValue() != null && AnnotationUtils - .findAnnotation(entry.getValue(), annotation) != null) + .findAnnotation(entry.getValue().resolve(), annotation) != null) .map(Map.Entry::getKey) .collect(Collectors.toCollection(LinkedHashSet::new)); } @@ -127,19 +131,22 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { } private void updateTypesIfNecessary() { - this.beanFactory.getBeanNamesIterator().forEachRemaining((name) -> { - if (!this.beanTypes.containsKey(name)) { - addBeanType(name); - } - else { - updateBeanType(name); - } - }); + this.beanFactory.getBeanNamesIterator() + .forEachRemaining(this::updateTypesIfNecessary); + } + + private void updateTypesIfNecessary(String name) { + if (!this.beanTypes.containsKey(name)) { + addBeanType(name); + } + else { + updateBeanType(name); + } } private void addBeanType(String name) { if (this.beanFactory.containsSingleton(name)) { - this.beanTypes.put(name, this.beanFactory.getType(name)); + this.beanTypes.put(name, getType(name, null)); } else if (!this.beanFactory.isAlias(name)) { addBeanTypeForNonAliasDefinition(name); @@ -147,9 +154,9 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { } private void addBeanTypeForNonAliasDefinition(String name) { - RootBeanDefinition beanDefinition = getBeanDefinition(name); - if (beanDefinition != null) { - addBeanTypeForNonAliasDefinition(name, beanDefinition); + RootBeanDefinition definition = getBeanDefinition(name); + if (definition != null) { + addBeanTypeForNonAliasDefinition(name, definition); } } @@ -157,34 +164,46 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { if (this.beanFactory.isAlias(name) || this.beanFactory.containsSingleton(name)) { return; } - RootBeanDefinition beanDefinition = getBeanDefinition(name); - if (beanDefinition == null) { + RootBeanDefinition definition = getBeanDefinition(name); + if (definition == null) { return; } - RootBeanDefinition previous = this.beanDefinitions.put(name, beanDefinition); - if (previous != null && !beanDefinition.equals(previous)) { - addBeanTypeForNonAliasDefinition(name, beanDefinition); + RootBeanDefinition previous = this.beanDefinitions.put(name, definition); + if (previous != null && !definition.equals(previous)) { + addBeanTypeForNonAliasDefinition(name, definition); + } + } + + private RootBeanDefinition getBeanDefinition(String name) { + try { + return (RootBeanDefinition) this.beanFactory.getMergedBeanDefinition(name); + } + catch (BeanDefinitionStoreException ex) { + logIgnoredError("unresolvable metadata in bean definition", name, ex); + return null; } } private void addBeanTypeForNonAliasDefinition(String name, - RootBeanDefinition beanDefinition) { + RootBeanDefinition definition) { try { - if (!beanDefinition.isAbstract() - && !requiresEagerInit(beanDefinition.getFactoryBeanName())) { - String factoryName = BeanFactory.FACTORY_BEAN_PREFIX + name; - if (this.beanFactory.isFactoryBean(factoryName)) { - Class factoryBeanGeneric = getFactoryBeanGeneric(this.beanFactory, - beanDefinition); + if (!definition.isAbstract() + && !requiresEagerInit(definition.getFactoryBeanName())) { + ResolvableType factoryMethodReturnType = getFactoryMethodReturnType( + definition); + String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + name; + if (this.beanFactory.isFactoryBean(factoryBeanName)) { + ResolvableType factoryBeanGeneric = getFactoryBeanGeneric( + this.beanFactory, definition, factoryMethodReturnType); this.beanTypes.put(name, factoryBeanGeneric); - this.beanTypes.put(factoryName, - this.beanFactory.getType(factoryName)); + this.beanTypes.put(factoryBeanName, + getType(factoryBeanName, factoryMethodReturnType)); } else { - this.beanTypes.put(name, this.beanFactory.getType(name)); + this.beanTypes.put(name, getType(name, factoryMethodReturnType)); } } - this.beanDefinitions.put(name, beanDefinition); + this.beanDefinitions.put(name, definition); } catch (CannotLoadBeanClassException ex) { // Probably contains a placeholder @@ -192,71 +211,26 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { } } - private RootBeanDefinition getBeanDefinition(String name) { - try { - return (RootBeanDefinition) this.beanFactory.getMergedBeanDefinition(name); - } - catch (BeanDefinitionStoreException ex) { - logIgnoredError("unresolvable metadata in bean definition", name, ex); - return null; - } - } - - private void logIgnoredError(String message, String name, Exception ex) { - if (logger.isDebugEnabled()) { - logger.debug("Ignoring " + message + " '" + name + "'", ex); - } - } - private boolean requiresEagerInit(String factoryBeanName) { return (factoryBeanName != null && this.beanFactory.isFactoryBean(factoryBeanName) && !this.beanFactory.containsSingleton(factoryBeanName)); } - /** - * Attempt to guess the type that a {@link FactoryBean} will return based on the - * generics in its method signature. - * @param beanFactory the source bean factory - * @param definition the bean definition - * @return the generic type of the {@link FactoryBean} or {@code null} - */ - private Class getFactoryBeanGeneric(ConfigurableListableBeanFactory beanFactory, - BeanDefinition definition) { + private ResolvableType getFactoryMethodReturnType(BeanDefinition definition) { try { - return doGetFactoryBeanGeneric(beanFactory, definition); + if (StringUtils.hasLength(definition.getFactoryBeanName()) + && StringUtils.hasLength(definition.getFactoryMethodName())) { + Method method = getFactoryMethod(this.beanFactory, definition); + ResolvableType type = (method != null) + ? ResolvableType.forMethodReturnType(method) : null; + return type; + } } catch (Exception ex) { - return null; - } - } - - private Class doGetFactoryBeanGeneric(ConfigurableListableBeanFactory beanFactory, - BeanDefinition definition) - throws Exception, ClassNotFoundException, LinkageError { - if (StringUtils.hasLength(definition.getFactoryBeanName()) - && StringUtils.hasLength(definition.getFactoryMethodName())) { - return getConfigurationClassFactoryBeanGeneric(beanFactory, definition); - } - if (StringUtils.hasLength(definition.getBeanClassName())) { - return getDirectFactoryBeanGeneric(beanFactory, definition); } return null; } - private Class getConfigurationClassFactoryBeanGeneric( - ConfigurableListableBeanFactory beanFactory, BeanDefinition definition) - throws Exception { - Method method = getFactoryMethod(beanFactory, definition); - Class generic = ResolvableType.forMethodReturnType(method) - .as(FactoryBean.class).resolveGeneric(); - if ((generic == null || generic.equals(Object.class)) - && definition.hasAttribute(FACTORY_BEAN_OBJECT_TYPE)) { - generic = getTypeFromAttribute( - definition.getAttribute(FACTORY_BEAN_OBJECT_TYPE)); - } - return generic; - } - private Method getFactoryMethod(ConfigurableListableBeanFactory beanFactory, BeanDefinition definition) throws Exception { if (definition instanceof AnnotatedBeanDefinition) { @@ -305,14 +279,48 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { return Arrays.equals(candidate.getParameterTypes(), current.getParameterTypes()); } - private Class getDirectFactoryBeanGeneric( + private void logIgnoredError(String message, String name, Exception ex) { + if (logger.isDebugEnabled()) { + logger.debug("Ignoring " + message + " '" + name + "'", ex); + } + } + + /** + * Attempt to guess the type that a {@link FactoryBean} will return based on the + * generics in its method signature. + * @param beanFactory the source bean factory + * @param definition the bean definition + * @param factoryMethodReturnType the factory method return type + * @return the generic type of the {@link FactoryBean} or {@code null} + */ + private ResolvableType getFactoryBeanGeneric( + ConfigurableListableBeanFactory beanFactory, BeanDefinition definition, + ResolvableType factoryMethodReturnType) { + try { + if (factoryMethodReturnType != null) { + return getFactoryBeanType(definition, factoryMethodReturnType); + } + if (StringUtils.hasLength(definition.getBeanClassName())) { + return getDirectFactoryBeanGeneric(beanFactory, definition); + } + } + catch (Exception ex) { + } + return null; + } + + private ResolvableType getDirectFactoryBeanGeneric( ConfigurableListableBeanFactory beanFactory, BeanDefinition definition) throws ClassNotFoundException, LinkageError { Class factoryBeanClass = ClassUtils.forName(definition.getBeanClassName(), beanFactory.getBeanClassLoader()); - Class generic = ResolvableType.forClass(factoryBeanClass).as(FactoryBean.class) - .resolveGeneric(); - if ((generic == null || generic.equals(Object.class)) + return getFactoryBeanType(definition, ResolvableType.forClass(factoryBeanClass)); + } + + private ResolvableType getFactoryBeanType(BeanDefinition definition, + ResolvableType type) throws ClassNotFoundException, LinkageError { + ResolvableType generic = type.as(FactoryBean.class).getGeneric(); + if ((generic == null || generic.resolve().equals(Object.class)) && definition.hasAttribute(FACTORY_BEAN_OBJECT_TYPE)) { generic = getTypeFromAttribute( definition.getAttribute(FACTORY_BEAN_OBJECT_TYPE)); @@ -320,17 +328,26 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { return generic; } - private Class getTypeFromAttribute(Object attribute) + private ResolvableType getTypeFromAttribute(Object attribute) throws ClassNotFoundException, LinkageError { if (attribute instanceof Class) { - return (Class) attribute; + return ResolvableType.forClass((Class) attribute); } if (attribute instanceof String) { - return ClassUtils.forName((String) attribute, null); + return ResolvableType.forClass(ClassUtils.forName((String) attribute, null)); } return null; } + private ResolvableType getType(String name, ResolvableType factoryMethodReturnType) { + if (factoryMethodReturnType != null + && !factoryMethodReturnType.resolve(Object.class).equals(Object.class)) { + return factoryMethodReturnType; + } + Class type = this.beanFactory.getType(name); + return (type != null) ? ResolvableType.forClass(type) : null; + } + /** * Factory method to get the {@link BeanTypeRegistry} for a given {@link BeanFactory}. * @param beanFactory the source bean factory @@ -342,14 +359,25 @@ final class BeanTypeRegistry implements SmartInitializingSingleton { Assert.isTrue(listableBeanFactory.isAllowEagerClassLoading(), "Bean factory must allow eager class loading"); if (!listableBeanFactory.containsLocalBean(BEAN_NAME)) { - BeanDefinition bd = BeanDefinitionBuilder + BeanDefinition definition = BeanDefinitionBuilder .genericBeanDefinition(BeanTypeRegistry.class, () -> new BeanTypeRegistry( (DefaultListableBeanFactory) beanFactory)) .getBeanDefinition(); - listableBeanFactory.registerBeanDefinition(BEAN_NAME, bd); + listableBeanFactory.registerBeanDefinition(BEAN_NAME, definition); } return listableBeanFactory.getBean(BEAN_NAME, BeanTypeRegistry.class); } + /** + * Function used to extract the actual bean type from a source {@link ResolvableType}. + * May be used to support parameterized containers for beans. + */ + @FunctionalInterface + interface TypeExtractor { + + Class getBeanType(ResolvableType type); + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java index 4606b77e16..b234a35f6d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBean.java @@ -97,4 +97,14 @@ public @interface ConditionalOnBean { */ SearchStrategy search() default SearchStrategy.ALL; + /** + * Additional classes that may contain the specified bean types within their generic + * parameters. For example, an annotation declaring {@code value=Name.class} and + * {@code parameterizedContainer=NameRegistration.class} would detect both + * {@code Name} and {@code NameRegistration}. + * @return the container types + * @since 2.1.0 + */ + Class[] parameterizedContainer() default {}; + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java index be75081d1e..e075e6cab6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.java @@ -113,4 +113,14 @@ public @interface ConditionalOnMissingBean { */ SearchStrategy search() default SearchStrategy.ALL; + /** + * Additional classes that may contain the specified bean types within their generic + * parameters. For example, an annotation declaring {@code value=Name.class} and + * {@code parameterizedContainer=NameRegistration.class} would detect both + * {@code Name} and {@code NameRegistration}. + * @return the container types + * @since 2.1.0 + */ + Class[] parameterizedContainer() default {}; + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java index b819b2f9bf..97bbb0d2ca 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/OnBeanCondition.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.condition; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -35,18 +36,22 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigurationMetadata; +import org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.TypeExtractor; import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.ConfigurationCondition; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.Order; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -57,6 +62,9 @@ import org.springframework.util.StringUtils; * @author Jakub Kubrynski * @author Stephane Nicoll * @author Andy Wilkinson + * @see ConditionalOnBean + * @see ConditionalOnMissingBean + * @see ConditionalOnSingleCandidate */ @Order(Ordered.LOWEST_PRECEDENCE) class OnBeanCondition extends FilteringSpringBootCondition @@ -68,6 +76,11 @@ class OnBeanCondition extends FilteringSpringBootCondition */ public static final String FACTORY_BEAN_OBJECT_TYPE = BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE; + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + @Override protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { @@ -102,11 +115,6 @@ class OnBeanCondition extends FilteringSpringBootCondition return null; } - @Override - public ConfigurationPhase getConfigurationPhase() { - return ConfigurationPhase.REGISTER_BEAN; - } - @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { @@ -144,7 +152,7 @@ class OnBeanCondition extends FilteringSpringBootCondition matchMessage = matchMessage .andCondition(ConditionalOnSingleCandidate.class, spec) .found("a primary bean from beans") - .items(Style.QUOTE, matchResult.namesOfAllMatches); + .items(Style.QUOTE, matchResult.getNamesOfAllMatches()); } if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) { BeanSearchSpec spec = new BeanSearchSpec(context, metadata, @@ -162,61 +170,8 @@ class OnBeanCondition extends FilteringSpringBootCondition return ConditionOutcome.match(matchMessage); } - private String createOnBeanNoMatchReason(MatchResult matchResult) { - StringBuilder reason = new StringBuilder(); - appendMessageForNoMatches(reason, matchResult.unmatchedAnnotations, - "annotated with"); - appendMessageForNoMatches(reason, matchResult.unmatchedTypes, "of type"); - appendMessageForNoMatches(reason, matchResult.unmatchedNames, "named"); - return reason.toString(); - } - - private void appendMessageForNoMatches(StringBuilder reason, - Collection unmatched, String description) { - if (!unmatched.isEmpty()) { - if (reason.length() > 0) { - reason.append(" and "); - } - reason.append("did not find any beans "); - reason.append(description); - reason.append(" "); - reason.append(StringUtils.collectionToDelimitedString(unmatched, ", ")); - } - } - - private String createOnMissingBeanNoMatchReason(MatchResult matchResult) { - StringBuilder reason = new StringBuilder(); - appendMessageForMatches(reason, matchResult.matchedAnnotations, "annotated with"); - appendMessageForMatches(reason, matchResult.matchedTypes, "of type"); - if (!matchResult.matchedNames.isEmpty()) { - if (reason.length() > 0) { - reason.append(" and "); - } - reason.append("found beans named "); - reason.append(StringUtils - .collectionToDelimitedString(matchResult.matchedNames, ", ")); - } - return reason.toString(); - } - - private void appendMessageForMatches(StringBuilder reason, - Map> matches, String description) { - if (!matches.isEmpty()) { - matches.forEach((key, value) -> { - if (reason.length() > 0) { - reason.append(" and "); - } - reason.append("found beans "); - reason.append(description); - reason.append(" '"); - reason.append(key); - reason.append("' "); - reason.append(StringUtils.collectionToDelimitedString(value, ", ")); - }); - } - } - - private MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) { + protected final MatchResult getMatchingBeans(ConditionContext context, + BeanSearchSpec beans) { ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); if (beans.getStrategy() == SearchStrategy.ANCESTORS) { BeanFactory parent = beanFactory.getParentBeanFactory(); @@ -226,11 +181,13 @@ class OnBeanCondition extends FilteringSpringBootCondition } MatchResult matchResult = new MatchResult(); boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT; + TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader()); List beansIgnoredByType = getNamesOfBeansIgnoredByType( - beans.getIgnoredTypes(), beanFactory, context, considerHierarchy); + beans.getIgnoredTypes(), typeExtractor, beanFactory, context, + considerHierarchy); for (String type : beans.getTypes()) { Collection typeMatches = getBeanNamesForType(beanFactory, type, - context.getClassLoader(), considerHierarchy); + typeExtractor, context.getClassLoader(), considerHierarchy); typeMatches.removeAll(beansIgnoredByType); if (typeMatches.isEmpty()) { matchResult.recordUnmatchedType(type); @@ -263,12 +220,44 @@ class OnBeanCondition extends FilteringSpringBootCondition return matchResult; } - private List getNamesOfBeansIgnoredByType(List ignoredTypes, - ListableBeanFactory beanFactory, ConditionContext context, + private String[] getBeanNamesForAnnotation( + ConfigurableListableBeanFactory beanFactory, String type, + ClassLoader classLoader, boolean considerHierarchy) throws LinkageError { + Set names = new HashSet<>(); + try { + @SuppressWarnings("unchecked") + Class annotationType = (Class) ClassUtils + .forName(type, classLoader); + collectBeanNamesForAnnotation(names, beanFactory, annotationType, + considerHierarchy); + } + catch (ClassNotFoundException ex) { + // Continue + } + return StringUtils.toStringArray(names); + } + + private void collectBeanNamesForAnnotation(Set names, + ListableBeanFactory beanFactory, Class annotationType, boolean considerHierarchy) { + BeanTypeRegistry registry = BeanTypeRegistry.get(beanFactory); + names.addAll(registry.getNamesForAnnotation(annotationType)); + if (considerHierarchy) { + BeanFactory parent = ((HierarchicalBeanFactory) beanFactory) + .getParentBeanFactory(); + if (parent instanceof ListableBeanFactory) { + collectBeanNamesForAnnotation(names, (ListableBeanFactory) parent, + annotationType, considerHierarchy); + } + } + } + + private List getNamesOfBeansIgnoredByType(List ignoredTypes, + TypeExtractor typeExtractor, ListableBeanFactory beanFactory, + ConditionContext context, boolean considerHierarchy) { List beanNames = new ArrayList<>(); for (String ignoredType : ignoredTypes) { - beanNames.addAll(getBeanNamesForType(beanFactory, ignoredType, + beanNames.addAll(getBeanNamesForType(beanFactory, ignoredType, typeExtractor, context.getClassLoader(), considerHierarchy)); } return beanNames; @@ -283,61 +272,92 @@ class OnBeanCondition extends FilteringSpringBootCondition } private Collection getBeanNamesForType(ListableBeanFactory beanFactory, - String type, ClassLoader classLoader, boolean considerHierarchy) - throws LinkageError { + String type, TypeExtractor typeExtractor, ClassLoader classLoader, + boolean considerHierarchy) throws LinkageError { try { - Set result = new LinkedHashSet<>(); - collectBeanNamesForType(result, beanFactory, - ClassUtils.forName(type, classLoader), considerHierarchy); - return result; + return getBeanNamesForType(beanFactory, considerHierarchy, + ClassUtils.forName(type, classLoader), typeExtractor); } catch (ClassNotFoundException | NoClassDefFoundError ex) { return Collections.emptySet(); } } + private Collection getBeanNamesForType(ListableBeanFactory beanFactory, + boolean considerHierarchy, Class type, TypeExtractor typeExtractor) { + Set result = new LinkedHashSet<>(); + collectBeanNamesForType(result, beanFactory, type, typeExtractor, + considerHierarchy); + return result; + } + private void collectBeanNamesForType(Set result, - ListableBeanFactory beanFactory, Class type, boolean considerHierarchy) { - result.addAll(BeanTypeRegistry.get(beanFactory).getNamesForType(type)); + ListableBeanFactory beanFactory, Class type, TypeExtractor typeExtractor, + boolean considerHierarchy) { + BeanTypeRegistry registry = BeanTypeRegistry.get(beanFactory); + result.addAll(registry.getNamesForType(type, typeExtractor)); if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) { BeanFactory parent = ((HierarchicalBeanFactory) beanFactory) .getParentBeanFactory(); if (parent instanceof ListableBeanFactory) { collectBeanNamesForType(result, (ListableBeanFactory) parent, type, - considerHierarchy); + typeExtractor, considerHierarchy); } } } - private String[] getBeanNamesForAnnotation( - ConfigurableListableBeanFactory beanFactory, String type, - ClassLoader classLoader, boolean considerHierarchy) throws LinkageError { - Set names = new HashSet<>(); - try { - @SuppressWarnings("unchecked") - Class annotationType = (Class) ClassUtils - .forName(type, classLoader); - collectBeanNamesForAnnotation(names, beanFactory, annotationType, - considerHierarchy); - } - catch (ClassNotFoundException ex) { - // Continue + private String createOnBeanNoMatchReason(MatchResult matchResult) { + StringBuilder reason = new StringBuilder(); + appendMessageForNoMatches(reason, matchResult.getUnmatchedAnnotations(), + "annotated with"); + appendMessageForNoMatches(reason, matchResult.getUnmatchedTypes(), "of type"); + appendMessageForNoMatches(reason, matchResult.getUnmatchedNames(), "named"); + return reason.toString(); + } + + private void appendMessageForNoMatches(StringBuilder reason, + Collection unmatched, String description) { + if (!unmatched.isEmpty()) { + if (reason.length() > 0) { + reason.append(" and "); + } + reason.append("did not find any beans "); + reason.append(description); + reason.append(" "); + reason.append(StringUtils.collectionToDelimitedString(unmatched, ", ")); } - return StringUtils.toStringArray(names); } - private void collectBeanNamesForAnnotation(Set names, - ListableBeanFactory beanFactory, Class annotationType, - boolean considerHierarchy) { - names.addAll( - BeanTypeRegistry.get(beanFactory).getNamesForAnnotation(annotationType)); - if (considerHierarchy) { - BeanFactory parent = ((HierarchicalBeanFactory) beanFactory) - .getParentBeanFactory(); - if (parent instanceof ListableBeanFactory) { - collectBeanNamesForAnnotation(names, (ListableBeanFactory) parent, - annotationType, considerHierarchy); + private String createOnMissingBeanNoMatchReason(MatchResult matchResult) { + StringBuilder reason = new StringBuilder(); + appendMessageForMatches(reason, matchResult.getMatchedAnnotations(), + "annotated with"); + appendMessageForMatches(reason, matchResult.getMatchedTypes(), "of type"); + if (!matchResult.getMatchedNames().isEmpty()) { + if (reason.length() > 0) { + reason.append(" and "); } + reason.append("found beans named "); + reason.append(StringUtils + .collectionToDelimitedString(matchResult.getMatchedNames(), ", ")); + } + return reason.toString(); + } + + private void appendMessageForMatches(StringBuilder reason, + Map> matches, String description) { + if (!matches.isEmpty()) { + matches.forEach((key, value) -> { + if (reason.length() > 0) { + reason.append(" and "); + } + reason.append("found beans "); + reason.append(description); + reason.append(" '"); + reason.append(key); + reason.append("' "); + reason.append(StringUtils.collectionToDelimitedString(value, ", ")); + }); } } @@ -375,7 +395,7 @@ class OnBeanCondition extends FilteringSpringBootCondition return null; } - private static class BeanSearchSpec { + protected static class BeanSearchSpec { private final Class annotationType; @@ -387,10 +407,17 @@ class OnBeanCondition extends FilteringSpringBootCondition private final List ignoredTypes = new ArrayList<>(); + private final List parameterizedContainers = new ArrayList<>(); + private final SearchStrategy strategy; - BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, + public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class annotationType) { + this(context, metadata, annotationType, null); + } + + public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, + Class annotationType, Class genericContainer) { this.annotationType = annotationType; MultiValueMap attributes = metadata .getAllAnnotationAttributes(annotationType.getName(), true); @@ -400,6 +427,7 @@ class OnBeanCondition extends FilteringSpringBootCondition collect(attributes, "annotation", this.annotations); collect(attributes, "ignored", this.ignoredTypes); collect(attributes, "ignoredType", this.ignoredTypes); + collect(attributes, "parameterizedContainer", this.parameterizedContainers); this.strategy = (SearchStrategy) attributes.getFirst("search"); BeanTypeDeductionException deductionException = null; try { @@ -415,7 +443,7 @@ class OnBeanCondition extends FilteringSpringBootCondition protected void validate(BeanTypeDeductionException ex) { if (!hasAtLeastOne(this.types, this.names, this.annotations)) { - String message = annotationName() + String message = getAnnotationName() + " did not specify a bean using type, name or annotation"; if (ex == null) { throw new IllegalStateException(message); @@ -429,7 +457,7 @@ class OnBeanCondition extends FilteringSpringBootCondition return Arrays.stream(lists).anyMatch((list) -> !list.isEmpty()); } - protected String annotationName() { + protected final String getAnnotationName() { return "@" + ClassUtils.getShortName(this.annotationType); } @@ -460,10 +488,7 @@ class OnBeanCondition extends FilteringSpringBootCondition private void addDeducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata, final List beanTypes) { try { - // We should be safe to load at this point since we are in the - // REGISTER_BEAN phase - Class returnType = ClassUtils.forName(metadata.getReturnTypeName(), - context.getClassLoader()); + Class returnType = getReturnType(context, metadata); beanTypes.add(returnType.getName()); } catch (Throwable ex) { @@ -472,6 +497,71 @@ class OnBeanCondition extends FilteringSpringBootCondition } } + private Class getReturnType(ConditionContext context, MethodMetadata metadata) + throws ClassNotFoundException, LinkageError { + // We should be safe to load at this point since we are in the + // REGISTER_BEAN phase + ClassLoader classLoader = context.getClassLoader(); + Class returnType = ClassUtils.forName(metadata.getReturnTypeName(), + classLoader); + if (isParameterizedContainer(returnType, classLoader)) { + returnType = getReturnTypeGeneric(metadata, classLoader); + } + return returnType; + } + + private Class getReturnTypeGeneric(MethodMetadata metadata, + ClassLoader classLoader) throws ClassNotFoundException, LinkageError { + Class declaringClass = ClassUtils.forName(metadata.getDeclaringClassName(), + classLoader); + Method beanMethod = findBeanMethod(declaringClass, metadata.getMethodName()); + return ResolvableType.forMethodReturnType(beanMethod).resolveGeneric(); + } + + private Method findBeanMethod(Class declaringClass, String methodName) { + Method method = ReflectionUtils.findMethod(declaringClass, methodName); + if (isBeanMethod(method)) { + return method; + } + return Arrays.stream(ReflectionUtils.getAllDeclaredMethods(declaringClass)) + .filter((candidate) -> candidate.getName().equals(methodName)) + .filter(this::isBeanMethod).findFirst() + .orElseThrow(() -> new IllegalStateException( + "Unable to find bean method " + methodName)); + } + + private boolean isBeanMethod(Method method) { + return method != null + && AnnotatedElementUtils.hasAnnotation(method, Bean.class); + } + + public TypeExtractor getTypeExtractor(ClassLoader classLoader) { + if (this.parameterizedContainers.isEmpty()) { + return ResolvableType::resolve; + } + return (type) -> { + Class resolved = type.resolve(); + if (isParameterizedContainer(resolved, classLoader)) { + return type.getGeneric().resolve(); + } + return resolved; + }; + } + + private boolean isParameterizedContainer(Class type, ClassLoader classLoader) { + for (String candidate : this.parameterizedContainers) { + try { + if (ClassUtils.forName(candidate, classLoader) + .isAssignableFrom(type)) { + return true; + } + } + catch (Exception ex) { + } + } + return false; + } + public SearchStrategy getStrategy() { return (this.strategy != null) ? this.strategy : SearchStrategy.ALL; } @@ -531,23 +621,13 @@ class OnBeanCondition extends FilteringSpringBootCondition @Override protected void validate(BeanTypeDeductionException ex) { - Assert.isTrue(getTypes().size() == 1, () -> annotationName() + Assert.isTrue(getTypes().size() == 1, () -> getAnnotationName() + " 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); - } - - } - - static final class MatchResult { + protected static final class MatchResult { private final Map> matchedAnnotations = new HashMap<>(); @@ -591,20 +671,54 @@ class OnBeanCondition extends FilteringSpringBootCondition this.unmatchedTypes.add(type); } - private boolean isAllMatched() { + public boolean isAllMatched() { return this.unmatchedAnnotations.isEmpty() && this.unmatchedNames.isEmpty() && this.unmatchedTypes.isEmpty(); } - private boolean isAnyMatched() { + public boolean isAnyMatched() { return (!this.matchedAnnotations.isEmpty()) || (!this.matchedNames.isEmpty()) || (!this.matchedTypes.isEmpty()); } - private Set getNamesOfAllMatches() { + public Map> getMatchedAnnotations() { + return this.matchedAnnotations; + } + + public List getMatchedNames() { + return this.matchedNames; + } + + public Map> getMatchedTypes() { + return this.matchedTypes; + } + + public List getUnmatchedAnnotations() { + return this.unmatchedAnnotations; + } + + public List getUnmatchedNames() { + return this.unmatchedNames; + } + + public List getUnmatchedTypes() { + return this.unmatchedTypes; + } + + public Set getNamesOfAllMatches() { return this.namesOfAllMatches; } } + 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-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java index 6ae41a8f34..2e9f0982cf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanTests.java @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Date; +import java.util.function.Consumer; import org.junit.Test; @@ -30,6 +31,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -38,6 +40,7 @@ import org.springframework.context.annotation.ImportResource; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.type.AnnotationMetadata; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -149,6 +152,89 @@ public class ConditionalOnBeanTests { }); } + @Test + public void parameterizedContainerWhenValueIsOfMissingBeanDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithoutCustomConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("otherExampleBean"))); + } + + @Test + public void parameterizedContainerWhenValueIsOfExistingBeanMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "customExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenValueIsOfMissingBeanRegistrationDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithoutCustomContainerConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("otherExampleBean"))); + } + + @Test + public void parameterizedContainerWhenValueIsOfExistingBeanRegistrationMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomContainerConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "customExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnTypeIsOfExistingBeanMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomConfig.class, + ParmeterizedConditionWithReturnTypeConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "customExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnTypeIsOfExistingBeanRegistrationMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomContainerConfig.class, + ParmeterizedConditionWithReturnTypeConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "customExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomConfig.class, + ParmeterizedConditionWithReturnRegistrationTypeConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "customExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanRegistrationMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomContainerConfig.class, + ParmeterizedConditionWithReturnRegistrationTypeConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "customExampleBean", "conditionalCustomExampleBean"))); + } + + private Consumer exampleBeanRequirement( + String... names) { + return (context) -> { + String[] beans = context.getBeanNamesForType(ExampleBean.class); + String[] containers = context + .getBeanNamesForType(TestParameterizedContainer.class); + assertThat(StringUtils.concatenateStringArrays(beans, containers)) + .containsOnly(names); + }; + } + @Configuration @ConditionalOnBean(name = "foo") protected static class OnBeanNameConfiguration { @@ -298,57 +384,146 @@ public class ConditionalOnBeanTests { } - @TestAnnotation - public static class ExampleBean { + @Configuration + public static class OriginalDefinition { - private String value; + @Bean + public String testBean() { + return "test"; + } - public ExampleBean(String value) { - this.value = value; + } + + @Configuration + @ConditionalOnBean(String.class) + public static class OverridingDefinition { + + @Bean + public Integer testBean() { + return 1; } - @Override - public String toString() { - return this.value; + } + + @Configuration + @ConditionalOnBean(String.class) + public static class ConsumingConfiguration { + + ConsumingConfiguration(String testBean) { } } - @Target(ElementType.TYPE) - @Retention(RetentionPolicy.RUNTIME) - @Documented - public @interface TestAnnotation { + @Configuration + static class ParmeterizedWithCustomConfig { + + @Bean + public CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } } @Configuration - public static class OriginalDefinition { + static class ParmeterizedWithoutCustomConfig { @Bean - public String testBean() { - return "test"; + public OtherExampleBean otherExampleBean() { + return new OtherExampleBean(); } } @Configuration - @ConditionalOnBean(String.class) - public static class OverridingDefinition { + static class ParmeterizedWithoutCustomContainerConfig { @Bean - public Integer testBean() { - return 1; + public TestParameterizedContainer otherExampleBean() { + return new TestParameterizedContainer(); } } @Configuration - @ConditionalOnBean(String.class) - public static class ConsumingConfiguration { + static class ParmeterizedWithCustomContainerConfig { - ConsumingConfiguration(String testBean) { + @Bean + public TestParameterizedContainer customExampleBean() { + return new TestParameterizedContainer(); + } + + } + + @Configuration + static class ParmeterizedConditionWithValueConfig { + + @Bean + @ConditionalOnBean(value = CustomExampleBean.class, parameterizedContainer = TestParameterizedContainer.class) + public CustomExampleBean conditionalCustomExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration + static class ParmeterizedConditionWithReturnTypeConfig { + + @Bean + @ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class) + public CustomExampleBean conditionalCustomExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration + static class ParmeterizedConditionWithReturnRegistrationTypeConfig { + + @Bean + @ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class) + public TestParameterizedContainer conditionalCustomExampleBean() { + return new TestParameterizedContainer(); + } + + } + + @TestAnnotation + public static class ExampleBean { + + private String value; + + public ExampleBean(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; } } + public static class CustomExampleBean extends ExampleBean { + + public CustomExampleBean() { + super("custom subclass"); + } + + } + + public static class OtherExampleBean extends ExampleBean { + + public OtherExampleBean() { + super("other subclass"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + public @interface TestAnnotation { + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java index 5d248d70df..16f33bc646 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanTests.java @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Date; +import java.util.function.Consumer; import org.junit.Test; @@ -33,6 +34,7 @@ import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanC import org.springframework.boot.autoconfigure.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan.Filter; @@ -44,6 +46,7 @@ import org.springframework.context.annotation.ImportResource; import org.springframework.core.type.AnnotationMetadata; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -304,6 +307,89 @@ public class ConditionalOnMissingBeanTests { }); } + @Test + public void parameterizedContainerWhenValueIsOfMissingBeanMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithoutCustomConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "otherExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenValueIsOfExistingBeanDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + public void parameterizedContainerWhenValueIsOfMissingBeanRegistrationMatches() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithoutCustomContainerConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement( + "otherExampleBean", "conditionalCustomExampleBean"))); + } + + @Test + public void parameterizedContainerWhenValueIsOfExistingBeanRegistrationDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomContainerConfig.class, + ParmeterizedConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnTypeIsOfExistingBeanDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomConfig.class, + ParmeterizedConditionWithReturnTypeConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnTypeIsOfExistingBeanRegistrationDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomContainerConfig.class, + ParmeterizedConditionWithReturnTypeConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomConfig.class, + ParmeterizedConditionWithReturnRegistrationTypeConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + public void parameterizedContainerWhenReturnRegistrationTypeIsOfExistingBeanRegistrationDoesNotMatch() { + this.contextRunner + .withUserConfiguration(ParmeterizedWithCustomContainerConfig.class, + ParmeterizedConditionWithReturnRegistrationTypeConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean"))); + } + + private Consumer exampleBeanRequirement( + String... names) { + return (context) -> { + String[] beans = context.getBeanNamesForType(ExampleBean.class); + String[] containers = context + .getBeanNamesForType(TestParameterizedContainer.class); + assertThat(StringUtils.concatenateStringArrays(beans, containers)) + .containsOnly(names); + }; + } + @Configuration protected static class OnBeanInAncestorsConfiguration { @@ -584,30 +670,6 @@ public class ConditionalOnMissingBeanTests { } - @TestAnnotation - public static class ExampleBean { - - private String value; - - public ExampleBean(String value) { - this.value = value; - } - - @Override - public String toString() { - return this.value; - } - - } - - public static class CustomExampleBean extends ExampleBean { - - public CustomExampleBean() { - super("custom subclass"); - } - - } - public static class ExampleFactoryBean implements FactoryBean { public ExampleFactoryBean(String value) { @@ -654,6 +716,111 @@ public class ConditionalOnMissingBeanTests { } + @Configuration + static class ParmeterizedWithCustomConfig { + + @Bean + public CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration + static class ParmeterizedWithoutCustomConfig { + + @Bean + public OtherExampleBean otherExampleBean() { + return new OtherExampleBean(); + } + + } + + @Configuration + static class ParmeterizedWithoutCustomContainerConfig { + + @Bean + public TestParameterizedContainer otherExampleBean() { + return new TestParameterizedContainer(); + } + + } + + @Configuration + static class ParmeterizedWithCustomContainerConfig { + + @Bean + public TestParameterizedContainer customExampleBean() { + return new TestParameterizedContainer(); + } + + } + + @Configuration + static class ParmeterizedConditionWithValueConfig { + + @Bean + @ConditionalOnMissingBean(value = CustomExampleBean.class, parameterizedContainer = TestParameterizedContainer.class) + public CustomExampleBean conditionalCustomExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration + static class ParmeterizedConditionWithReturnTypeConfig { + + @Bean + @ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class) + public CustomExampleBean conditionalCustomExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration + static class ParmeterizedConditionWithReturnRegistrationTypeConfig { + + @Bean + @ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class) + public TestParameterizedContainer conditionalCustomExampleBean() { + return new TestParameterizedContainer(); + } + + } + + @TestAnnotation + public static class ExampleBean { + + private String value; + + public ExampleBean(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + } + + public static class CustomExampleBean extends ExampleBean { + + public CustomExampleBean() { + super("custom subclass"); + } + + } + + public static class OtherExampleBean extends ExampleBean { + + public OtherExampleBean() { + super("other subclass"); + } + + } + @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/TestParameterizedContainer.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/TestParameterizedContainer.java new file mode 100644 index 0000000000..13324db21f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/TestParameterizedContainer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2018 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; + +/** + * Simple parameterized container for testing {@link ConditionalOnBean} and + * {@link ConditionalOnMissingBean}. + * + * @param The bean type + * @author Phillip Webb + */ +public class TestParameterizedContainer { + +}