From ff9f5fd1c71d257f049e6aac504fe1cef360589e Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 20 Jul 2019 13:36:56 +0100 Subject: [PATCH] Polish OnBeanCondition Polish `OnBeanCondition` and make it easier to remove `BeanTypeRegistry` in the future. See gh-17594 --- .../FilteringSpringBootCondition.java | 27 +- .../condition/OnBeanCondition.java | 504 +++++++++++------- .../ConditionalOnMissingBeanTests.java | 6 +- .../ConditionalOnSingleCandidateTests.java | 2 +- 4 files changed, 319 insertions(+), 220 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java index 2c023e8e8b..76f1700745 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/condition/FilteringSpringBootCondition.java @@ -81,7 +81,7 @@ abstract class FilteringSpringBootCondition extends SpringBootCondition this.beanClassLoader = classLoader; } - protected List filter(Collection classNames, ClassNameFilter classNameFilter, + protected final List filter(Collection classNames, ClassNameFilter classNameFilter, ClassLoader classLoader) { if (CollectionUtils.isEmpty(classNames)) { return Collections.emptyList(); @@ -95,6 +95,22 @@ abstract class FilteringSpringBootCondition extends SpringBootCondition return matches; } + /** + * Slightly faster variant of {@link ClassUtils#forName(String, ClassLoader)} that + * doesn't deal with primitives, arrays or innter types. + * @param className the class name to resolve + * @param classLoader the class loader to use + * @return a resolved class + * @throws ClassNotFoundException if the class cannot be found + */ + protected static Class resolve(String className, ClassLoader classLoader) throws ClassNotFoundException { + if (classLoader != null) { + return classLoader.loadClass(className); + } + return Class.forName(className); + + } + protected enum ClassNameFilter { PRESENT { @@ -122,7 +138,7 @@ abstract class FilteringSpringBootCondition extends SpringBootCondition classLoader = ClassUtils.getDefaultClassLoader(); } try { - forName(className, classLoader); + resolve(className, classLoader); return true; } catch (Throwable ex) { @@ -130,13 +146,6 @@ abstract class FilteringSpringBootCondition extends SpringBootCondition } } - private static Class forName(String className, ClassLoader classLoader) throws ClassNotFoundException { - if (classLoader != null) { - return classLoader.loadClass(className); - } - return Class.forName(className); - } - } } 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 8b28f70a70..84dbaad269 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 @@ -44,12 +44,17 @@ 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.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotation.Adapt; +import org.springframework.core.annotation.MergedAnnotationCollectors; +import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotations; 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.CollectionUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -69,11 +74,7 @@ import org.springframework.util.StringUtils; @Order(Ordered.LOWEST_PRECEDENCE) class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition { - /** - * Bean definition attribute name for factory beans to signal their product type (if - * known and it can't be deduced from the factory bean class). - */ - public static final String FACTORY_BEAN_OBJECT_TYPE = BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE; + private static final TypeExtractor RESOLVING_EXTRACTOR = ResolvableType::resolve; @Override public ConfigurationPhase getConfigurationPhase() { @@ -112,128 +113,164 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { ConditionMessage matchMessage = ConditionMessage.empty(); - if (metadata.isAnnotated(ConditionalOnBean.class.getName())) { - BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnBean.class); + MergedAnnotations annotations = metadata.getAnnotations(); + if (annotations.isPresent(ConditionalOnBean.class)) { + Spec spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class); MatchResult matchResult = getMatchingBeans(context, spec); if (!matchResult.isAllMatched()) { String reason = createOnBeanNoMatchReason(matchResult); - return ConditionOutcome - .noMatch(ConditionMessage.forCondition(ConditionalOnBean.class, spec).because(reason)); + return ConditionOutcome.noMatch(spec.message().because(reason)); } - matchMessage = matchMessage.andCondition(ConditionalOnBean.class, spec).found("bean", "beans") - .items(Style.QUOTE, matchResult.getNamesOfAllMatches()); + matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE, + matchResult.getNamesOfAllMatches()); } if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) { - BeanSearchSpec spec = new SingleCandidateBeanSearchSpec(context, metadata, - ConditionalOnSingleCandidate.class); + Spec spec = new SingleCandidateSpec(context, metadata, annotations); MatchResult matchResult = getMatchingBeans(context, spec); if (!matchResult.isAllMatched()) { - return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, spec) - .didNotFind("any beans").atAll()); + return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll()); } else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(), spec.getStrategy() == SearchStrategy.ALL)) { - return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, spec) - .didNotFind("a primary bean from beans") + return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans") .items(Style.QUOTE, matchResult.getNamesOfAllMatches())); } - matchMessage = matchMessage.andCondition(ConditionalOnSingleCandidate.class, spec) - .found("a primary bean from beans").items(Style.QUOTE, matchResult.getNamesOfAllMatches()); + matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE, + matchResult.getNamesOfAllMatches()); } if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) { - BeanSearchSpec spec = new BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class); + Spec spec = new Spec<>(context, metadata, annotations, + ConditionalOnMissingBean.class); MatchResult matchResult = getMatchingBeans(context, spec); if (matchResult.isAnyMatched()) { String reason = createOnMissingBeanNoMatchReason(matchResult); - return ConditionOutcome - .noMatch(ConditionMessage.forCondition(ConditionalOnMissingBean.class, spec).because(reason)); + return ConditionOutcome.noMatch(spec.message().because(reason)); } - matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec).didNotFind("any beans") - .atAll(); + matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll(); } return ConditionOutcome.match(matchMessage); } - protected final MatchResult getMatchingBeans(ConditionContext context, BeanSearchSpec beans) { + protected final MatchResult getMatchingBeans(ConditionContext context, Spec spec) { + ClassLoader classLoader = context.getClassLoader(); ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); - if (beans.getStrategy() == SearchStrategy.ANCESTORS) { + boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT; + Set> parameterizedContainers = spec.getParameterizedContainers(); + if (spec.getStrategy() == SearchStrategy.ANCESTORS) { BeanFactory parent = beanFactory.getParentBeanFactory(); Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent, "Unable to use SearchStrategy.PARENTS"); beanFactory = (ConfigurableListableBeanFactory) parent; } - MatchResult matchResult = new MatchResult(); - boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT; - TypeExtractor typeExtractor = beans.getTypeExtractor(context.getClassLoader()); - List beansIgnoredByType = getNamesOfBeansIgnoredByType(beans.getIgnoredTypes(), typeExtractor, - beanFactory, context, considerHierarchy); - for (String type : beans.getTypes()) { - Collection typeMatches = getBeanNamesForType(beanFactory, type, typeExtractor, - context.getClassLoader(), considerHierarchy); + MatchResult result = new MatchResult(); + Set beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy, + spec.getIgnoredTypes(), parameterizedContainers); + for (String type : spec.getTypes()) { + Collection typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type, + parameterizedContainers); typeMatches.removeAll(beansIgnoredByType); if (typeMatches.isEmpty()) { - matchResult.recordUnmatchedType(type); + result.recordUnmatchedType(type); } else { - matchResult.recordMatchedType(type, typeMatches); + result.recordMatchedType(type, typeMatches); } } - for (String annotation : beans.getAnnotations()) { - List annotationMatches = Arrays.asList( - getBeanNamesForAnnotation(beanFactory, annotation, context.getClassLoader(), considerHierarchy)); + for (String annotation : spec.getAnnotations()) { + Set annotationMatches = getBeanNamesForAnnotation(classLoader, beanFactory, annotation, + considerHierarchy); annotationMatches.removeAll(beansIgnoredByType); if (annotationMatches.isEmpty()) { - matchResult.recordUnmatchedAnnotation(annotation); + result.recordUnmatchedAnnotation(annotation); } else { - matchResult.recordMatchedAnnotation(annotation, annotationMatches); + result.recordMatchedAnnotation(annotation, annotationMatches); } } - for (String beanName : beans.getNames()) { + for (String beanName : spec.getNames()) { if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) { - matchResult.recordMatchedName(beanName); + result.recordMatchedName(beanName); } else { - matchResult.recordUnmatchedName(beanName); + result.recordUnmatchedName(beanName); } } - return matchResult; + return result; } - private String[] getBeanNamesForAnnotation(ConfigurableListableBeanFactory beanFactory, String type, - ClassLoader classLoader, boolean considerHierarchy) throws LinkageError { - Set names = new HashSet<>(); + private Set getNamesOfBeansIgnoredByType(ClassLoader classLoader, ListableBeanFactory beanFactory, + boolean considerHierarchy, Set ignoredTypes, Set> parameterizedContainers) { + Set result = null; + for (String ignoredType : ignoredTypes) { + Collection ignoredNames = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, + ignoredType, parameterizedContainers); + result = addAll(result, ignoredNames); + } + return (result != null) ? result : Collections.emptySet(); + } + + private Set getBeanNamesForType(ClassLoader classLoader, boolean considerHierarchy, + ListableBeanFactory beanFactory, String type, Set> parameterizedContainers) throws LinkageError { try { - @SuppressWarnings("unchecked") - Class annotationType = (Class) ClassUtils.forName(type, - classLoader); - collectBeanNamesForAnnotation(names, beanFactory, annotationType, considerHierarchy); + return getBeanNamesForType(beanFactory, considerHierarchy, resolve(type, classLoader), + parameterizedContainers); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + return Collections.emptySet(); + } + } + + private Set getBeanNamesForType(ListableBeanFactory beanFactory, boolean considerHierarchy, Class type, + Set> parameterizedContainers) { + Set result = collectBeanNamesForType(beanFactory, considerHierarchy, type, parameterizedContainers, + null); + return (result != null) ? result : Collections.emptySet(); + } + + private Set collectBeanNamesForType(ListableBeanFactory beanFactory, boolean considerHierarchy, + Class type, Set> parameterizedContainers, Set result) { + BeanTypeRegistry registry = BeanTypeRegistry.get(beanFactory); + result = addAll(result, registry.getNamesForType(type, getTypeExtractor(parameterizedContainers))); + if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) { + BeanFactory parent = ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory(); + if (parent instanceof ListableBeanFactory) { + result = collectBeanNamesForType((ListableBeanFactory) parent, considerHierarchy, type, + parameterizedContainers, result); + } + } + return result; + } + + private Set getBeanNamesForAnnotation(ClassLoader classLoader, ConfigurableListableBeanFactory beanFactory, + String type, boolean considerHierarchy) throws LinkageError { + Set result = null; + try { + result = collectBeanNamesForAnnotation(beanFactory, resolveAnnotationType(classLoader, type), + considerHierarchy, result); } catch (ClassNotFoundException ex) { // Continue } - return StringUtils.toStringArray(names); + return (result != null) ? result : Collections.emptySet(); } - private void collectBeanNamesForAnnotation(Set names, ListableBeanFactory beanFactory, - Class annotationType, boolean considerHierarchy) { + @SuppressWarnings("unchecked") + private Class resolveAnnotationType(ClassLoader classLoader, String type) + throws ClassNotFoundException { + return (Class) resolve(type, classLoader); + } + + private Set collectBeanNamesForAnnotation(ListableBeanFactory beanFactory, + Class annotationType, boolean considerHierarchy, Set result) { BeanTypeRegistry registry = BeanTypeRegistry.get(beanFactory); - names.addAll(registry.getNamesForAnnotation(annotationType)); + result = addAll(result, registry.getNamesForAnnotation(annotationType)); if (considerHierarchy) { BeanFactory parent = ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory(); if (parent instanceof ListableBeanFactory) { - collectBeanNamesForAnnotation(names, (ListableBeanFactory) parent, annotationType, considerHierarchy); + result = collectBeanNamesForAnnotation((ListableBeanFactory) parent, annotationType, considerHierarchy, + result); } } - } - - 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, typeExtractor, context.getClassLoader(), - considerHierarchy)); - } - return beanNames; + return result; } private boolean containsBean(ConfigurableListableBeanFactory beanFactory, String beanName, @@ -244,36 +281,15 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat return beanFactory.containsLocalBean(beanName); } - private Collection getBeanNamesForType(ListableBeanFactory beanFactory, String type, - TypeExtractor typeExtractor, ClassLoader classLoader, boolean considerHierarchy) throws LinkageError { - try { - return getBeanNamesForType(beanFactory, considerHierarchy, ClassUtils.forName(type, classLoader), - typeExtractor); + private Set addAll(Set result, Collection additional) { + if (CollectionUtils.isEmpty(additional)) { + return result; } - 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); + result = (result != null) ? result : new LinkedHashSet<>(); + result.addAll(additional); return result; } - private void collectBeanNamesForType(Set result, 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, typeExtractor, considerHierarchy); - } - } - } - private String createOnBeanNoMatchReason(MatchResult matchResult) { StringBuilder reason = new StringBuilder(); appendMessageForNoMatches(reason, matchResult.getUnmatchedAnnotations(), "annotated with"); @@ -354,53 +370,122 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat return null; } - protected static class BeanSearchSpec { + private TypeExtractor getTypeExtractor(Set> parameterizedContainers) { + if (parameterizedContainers.isEmpty()) { + return RESOLVING_EXTRACTOR; + } + return (type) -> { + Class resolved = RESOLVING_EXTRACTOR.getBeanType(type); + if (isParameterizedContainer(resolved, parameterizedContainers)) { + resolved = type.getGeneric().resolve(); + } + return resolved; + }; + } + + private boolean isParameterizedContainer(Class type, Set> parameterizedContainers) { + for (Class parameterizedContainer : parameterizedContainers) { + if (parameterizedContainer.isAssignableFrom(type)) { + return true; + } + } + return false; + } + + /** + * A search specification extracted from the underlying annotation. + */ + private static class Spec { + + private final ClassLoader classLoader; private final Class annotationType; - private final List names = new ArrayList<>(); + private final Set names; - private final List types = new ArrayList<>(); + private final Set types; - private final List annotations = new ArrayList<>(); + private final Set annotations; - private final List ignoredTypes = new ArrayList<>(); + private final Set ignoredTypes; - private final List parameterizedContainers = new ArrayList<>(); + private final Set> parameterizedContainers; private final SearchStrategy strategy; - public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class annotationType) { - this(context, metadata, annotationType, null); - } - - public BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class annotationType, - Class genericContainer) { + Spec(ConditionContext context, AnnotatedTypeMetadata metadata, MergedAnnotations annotations, + Class annotationType) { + MultiValueMap attributes = annotations.stream(annotationType) + .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) + .collect(MergedAnnotationCollectors.toMultiValueMap(Adapt.CLASS_TO_STRING)); + MergedAnnotation annotation = annotations.get(annotationType); + this.classLoader = context.getClassLoader(); this.annotationType = annotationType; - MultiValueMap attributes = metadata.getAllAnnotationAttributes(annotationType.getName(), - true); - collect(attributes, "name", this.names); - collect(attributes, "value", this.types); - collect(attributes, "type", this.types); - 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"); + this.names = extract(attributes, "name"); + this.annotations = extract(attributes, "annotation"); + this.ignoredTypes = extract(attributes, "ignored", "ignoredType"); + this.parameterizedContainers = resolveWhenPossible(extract(attributes, "parameterizedContainer")); + this.strategy = annotation.getValue("search", SearchStrategy.class).orElse(null); + Set types = extractTypes(attributes); BeanTypeDeductionException deductionException = null; - try { - if (this.types.isEmpty() && this.names.isEmpty()) { - addDeducedBeanType(context, metadata, this.types); + if (types.isEmpty() && this.names.isEmpty()) { + try { + types = deducedBeanType(context, metadata); + } + catch (BeanTypeDeductionException ex) { + deductionException = ex; } } - catch (BeanTypeDeductionException ex) { - deductionException = ex; - } + this.types = types; validate(deductionException); } + protected Set extractTypes(MultiValueMap attributes) { + return extract(attributes, "value", "type"); + } + + private Set extract(MultiValueMap attributes, String... attributeNames) { + if (attributes.isEmpty()) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet<>(); + for (String attributeName : attributeNames) { + List values = attributes.getOrDefault(attributeName, Collections.emptyList()); + for (Object value : values) { + if (value instanceof String[]) { + merge(result, (String[]) value); + } + else if (value instanceof String) { + merge(result, (String) value); + } + } + } + return result.isEmpty() ? Collections.emptySet() : result; + } + + private void merge(Set result, String... additional) { + for (String addition : additional) { + result.add(addition); + } + } + + private Set> resolveWhenPossible(Set classNames) { + if (classNames.isEmpty()) { + return Collections.emptySet(); + } + Set> resolved = new LinkedHashSet<>(classNames.size()); + for (String className : classNames) { + try { + resolved.add(resolve(className, this.classLoader)); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + } + } + return resolved; + } + protected void validate(BeanTypeDeductionException ex) { - if (!hasAtLeastOne(this.types, this.names, this.annotations)) { + if (!hasAtLeastOneElement(this.types, this.names, this.annotations)) { String message = getAnnotationName() + " did not specify a bean using type, name or annotation"; if (ex == null) { throw new IllegalStateException(message); @@ -409,40 +494,30 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat } } - private boolean hasAtLeastOne(List... lists) { - return Arrays.stream(lists).anyMatch((list) -> !list.isEmpty()); + private boolean hasAtLeastOneElement(Set... sets) { + for (Set set : sets) { + if (!set.isEmpty()) { + return true; + } + } + return false; } protected final String getAnnotationName() { return "@" + ClassUtils.getShortName(this.annotationType); } - protected void collect(MultiValueMap attributes, String key, List destination) { - List values = attributes.get(key); - if (values != null) { - for (Object value : values) { - if (value instanceof String[]) { - Collections.addAll(destination, (String[]) value); - } - else { - destination.add((String) value); - } - } - } - } - - private void addDeducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata, - final List beanTypes) { + private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { if (metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName())) { - addDeducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata, beanTypes); + return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata); } + return Collections.emptySet(); } - private void addDeducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata, - final List beanTypes) { + private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { try { Class returnType = getReturnType(context, metadata); - beanTypes.add(returnType.getName()); + return Collections.singleton(returnType.getName()); } catch (Throwable ex) { throw new BeanTypeDeductionException(metadata.getDeclaringClassName(), metadata.getMethodName(), ex); @@ -451,19 +526,27 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat 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 + // 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)) { + Class returnType = resolve(metadata.getReturnTypeName(), classLoader); + if (isParameterizedContainer(returnType)) { returnType = getReturnTypeGeneric(metadata, classLoader); } return returnType; } + private boolean isParameterizedContainer(Class type) { + for (Class parameterizedContainer : this.parameterizedContainers) { + if (parameterizedContainer.isAssignableFrom(type)) { + return true; + } + } + return false; + } + private Class getReturnTypeGeneric(MethodMetadata metadata, ClassLoader classLoader) throws ClassNotFoundException, LinkageError { - Class declaringClass = ClassUtils.forName(metadata.getDeclaringClassName(), classLoader); + Class declaringClass = resolve(metadata.getDeclaringClassName(), classLoader); Method beanMethod = findBeanMethod(declaringClass, metadata.getMethodName()); return ResolvableType.forMethodReturnType(beanMethod).resolveGeneric(); } @@ -473,10 +556,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat 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)); + Method[] candidates = ReflectionUtils.getAllDeclaredMethods(declaringClass); + for (Method candidate : candidates) { + if (candidate.getName().equals(methodName) && isBeanMethod(candidate)) { + return candidate; + } + } + throw new IllegalStateException("Unable to find bean method " + methodName); } private boolean isBeanMethod(Method method) { @@ -484,67 +570,58 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat .isPresent(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() { + private SearchStrategy getStrategy() { return (this.strategy != null) ? this.strategy : SearchStrategy.ALL; } - public List getNames() { + Set getNames() { return this.names; } - public List getTypes() { + Set getTypes() { return this.types; } - public List getAnnotations() { + Set getAnnotations() { return this.annotations; } - public List getIgnoredTypes() { + Set getIgnoredTypes() { return this.ignoredTypes; } + Set> getParameterizedContainers() { + return this.parameterizedContainers; + } + + ConditionMessage.Builder message() { + return ConditionMessage.forCondition(ConditionalOnBean.class, this); + } + + ConditionMessage.Builder message(ConditionMessage message) { + return message.andCondition(ConditionalOnBean.class, this); + } + @Override public String toString() { + boolean hasNames = !this.names.isEmpty(); + boolean hasTypes = !this.types.isEmpty(); + boolean hasIgnoredTypes = !this.ignoredTypes.isEmpty(); StringBuilder string = new StringBuilder(); string.append("("); - if (!this.names.isEmpty()) { + if (hasNames) { string.append("names: "); string.append(StringUtils.collectionToCommaDelimitedString(this.names)); - if (!this.types.isEmpty()) { - string.append("; "); - } + string.append(hasTypes ? "" : "; "); } - if (!this.types.isEmpty()) { + if (hasTypes) { string.append("types: "); string.append(StringUtils.collectionToCommaDelimitedString(this.types)); } + if (hasIgnoredTypes) { + string.append("ignored: "); + string.append(StringUtils.collectionToCommaDelimitedString(this.ignoredTypes)); + } string.append("; SearchStrategy: "); string.append(this.strategy.toString().toLowerCase(Locale.ENGLISH)); string.append(")"); @@ -553,28 +630,38 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat } - private static class SingleCandidateBeanSearchSpec extends BeanSearchSpec { + /** + * Specialized {@link Spec specification} for + * {@link ConditionalOnSingleCandidate @ConditionalOnSingleCandidate}. + */ + private static class SingleCandidateSpec extends Spec { - SingleCandidateBeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, - Class annotationType) { - super(context, metadata, annotationType); + private static final Collection FILTERED_TYPES = Arrays.asList("", Object.class.getName()); + + SingleCandidateSpec(ConditionContext context, AnnotatedTypeMetadata metadata, MergedAnnotations annotations) { + super(context, metadata, annotations, ConditionalOnSingleCandidate.class); } @Override - protected void collect(MultiValueMap attributes, String key, List destination) { - super.collect(attributes, key, destination); - destination.removeAll(Arrays.asList("", Object.class.getName())); + protected Set extractTypes(MultiValueMap attributes) { + Set types = super.extractTypes(attributes); + types.removeAll(FILTERED_TYPES); + return types; } @Override protected void validate(BeanTypeDeductionException ex) { Assert.isTrue(getTypes().size() == 1, - () -> getAnnotationName() + " annotations must specify only one type (got " + getTypes() + ")"); + () -> getAnnotationName() + " annotations must specify only one type (got " + + StringUtils.collectionToCommaDelimitedString(getTypes()) + ")"); } } - protected static final class MatchResult { + /** + * Results collected during the condition evaluation. + */ + private static final class MatchResult { private final Map> matchedAnnotations = new HashMap<>(); @@ -617,46 +704,49 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat this.unmatchedTypes.add(type); } - public boolean isAllMatched() { + boolean isAllMatched() { return this.unmatchedAnnotations.isEmpty() && this.unmatchedNames.isEmpty() && this.unmatchedTypes.isEmpty(); } - public boolean isAnyMatched() { + boolean isAnyMatched() { return (!this.matchedAnnotations.isEmpty()) || (!this.matchedNames.isEmpty()) || (!this.matchedTypes.isEmpty()); } - public Map> getMatchedAnnotations() { + Map> getMatchedAnnotations() { return this.matchedAnnotations; } - public List getMatchedNames() { + List getMatchedNames() { return this.matchedNames; } - public Map> getMatchedTypes() { + Map> getMatchedTypes() { return this.matchedTypes; } - public List getUnmatchedAnnotations() { + List getUnmatchedAnnotations() { return this.unmatchedAnnotations; } - public List getUnmatchedNames() { + List getUnmatchedNames() { return this.unmatchedNames; } - public List getUnmatchedTypes() { + List getUnmatchedTypes() { return this.unmatchedTypes; } - public Set getNamesOfAllMatches() { + Set getNamesOfAllMatches() { return this.namesOfAllMatches; } } + /** + * Exteption thrown when the bean type cannot be deduced. + */ static final class BeanTypeDeductionException extends RuntimeException { private BeanTypeDeductionException(String className, String beanMethodName, Throwable cause) { 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 d0c118aeff..3af45a72a0 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 @@ -438,7 +438,7 @@ public class ConditionalOnMissingBeanTests { public void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(NonspecificFactoryBean.class); builder.addConstructorArgValue("foo"); - builder.getBeanDefinition().setAttribute(OnBeanCondition.FACTORY_BEAN_OBJECT_TYPE, ExampleBean.class); + builder.getBeanDefinition().setAttribute(BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE, ExampleBean.class); registry.registerBeanDefinition("exampleBeanFactoryBean", builder.getBeanDefinition()); } @@ -456,7 +456,7 @@ public class ConditionalOnMissingBeanTests { public void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(NonspecificFactoryBean.class); builder.addConstructorArgValue("foo"); - builder.getBeanDefinition().setAttribute(OnBeanCondition.FACTORY_BEAN_OBJECT_TYPE, + builder.getBeanDefinition().setAttribute(BeanTypeRegistry.FACTORY_BEAN_OBJECT_TYPE, ExampleBean.class.getName()); registry.registerBeanDefinition("exampleBeanFactoryBean", builder.getBeanDefinition()); } @@ -513,7 +513,7 @@ public class ConditionalOnMissingBeanTests { @Bean @ConditionalOnMissingBean(value = ExampleBean.class, - ignoredType = "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBeanTests.CustomExampleBean") + ignoredType = "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBeanTests$CustomExampleBean") ExampleBean exampleBean() { return new ExampleBean("test"); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java index be8366b24b..6742609bbc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnSingleCandidateTests.java @@ -171,7 +171,7 @@ class ConditionalOnSingleCandidateTests { } @Configuration(proxyBeanMethods = false) - @ConditionalOnSingleCandidate(value = String.class, type = "java.lang.String") + @ConditionalOnSingleCandidate(value = String.class, type = "java.lang.Integer") static class OnBeanSingleCandidateTwoTypesConfiguration { }