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
pull/14950/head
Phillip Webb 6 years ago
parent 4d3d711e0e
commit 9f858e759c

@ -75,7 +75,7 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
private final DefaultListableBeanFactory beanFactory; private final DefaultListableBeanFactory beanFactory;
private final Map<String, Class<?>> beanTypes = new HashMap<>(); private final Map<String, ResolvableType> beanTypes = new HashMap<>();
private final Map<String, RootBeanDefinition> beanDefinitions = new HashMap<>(); private final Map<String, RootBeanDefinition> beanDefinitions = new HashMap<>();
@ -89,16 +89,20 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
* the case of {@link FactoryBean FactoryBeans}. Will include singletons but will not * the case of {@link FactoryBean FactoryBeans}. Will include singletons but will not
* cause early bean initialization. * cause early bean initialization.
* @param type the class or interface to match (must not be {@code null}) * @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 * @return the names of beans (or objects created by FactoryBeans) matching the given
* object type (including subclasses), or an empty set if none * object type (including subclasses), or an empty set if none
*/ */
public Set<String> getNamesForType(Class<?> type) { public Set<String> getNamesForType(Class<?> type, TypeExtractor typeExtractor) {
updateTypesIfNecessary(); updateTypesIfNecessary();
return this.beanTypes.entrySet().stream() return this.beanTypes.entrySet().stream().filter((entry) -> {
.filter((entry) -> entry.getValue() != null Class<?> beanType = extractType(entry.getValue(), typeExtractor);
&& type.isAssignableFrom(entry.getValue())) return beanType != null && type.isAssignableFrom(beanType);
.map(Map.Entry::getKey) }).map(Map.Entry::getKey).collect(Collectors.toCollection(LinkedHashSet::new));
.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(); updateTypesIfNecessary();
return this.beanTypes.entrySet().stream() return this.beanTypes.entrySet().stream()
.filter((entry) -> entry.getValue() != null && AnnotationUtils .filter((entry) -> entry.getValue() != null && AnnotationUtils
.findAnnotation(entry.getValue(), annotation) != null) .findAnnotation(entry.getValue().resolve(), annotation) != null)
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.collect(Collectors.toCollection(LinkedHashSet::new)); .collect(Collectors.toCollection(LinkedHashSet::new));
} }
@ -127,19 +131,22 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
} }
private void updateTypesIfNecessary() { private void updateTypesIfNecessary() {
this.beanFactory.getBeanNamesIterator().forEachRemaining((name) -> { this.beanFactory.getBeanNamesIterator()
.forEachRemaining(this::updateTypesIfNecessary);
}
private void updateTypesIfNecessary(String name) {
if (!this.beanTypes.containsKey(name)) { if (!this.beanTypes.containsKey(name)) {
addBeanType(name); addBeanType(name);
} }
else { else {
updateBeanType(name); updateBeanType(name);
} }
});
} }
private void addBeanType(String name) { private void addBeanType(String name) {
if (this.beanFactory.containsSingleton(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)) { else if (!this.beanFactory.isAlias(name)) {
addBeanTypeForNonAliasDefinition(name); addBeanTypeForNonAliasDefinition(name);
@ -147,9 +154,9 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
} }
private void addBeanTypeForNonAliasDefinition(String name) { private void addBeanTypeForNonAliasDefinition(String name) {
RootBeanDefinition beanDefinition = getBeanDefinition(name); RootBeanDefinition definition = getBeanDefinition(name);
if (beanDefinition != null) { if (definition != null) {
addBeanTypeForNonAliasDefinition(name, beanDefinition); addBeanTypeForNonAliasDefinition(name, definition);
} }
} }
@ -157,34 +164,46 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
if (this.beanFactory.isAlias(name) || this.beanFactory.containsSingleton(name)) { if (this.beanFactory.isAlias(name) || this.beanFactory.containsSingleton(name)) {
return; return;
} }
RootBeanDefinition beanDefinition = getBeanDefinition(name); RootBeanDefinition definition = getBeanDefinition(name);
if (beanDefinition == null) { if (definition == null) {
return; return;
} }
RootBeanDefinition previous = this.beanDefinitions.put(name, beanDefinition); RootBeanDefinition previous = this.beanDefinitions.put(name, definition);
if (previous != null && !beanDefinition.equals(previous)) { if (previous != null && !definition.equals(previous)) {
addBeanTypeForNonAliasDefinition(name, beanDefinition); 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, private void addBeanTypeForNonAliasDefinition(String name,
RootBeanDefinition beanDefinition) { RootBeanDefinition definition) {
try { try {
if (!beanDefinition.isAbstract() if (!definition.isAbstract()
&& !requiresEagerInit(beanDefinition.getFactoryBeanName())) { && !requiresEagerInit(definition.getFactoryBeanName())) {
String factoryName = BeanFactory.FACTORY_BEAN_PREFIX + name; ResolvableType factoryMethodReturnType = getFactoryMethodReturnType(
if (this.beanFactory.isFactoryBean(factoryName)) { definition);
Class<?> factoryBeanGeneric = getFactoryBeanGeneric(this.beanFactory, String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + name;
beanDefinition); if (this.beanFactory.isFactoryBean(factoryBeanName)) {
ResolvableType factoryBeanGeneric = getFactoryBeanGeneric(
this.beanFactory, definition, factoryMethodReturnType);
this.beanTypes.put(name, factoryBeanGeneric); this.beanTypes.put(name, factoryBeanGeneric);
this.beanTypes.put(factoryName, this.beanTypes.put(factoryBeanName,
this.beanFactory.getType(factoryName)); getType(factoryBeanName, factoryMethodReturnType));
} }
else { 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) { catch (CannotLoadBeanClassException ex) {
// Probably contains a placeholder // Probably contains a placeholder
@ -192,69 +211,24 @@ 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) { private boolean requiresEagerInit(String factoryBeanName) {
return (factoryBeanName != null && this.beanFactory.isFactoryBean(factoryBeanName) return (factoryBeanName != null && this.beanFactory.isFactoryBean(factoryBeanName)
&& !this.beanFactory.containsSingleton(factoryBeanName)); && !this.beanFactory.containsSingleton(factoryBeanName));
} }
/** private ResolvableType getFactoryMethodReturnType(BeanDefinition definition) {
* 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) {
try { try {
return doGetFactoryBeanGeneric(beanFactory, definition);
}
catch (Exception ex) {
return null;
}
}
private Class<?> doGetFactoryBeanGeneric(ConfigurableListableBeanFactory beanFactory,
BeanDefinition definition)
throws Exception, ClassNotFoundException, LinkageError {
if (StringUtils.hasLength(definition.getFactoryBeanName()) if (StringUtils.hasLength(definition.getFactoryBeanName())
&& StringUtils.hasLength(definition.getFactoryMethodName())) { && StringUtils.hasLength(definition.getFactoryMethodName())) {
return getConfigurationClassFactoryBeanGeneric(beanFactory, definition); Method method = getFactoryMethod(this.beanFactory, definition);
ResolvableType type = (method != null)
? ResolvableType.forMethodReturnType(method) : null;
return type;
} }
if (StringUtils.hasLength(definition.getBeanClassName())) {
return getDirectFactoryBeanGeneric(beanFactory, definition);
}
return null;
} }
catch (Exception ex) {
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; return null;
} }
private Method getFactoryMethod(ConfigurableListableBeanFactory beanFactory, private Method getFactoryMethod(ConfigurableListableBeanFactory beanFactory,
@ -305,14 +279,48 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
return Arrays.equals(candidate.getParameterTypes(), current.getParameterTypes()); 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) ConfigurableListableBeanFactory beanFactory, BeanDefinition definition)
throws ClassNotFoundException, LinkageError { throws ClassNotFoundException, LinkageError {
Class<?> factoryBeanClass = ClassUtils.forName(definition.getBeanClassName(), Class<?> factoryBeanClass = ClassUtils.forName(definition.getBeanClassName(),
beanFactory.getBeanClassLoader()); beanFactory.getBeanClassLoader());
Class<?> generic = ResolvableType.forClass(factoryBeanClass).as(FactoryBean.class) return getFactoryBeanType(definition, ResolvableType.forClass(factoryBeanClass));
.resolveGeneric(); }
if ((generic == null || generic.equals(Object.class))
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)) { && definition.hasAttribute(FACTORY_BEAN_OBJECT_TYPE)) {
generic = getTypeFromAttribute( generic = getTypeFromAttribute(
definition.getAttribute(FACTORY_BEAN_OBJECT_TYPE)); definition.getAttribute(FACTORY_BEAN_OBJECT_TYPE));
@ -320,17 +328,26 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
return generic; return generic;
} }
private Class<?> getTypeFromAttribute(Object attribute) private ResolvableType getTypeFromAttribute(Object attribute)
throws ClassNotFoundException, LinkageError { throws ClassNotFoundException, LinkageError {
if (attribute instanceof Class<?>) { if (attribute instanceof Class<?>) {
return (Class<?>) attribute; return ResolvableType.forClass((Class<?>) attribute);
} }
if (attribute instanceof String) { if (attribute instanceof String) {
return ClassUtils.forName((String) attribute, null); return ResolvableType.forClass(ClassUtils.forName((String) attribute, null));
} }
return 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}. * Factory method to get the {@link BeanTypeRegistry} for a given {@link BeanFactory}.
* @param beanFactory the source bean factory * @param beanFactory the source bean factory
@ -342,14 +359,25 @@ final class BeanTypeRegistry implements SmartInitializingSingleton {
Assert.isTrue(listableBeanFactory.isAllowEagerClassLoading(), Assert.isTrue(listableBeanFactory.isAllowEagerClassLoading(),
"Bean factory must allow eager class loading"); "Bean factory must allow eager class loading");
if (!listableBeanFactory.containsLocalBean(BEAN_NAME)) { if (!listableBeanFactory.containsLocalBean(BEAN_NAME)) {
BeanDefinition bd = BeanDefinitionBuilder BeanDefinition definition = BeanDefinitionBuilder
.genericBeanDefinition(BeanTypeRegistry.class, .genericBeanDefinition(BeanTypeRegistry.class,
() -> new BeanTypeRegistry( () -> new BeanTypeRegistry(
(DefaultListableBeanFactory) beanFactory)) (DefaultListableBeanFactory) beanFactory))
.getBeanDefinition(); .getBeanDefinition();
listableBeanFactory.registerBeanDefinition(BEAN_NAME, bd); listableBeanFactory.registerBeanDefinition(BEAN_NAME, definition);
} }
return listableBeanFactory.getBean(BEAN_NAME, BeanTypeRegistry.class); 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);
}
} }

@ -97,4 +97,14 @@ public @interface ConditionalOnBean {
*/ */
SearchStrategy search() default SearchStrategy.ALL; 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<Name>}.
* @return the container types
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {};
} }

@ -113,4 +113,14 @@ public @interface ConditionalOnMissingBean {
*/ */
SearchStrategy search() default SearchStrategy.ALL; 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<Name>}.
* @return the container types
* @since 2.1.0
*/
Class<?>[] parameterizedContainer() default {};
} }

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.condition; package org.springframework.boot.autoconfigure.condition;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; 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.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurationMetadata; 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.boot.autoconfigure.condition.ConditionMessage.Style;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition; import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.Ordered; 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.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.MethodMetadata;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -57,6 +62,9 @@ import org.springframework.util.StringUtils;
* @author Jakub Kubrynski * @author Jakub Kubrynski
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Andy Wilkinson * @author Andy Wilkinson
* @see ConditionalOnBean
* @see ConditionalOnMissingBean
* @see ConditionalOnSingleCandidate
*/ */
@Order(Ordered.LOWEST_PRECEDENCE) @Order(Ordered.LOWEST_PRECEDENCE)
class OnBeanCondition extends FilteringSpringBootCondition 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; public static final String FACTORY_BEAN_OBJECT_TYPE = BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE;
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override @Override
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) { AutoConfigurationMetadata autoConfigurationMetadata) {
@ -102,11 +115,6 @@ class OnBeanCondition extends FilteringSpringBootCondition
return null; return null;
} }
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
@Override @Override
public ConditionOutcome getMatchOutcome(ConditionContext context, public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) { AnnotatedTypeMetadata metadata) {
@ -144,7 +152,7 @@ class OnBeanCondition extends FilteringSpringBootCondition
matchMessage = matchMessage matchMessage = matchMessage
.andCondition(ConditionalOnSingleCandidate.class, spec) .andCondition(ConditionalOnSingleCandidate.class, spec)
.found("a primary bean from beans") .found("a primary bean from beans")
.items(Style.QUOTE, matchResult.namesOfAllMatches); .items(Style.QUOTE, matchResult.getNamesOfAllMatches());
} }
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) { if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata, BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
@ -162,61 +170,8 @@ class OnBeanCondition extends FilteringSpringBootCondition
return ConditionOutcome.match(matchMessage); return ConditionOutcome.match(matchMessage);
} }
private String createOnBeanNoMatchReason(MatchResult matchResult) { protected final MatchResult getMatchingBeans(ConditionContext context,
StringBuilder reason = new StringBuilder(); BeanSearchSpec beans) {
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<String> 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<String, Collection<String>> 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) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beans.getStrategy() == SearchStrategy.ANCESTORS) { if (beans.getStrategy() == SearchStrategy.ANCESTORS) {
BeanFactory parent = beanFactory.getParentBeanFactory(); BeanFactory parent = beanFactory.getParentBeanFactory();
@ -226,11 +181,13 @@ class OnBeanCondition extends FilteringSpringBootCondition
} }
MatchResult matchResult = new MatchResult(); MatchResult matchResult = new MatchResult();
boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT; boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader());
List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType( List<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(
beans.getIgnoredTypes(), beanFactory, context, considerHierarchy); beans.getIgnoredTypes(), typeExtractor, beanFactory, context,
considerHierarchy);
for (String type : beans.getTypes()) { for (String type : beans.getTypes()) {
Collection<String> typeMatches = getBeanNamesForType(beanFactory, type, Collection<String> typeMatches = getBeanNamesForType(beanFactory, type,
context.getClassLoader(), considerHierarchy); typeExtractor, context.getClassLoader(), considerHierarchy);
typeMatches.removeAll(beansIgnoredByType); typeMatches.removeAll(beansIgnoredByType);
if (typeMatches.isEmpty()) { if (typeMatches.isEmpty()) {
matchResult.recordUnmatchedType(type); matchResult.recordUnmatchedType(type);
@ -263,12 +220,44 @@ class OnBeanCondition extends FilteringSpringBootCondition
return matchResult; return matchResult;
} }
private List<String> getNamesOfBeansIgnoredByType(List<String> ignoredTypes, private String[] getBeanNamesForAnnotation(
ListableBeanFactory beanFactory, ConditionContext context, ConfigurableListableBeanFactory beanFactory, String type,
ClassLoader classLoader, boolean considerHierarchy) throws LinkageError {
Set<String> names = new HashSet<>();
try {
@SuppressWarnings("unchecked")
Class<? extends Annotation> annotationType = (Class<? extends Annotation>) ClassUtils
.forName(type, classLoader);
collectBeanNamesForAnnotation(names, beanFactory, annotationType,
considerHierarchy);
}
catch (ClassNotFoundException ex) {
// Continue
}
return StringUtils.toStringArray(names);
}
private void collectBeanNamesForAnnotation(Set<String> names,
ListableBeanFactory beanFactory, Class<? extends Annotation> annotationType,
boolean considerHierarchy) { 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<String> getNamesOfBeansIgnoredByType(List<String> ignoredTypes,
TypeExtractor typeExtractor, ListableBeanFactory beanFactory,
ConditionContext context, boolean considerHierarchy) {
List<String> beanNames = new ArrayList<>(); List<String> beanNames = new ArrayList<>();
for (String ignoredType : ignoredTypes) { for (String ignoredType : ignoredTypes) {
beanNames.addAll(getBeanNamesForType(beanFactory, ignoredType, beanNames.addAll(getBeanNamesForType(beanFactory, ignoredType, typeExtractor,
context.getClassLoader(), considerHierarchy)); context.getClassLoader(), considerHierarchy));
} }
return beanNames; return beanNames;
@ -283,61 +272,92 @@ class OnBeanCondition extends FilteringSpringBootCondition
} }
private Collection<String> getBeanNamesForType(ListableBeanFactory beanFactory, private Collection<String> getBeanNamesForType(ListableBeanFactory beanFactory,
String type, ClassLoader classLoader, boolean considerHierarchy) String type, TypeExtractor typeExtractor, ClassLoader classLoader,
throws LinkageError { boolean considerHierarchy) throws LinkageError {
try { try {
Set<String> result = new LinkedHashSet<>(); return getBeanNamesForType(beanFactory, considerHierarchy,
collectBeanNamesForType(result, beanFactory, ClassUtils.forName(type, classLoader), typeExtractor);
ClassUtils.forName(type, classLoader), considerHierarchy);
return result;
} }
catch (ClassNotFoundException | NoClassDefFoundError ex) { catch (ClassNotFoundException | NoClassDefFoundError ex) {
return Collections.emptySet(); return Collections.emptySet();
} }
} }
private Collection<String> getBeanNamesForType(ListableBeanFactory beanFactory,
boolean considerHierarchy, Class<?> type, TypeExtractor typeExtractor) {
Set<String> result = new LinkedHashSet<>();
collectBeanNamesForType(result, beanFactory, type, typeExtractor,
considerHierarchy);
return result;
}
private void collectBeanNamesForType(Set<String> result, private void collectBeanNamesForType(Set<String> result,
ListableBeanFactory beanFactory, Class<?> type, boolean considerHierarchy) { ListableBeanFactory beanFactory, Class<?> type, TypeExtractor typeExtractor,
result.addAll(BeanTypeRegistry.get(beanFactory).getNamesForType(type)); boolean considerHierarchy) {
BeanTypeRegistry registry = BeanTypeRegistry.get(beanFactory);
result.addAll(registry.getNamesForType(type, typeExtractor));
if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) { if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) {
BeanFactory parent = ((HierarchicalBeanFactory) beanFactory) BeanFactory parent = ((HierarchicalBeanFactory) beanFactory)
.getParentBeanFactory(); .getParentBeanFactory();
if (parent instanceof ListableBeanFactory) { if (parent instanceof ListableBeanFactory) {
collectBeanNamesForType(result, (ListableBeanFactory) parent, type, collectBeanNamesForType(result, (ListableBeanFactory) parent, type,
considerHierarchy); typeExtractor, considerHierarchy);
} }
} }
} }
private String[] getBeanNamesForAnnotation( private String createOnBeanNoMatchReason(MatchResult matchResult) {
ConfigurableListableBeanFactory beanFactory, String type, StringBuilder reason = new StringBuilder();
ClassLoader classLoader, boolean considerHierarchy) throws LinkageError { appendMessageForNoMatches(reason, matchResult.getUnmatchedAnnotations(),
Set<String> names = new HashSet<>(); "annotated with");
try { appendMessageForNoMatches(reason, matchResult.getUnmatchedTypes(), "of type");
@SuppressWarnings("unchecked") appendMessageForNoMatches(reason, matchResult.getUnmatchedNames(), "named");
Class<? extends Annotation> annotationType = (Class<? extends Annotation>) ClassUtils return reason.toString();
.forName(type, classLoader);
collectBeanNamesForAnnotation(names, beanFactory, annotationType,
considerHierarchy);
} }
catch (ClassNotFoundException ex) {
// Continue private void appendMessageForNoMatches(StringBuilder reason,
Collection<String> 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<String> names, private String createOnMissingBeanNoMatchReason(MatchResult matchResult) {
ListableBeanFactory beanFactory, Class<? extends Annotation> annotationType, StringBuilder reason = new StringBuilder();
boolean considerHierarchy) { appendMessageForMatches(reason, matchResult.getMatchedAnnotations(),
names.addAll( "annotated with");
BeanTypeRegistry.get(beanFactory).getNamesForAnnotation(annotationType)); appendMessageForMatches(reason, matchResult.getMatchedTypes(), "of type");
if (considerHierarchy) { if (!matchResult.getMatchedNames().isEmpty()) {
BeanFactory parent = ((HierarchicalBeanFactory) beanFactory) if (reason.length() > 0) {
.getParentBeanFactory(); reason.append(" and ");
if (parent instanceof ListableBeanFactory) { }
collectBeanNamesForAnnotation(names, (ListableBeanFactory) parent, reason.append("found beans named ");
annotationType, considerHierarchy); reason.append(StringUtils
.collectionToDelimitedString(matchResult.getMatchedNames(), ", "));
}
return reason.toString();
}
private void appendMessageForMatches(StringBuilder reason,
Map<String, Collection<String>> 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; return null;
} }
private static class BeanSearchSpec { protected static class BeanSearchSpec {
private final Class<?> annotationType; private final Class<?> annotationType;
@ -387,10 +407,17 @@ class OnBeanCondition extends FilteringSpringBootCondition
private final List<String> ignoredTypes = new ArrayList<>(); private final List<String> ignoredTypes = new ArrayList<>();
private final List<String> parameterizedContainers = new ArrayList<>();
private final SearchStrategy strategy; private final SearchStrategy strategy;
BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata,
Class<?> annotationType) { Class<?> annotationType) {
this(context, metadata, annotationType, null);
}
public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata,
Class<?> annotationType, Class<?> genericContainer) {
this.annotationType = annotationType; this.annotationType = annotationType;
MultiValueMap<String, Object> attributes = metadata MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(annotationType.getName(), true); .getAllAnnotationAttributes(annotationType.getName(), true);
@ -400,6 +427,7 @@ class OnBeanCondition extends FilteringSpringBootCondition
collect(attributes, "annotation", this.annotations); collect(attributes, "annotation", this.annotations);
collect(attributes, "ignored", this.ignoredTypes); collect(attributes, "ignored", this.ignoredTypes);
collect(attributes, "ignoredType", this.ignoredTypes); collect(attributes, "ignoredType", this.ignoredTypes);
collect(attributes, "parameterizedContainer", this.parameterizedContainers);
this.strategy = (SearchStrategy) attributes.getFirst("search"); this.strategy = (SearchStrategy) attributes.getFirst("search");
BeanTypeDeductionException deductionException = null; BeanTypeDeductionException deductionException = null;
try { try {
@ -415,7 +443,7 @@ class OnBeanCondition extends FilteringSpringBootCondition
protected void validate(BeanTypeDeductionException ex) { protected void validate(BeanTypeDeductionException ex) {
if (!hasAtLeastOne(this.types, this.names, this.annotations)) { if (!hasAtLeastOne(this.types, this.names, this.annotations)) {
String message = annotationName() String message = getAnnotationName()
+ " did not specify a bean using type, name or annotation"; + " did not specify a bean using type, name or annotation";
if (ex == null) { if (ex == null) {
throw new IllegalStateException(message); throw new IllegalStateException(message);
@ -429,7 +457,7 @@ class OnBeanCondition extends FilteringSpringBootCondition
return Arrays.stream(lists).anyMatch((list) -> !list.isEmpty()); return Arrays.stream(lists).anyMatch((list) -> !list.isEmpty());
} }
protected String annotationName() { protected final String getAnnotationName() {
return "@" + ClassUtils.getShortName(this.annotationType); return "@" + ClassUtils.getShortName(this.annotationType);
} }
@ -460,10 +488,7 @@ class OnBeanCondition extends FilteringSpringBootCondition
private void addDeducedBeanTypeForBeanMethod(ConditionContext context, private void addDeducedBeanTypeForBeanMethod(ConditionContext context,
MethodMetadata metadata, final List<String> beanTypes) { MethodMetadata metadata, final List<String> beanTypes) {
try { try {
// We should be safe to load at this point since we are in the Class<?> returnType = getReturnType(context, metadata);
// REGISTER_BEAN phase
Class<?> returnType = ClassUtils.forName(metadata.getReturnTypeName(),
context.getClassLoader());
beanTypes.add(returnType.getName()); beanTypes.add(returnType.getName());
} }
catch (Throwable ex) { 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() { public SearchStrategy getStrategy() {
return (this.strategy != null) ? this.strategy : SearchStrategy.ALL; return (this.strategy != null) ? this.strategy : SearchStrategy.ALL;
} }
@ -531,23 +621,13 @@ class OnBeanCondition extends FilteringSpringBootCondition
@Override @Override
protected void validate(BeanTypeDeductionException ex) { 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() + ")"); + " annotations must specify only one type (got " + getTypes() + ")");
} }
} }
static final class BeanTypeDeductionException extends RuntimeException { protected static final class MatchResult {
private BeanTypeDeductionException(String className, String beanMethodName,
Throwable cause) {
super("Failed to deduce bean type for " + className + "." + beanMethodName,
cause);
}
}
static final class MatchResult {
private final Map<String, Collection<String>> matchedAnnotations = new HashMap<>(); private final Map<String, Collection<String>> matchedAnnotations = new HashMap<>();
@ -591,20 +671,54 @@ class OnBeanCondition extends FilteringSpringBootCondition
this.unmatchedTypes.add(type); this.unmatchedTypes.add(type);
} }
private boolean isAllMatched() { public boolean isAllMatched() {
return this.unmatchedAnnotations.isEmpty() && this.unmatchedNames.isEmpty() return this.unmatchedAnnotations.isEmpty() && this.unmatchedNames.isEmpty()
&& this.unmatchedTypes.isEmpty(); && this.unmatchedTypes.isEmpty();
} }
private boolean isAnyMatched() { public boolean isAnyMatched() {
return (!this.matchedAnnotations.isEmpty()) || (!this.matchedNames.isEmpty()) return (!this.matchedAnnotations.isEmpty()) || (!this.matchedNames.isEmpty())
|| (!this.matchedTypes.isEmpty()); || (!this.matchedTypes.isEmpty());
} }
private Set<String> getNamesOfAllMatches() { public Map<String, Collection<String>> getMatchedAnnotations() {
return this.matchedAnnotations;
}
public List<String> getMatchedNames() {
return this.matchedNames;
}
public Map<String, Collection<String>> getMatchedTypes() {
return this.matchedTypes;
}
public List<String> getUnmatchedAnnotations() {
return this.unmatchedAnnotations;
}
public List<String> getUnmatchedNames() {
return this.unmatchedNames;
}
public List<String> getUnmatchedTypes() {
return this.unmatchedTypes;
}
public Set<String> getNamesOfAllMatches() {
return this.namesOfAllMatches; 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);
}
}
} }

@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Date; import java.util.Date;
import java.util.function.Consumer;
import org.junit.Test; 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.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; 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.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; 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<ConfigurableApplicationContext> 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 @Configuration
@ConditionalOnBean(name = "foo") @ConditionalOnBean(name = "foo")
protected static class OnBeanNameConfiguration { protected static class OnBeanNameConfiguration {
@ -298,57 +384,146 @@ public class ConditionalOnBeanTests {
} }
@TestAnnotation @Configuration
public static class ExampleBean { public static class OriginalDefinition {
private String value; @Bean
public String testBean() {
return "test";
}
public ExampleBean(String value) {
this.value = value;
} }
@Override @Configuration
public String toString() { @ConditionalOnBean(String.class)
return this.value; public static class OverridingDefinition {
@Bean
public Integer testBean() {
return 1;
} }
} }
@Target(ElementType.TYPE) @Configuration
@Retention(RetentionPolicy.RUNTIME) @ConditionalOnBean(String.class)
@Documented public static class ConsumingConfiguration {
public @interface TestAnnotation {
ConsumingConfiguration(String testBean) {
}
} }
@Configuration @Configuration
public static class OriginalDefinition { static class ParmeterizedWithCustomConfig {
@Bean @Bean
public String testBean() { public CustomExampleBean customExampleBean() {
return "test"; return new CustomExampleBean();
} }
} }
@Configuration @Configuration
@ConditionalOnBean(String.class) static class ParmeterizedWithoutCustomConfig {
public static class OverridingDefinition {
@Bean @Bean
public Integer testBean() { public OtherExampleBean otherExampleBean() {
return 1; return new OtherExampleBean();
} }
} }
@Configuration @Configuration
@ConditionalOnBean(String.class) static class ParmeterizedWithoutCustomContainerConfig {
public static class ConsumingConfiguration {
ConsumingConfiguration(String testBean) { @Bean
public TestParameterizedContainer<OtherExampleBean> otherExampleBean() {
return new TestParameterizedContainer<OtherExampleBean>();
}
}
@Configuration
static class ParmeterizedWithCustomContainerConfig {
@Bean
public TestParameterizedContainer<CustomExampleBean> customExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@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<CustomExampleBean> conditionalCustomExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@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 {
}
} }

@ -22,6 +22,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.Date; import java.util.Date;
import java.util.function.Consumer;
import org.junit.Test; 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.condition.scan.ScannedFactoryBeanWithBeanMethodArgumentsConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter; 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.core.type.AnnotationMetadata;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat; 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<ConfigurableApplicationContext> 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 @Configuration
protected static class OnBeanInAncestorsConfiguration { 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<ExampleBean> { public static class ExampleFactoryBean implements FactoryBean<ExampleBean> {
public ExampleFactoryBean(String value) { 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> otherExampleBean() {
return new TestParameterizedContainer<OtherExampleBean>();
}
}
@Configuration
static class ParmeterizedWithCustomContainerConfig {
@Bean
public TestParameterizedContainer<CustomExampleBean> customExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@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<CustomExampleBean> conditionalCustomExampleBean() {
return new TestParameterizedContainer<CustomExampleBean>();
}
}
@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) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented

@ -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 <T> The bean type
* @author Phillip Webb
*/
public class TestParameterizedContainer<T> {
}
Loading…
Cancel
Save