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 756068f5b13d..1bf3d313ddfa 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 @@ -69,6 +69,7 @@ * @author Jakub Kubrynski * @author Stephane Nicoll * @author Andy Wilkinson + * @author Uladzislau Seuruk * @see ConditionalOnBean * @see ConditionalOnMissingBean * @see ConditionalOnSingleCandidate @@ -163,10 +164,10 @@ protected final MatchResult getMatchingBeans(ConditionContext context, Spec s beanFactory = (ConfigurableListableBeanFactory) parent; } MatchResult result = new MatchResult(); - Set beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy, + Set beansIgnoredByType = getNamesOfBeansIgnoredByType(beanFactory, considerHierarchy, spec.getIgnoredTypes(), parameterizedContainers); - for (String type : spec.getTypes()) { - Collection typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type, + for (ResolvableType type : spec.getTypes()) { + Collection typeMatches = getBeanNamesForType(considerHierarchy, beanFactory, type, parameterizedContainers); Iterator iterator = typeMatches.iterator(); while (iterator.hasNext()) { @@ -176,10 +177,10 @@ protected final MatchResult getMatchingBeans(ConditionContext context, Spec s } } if (typeMatches.isEmpty()) { - result.recordUnmatchedType(type); + result.recordUnmatchedType(type.toString()); } else { - result.recordMatchedType(type, typeMatches); + result.recordMatchedType(type.toString(), typeMatches); } } for (String annotation : spec.getAnnotations()) { @@ -204,42 +205,48 @@ protected final MatchResult getMatchingBeans(ConditionContext context, Spec s return result; } - private Set getNamesOfBeansIgnoredByType(ClassLoader classLoader, ListableBeanFactory beanFactory, - boolean considerHierarchy, Set ignoredTypes, Set> parameterizedContainers) { + private Set getNamesOfBeansIgnoredByType(ListableBeanFactory beanFactory, boolean considerHierarchy, + Set> ignoredTypes, Set> parameterizedContainers) { Set result = null; - for (String ignoredType : ignoredTypes) { - Collection ignoredNames = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, - ignoredType, parameterizedContainers); + for (Class ignoredType : ignoredTypes) { + ResolvableType ignoredResolvableType = ResolvableType.forClass(ignoredType); + Collection ignoredNames = getBeanNamesForType(considerHierarchy, beanFactory, ignoredResolvableType, + 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 { + private Set getBeanNamesForType(boolean considerHierarchy, ListableBeanFactory beanFactory, + ResolvableType type, Set> parameterizedContainers) throws LinkageError { try { - return getBeanNamesForType(beanFactory, considerHierarchy, resolve(type, classLoader), - parameterizedContainers); + return getBeanNamesForType(beanFactory, considerHierarchy, type, parameterizedContainers); } - catch (ClassNotFoundException | NoClassDefFoundError ex) { + catch (NoClassDefFoundError ex) { return Collections.emptySet(); } } - private Set getBeanNamesForType(ListableBeanFactory beanFactory, boolean considerHierarchy, Class type, - Set> parameterizedContainers) { + private Set getBeanNamesForType(ListableBeanFactory beanFactory, boolean considerHierarchy, + ResolvableType 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) { + private Set collectBeanNamesForType(ListableBeanFactory beanFactory, ResolvableType type, + Set> parameterizedContainers, Set result) { result = addAll(result, beanFactory.getBeanNamesForType(type, true, false)); for (Class container : parameterizedContainers) { ResolvableType generic = ResolvableType.forClassWithGenerics(container, type); result = addAll(result, beanFactory.getBeanNamesForType(generic, true, false)); } + return result; + } + + private Set collectBeanNamesForType(ListableBeanFactory beanFactory, boolean considerHierarchy, + ResolvableType type, Set> parameterizedContainers, Set result) { + result = collectBeanNamesForType(beanFactory, type, parameterizedContainers, result); if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) { BeanFactory parent = ((HierarchicalBeanFactory) beanFactory).getParentBeanFactory(); if (parent instanceof ListableBeanFactory) { @@ -399,11 +406,11 @@ private static class Spec { private final Set names; - private final Set types; + private final Set types; private final Set annotations; - private final Set ignoredTypes; + private final Set> ignoredTypes; private final Set> parameterizedContainers; @@ -419,10 +426,10 @@ private static class Spec { this.annotationType = annotationType; this.names = extract(attributes, "name"); this.annotations = extract(attributes, "annotation"); - this.ignoredTypes = extract(attributes, "ignored", "ignoredType"); + this.ignoredTypes = resolveWhenPossible(extract(attributes, "ignored", "ignoredType")); this.parameterizedContainers = resolveWhenPossible(extract(attributes, "parameterizedContainer")); this.strategy = annotation.getValue("search", SearchStrategy.class).orElse(null); - Set types = extractTypes(attributes); + Set types = resolveTypes(extractTypes(attributes)); BeanTypeDeductionException deductionException = null; if (types.isEmpty() && this.names.isEmpty()) { try { @@ -459,6 +466,25 @@ else if (value instanceof String) { return result.isEmpty() ? Collections.emptySet() : result; } + private Set resolveTypes(Set types) { + if (types.isEmpty()) { + return Collections.emptySet(); + } + Set resolved = new LinkedHashSet<>(types.size()); + for (String type : types) { + try { + Class typeClass = resolve(type, this.classLoader); + ResolvableType resolvableType = (typeClass.getTypeParameters().length != 0) + ? ResolvableType.forRawClass(typeClass) : ResolvableType.forClass(typeClass); + resolved.add(resolvableType); + } + catch (ClassNotFoundException | NoClassDefFoundError ex) { + resolved.add(ResolvableType.NONE); + } + } + return resolved; + } + private void merge(Set result, String... additional) { Collections.addAll(result, additional); } @@ -501,48 +527,52 @@ protected final String getAnnotationName() { return "@" + ClassUtils.getShortName(this.annotationType); } - private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { + private Set deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) { if (metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName())) { return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata); } return Collections.emptySet(); } - private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { + private Set deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) { try { - Class returnType = getReturnType(context, metadata); - return Collections.singleton(returnType.getName()); + ResolvableType returnType = getReturnType(context, metadata); + return Collections.singleton(returnType); } catch (Throwable ex) { throw new BeanTypeDeductionException(metadata.getDeclaringClassName(), metadata.getMethodName(), ex); } } - private Class getReturnType(ConditionContext context, MethodMetadata metadata) + private ResolvableType getReturnType(ConditionContext context, MethodMetadata metadata) throws ClassNotFoundException, LinkageError { // Safe to load at this point since we are in the REGISTER_BEAN phase ClassLoader classLoader = context.getClassLoader(); - Class returnType = resolve(metadata.getReturnTypeName(), classLoader); + ResolvableType returnType = getMethodReturnType(metadata, classLoader); if (isParameterizedContainer(returnType)) { - returnType = getReturnTypeGeneric(metadata, classLoader); + returnType = returnType.getGeneric(); } return returnType; } - private boolean isParameterizedContainer(Class type) { + private boolean isParameterizedContainer(ResolvableType type) { + Class rawType = type.getRawClass(); + if (rawType == null) { + return false; + } for (Class parameterizedContainer : this.parameterizedContainers) { - if (parameterizedContainer.isAssignableFrom(type)) { + if (parameterizedContainer.isAssignableFrom(rawType)) { return true; } } return false; } - private Class getReturnTypeGeneric(MethodMetadata metadata, ClassLoader classLoader) + private ResolvableType getMethodReturnType(MethodMetadata metadata, ClassLoader classLoader) throws ClassNotFoundException, LinkageError { Class declaringClass = resolve(metadata.getDeclaringClassName(), classLoader); Method beanMethod = findBeanMethod(declaringClass, metadata.getMethodName()); - return ResolvableType.forMethodReturnType(beanMethod).resolveGeneric(); + return ResolvableType.forMethodReturnType(beanMethod); } private Method findBeanMethod(Class declaringClass, String methodName) { @@ -572,7 +602,7 @@ Set getNames() { return this.names; } - Set getTypes() { + Set getTypes() { return this.types; } @@ -580,7 +610,7 @@ Set getAnnotations() { return this.annotations; } - Set getIgnoredTypes() { + Set> getIgnoredTypes() { return this.ignoredTypes; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanGenericReturnTypeTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanGenericReturnTypeTests.java new file mode 100644 index 000000000000..e57101e5f4cc --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnBeanGenericReturnTypeTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2012-2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnBean @ConditionalOnBean} with generic bean return type. + * + * @author Uladzislau Seuruk + */ +class ConditionalOnBeanGenericReturnTypeTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void genericWhenTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, GenericWithStringTypeArgumentsConfig.class, + GenericWithIntegerTypeArgumentsConfig.class) + .run((context) -> assertThat(context).satisfies( + exampleBeanRequirement("customExampleBean", "genericStringTypeArgumentsExampleBean"))); + } + + @Test + void genericWhenTypeArgumentWithValueMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringConfig.class, TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies( + exampleBeanRequirement("genericStringExampleBean", "genericStringWithValueExampleBean"))); + } + + @Test + void genericWithValueWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean", "genericStringWithValueExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(GenericWithIntegerConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericIntegerExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericStringExampleBean", + "parameterizedContainerGenericExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies( + exampleBeanRequirement("customExampleBean", "parameterizedContainerGenericExampleBean"))); + } + + private Consumer exampleBeanRequirement(String... names) { + return (context) -> { + String[] beans = context.getBeanNamesForType(GenericExampleBean.class); + String[] containers = context.getBeanNamesForType(TestParameterizedContainer.class); + assertThat(StringUtils.concatenateStringArrays(beans, containers)).containsOnly(names); + }; + } + + @Configuration(proxyBeanMethods = false) + static class ParameterizedWithCustomConfig { + + @Bean + CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithStringConfig { + + @Bean + GenericExampleBean genericStringExampleBean() { + return new GenericExampleBean<>("genericStringExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithStringTypeArgumentsConfig { + + @Bean + @ConditionalOnBean + GenericExampleBean genericStringTypeArgumentsExampleBean() { + return new GenericExampleBean<>("genericStringTypeArgumentsExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithIntegerConfig { + + @Bean + GenericExampleBean genericIntegerExampleBean() { + return new GenericExampleBean<>(1_000); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithIntegerTypeArgumentsConfig { + + @Bean + @ConditionalOnBean + GenericExampleBean genericIntegerTypeArgumentsExampleBean() { + return new GenericExampleBean<>(1_000); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithValueConfig { + + @Bean + @ConditionalOnBean(GenericExampleBean.class) + GenericExampleBean genericStringWithValueExampleBean() { + return new GenericExampleBean<>("genericStringWithValueExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithParameterizedContainerConfig { + + @Bean + @ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class) + TestParameterizedContainer> parameterizedContainerGenericExampleBean() { + return new TestParameterizedContainer<>(); + } + + } + + @TestAnnotation + static class GenericExampleBean { + + private final T value; + + GenericExampleBean(T value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } + + } + + static class CustomExampleBean extends GenericExampleBean { + + CustomExampleBean() { + super("custom subclass"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface TestAnnotation { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanGenericReturnTypeTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanGenericReturnTypeTests.java new file mode 100644 index 000000000000..d9b5178b7879 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBeanGenericReturnTypeTests.java @@ -0,0 +1,204 @@ +/* + * Copyright 2012-2021 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConditionalOnMissingBean @ConditionalOnMissingBean} with generic bean + * return type. + * + * @author Uladzislau Seuruk + */ +@SuppressWarnings("resource") +class ConditionalOnMissingBeanGenericReturnTypeTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); + + @Test + void genericWhenTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringTypeArgumentsConfig.class, + GenericWithIntegerTypeArgumentsConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("genericStringExampleBean", "genericIntegerExampleBean"))); + } + + @Test + void genericWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, GenericWithStringTypeArgumentsConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + void genericWhenSubclassTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, GenericWithIntegerTypeArgumentsConfig.class) + .run((context) -> assertThat(context) + .satisfies(exampleBeanRequirement("customExampleBean", "genericIntegerExampleBean"))); + } + + @Test + void genericWhenTypeArgumentWithValueMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringTypeArgumentsConfig.class, + TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericStringExampleBean"))); + } + + @Test + void genericWithValueWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, TypeArgumentsConditionWithValueConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("customExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentNotMatches() { + this.contextRunner + .withUserConfiguration(GenericWithIntegerTypeArgumentsConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericIntegerExampleBean", + "parameterizedContainerGenericExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(GenericWithStringTypeArgumentsConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("genericStringExampleBean"))); + } + + @Test + void parameterizedContainerGenericWhenSubclassTypeArgumentMatches() { + this.contextRunner + .withUserConfiguration(ParameterizedWithCustomConfig.class, + TypeArgumentsConditionWithParameterizedContainerConfig.class) + .run((context) -> assertThat(context).satisfies(exampleBeanRequirement("customExampleBean"))); + } + + private Consumer exampleBeanRequirement(String... names) { + return (context) -> { + String[] beans = context.getBeanNamesForType(GenericExampleBean.class); + String[] containers = context.getBeanNamesForType(TestParameterizedContainer.class); + assertThat(StringUtils.concatenateStringArrays(beans, containers)).containsOnly(names); + }; + } + + @Configuration(proxyBeanMethods = false) + static class ParameterizedWithCustomConfig { + + @Bean + CustomExampleBean customExampleBean() { + return new CustomExampleBean(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithStringTypeArgumentsConfig { + + @Bean + @ConditionalOnMissingBean + GenericExampleBean genericStringExampleBean() { + return new GenericExampleBean<>("genericStringExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class GenericWithIntegerTypeArgumentsConfig { + + @Bean + @ConditionalOnMissingBean + GenericExampleBean genericIntegerExampleBean() { + return new GenericExampleBean<>(1_000); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithValueConfig { + + @Bean + @ConditionalOnMissingBean(GenericExampleBean.class) + GenericExampleBean genericStringWithValueExampleBean() { + return new GenericExampleBean<>("genericStringWithValueExampleBean"); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TypeArgumentsConditionWithParameterizedContainerConfig { + + @Bean + @ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class) + TestParameterizedContainer> parameterizedContainerGenericExampleBean() { + return new TestParameterizedContainer<>(); + } + + } + + @TestAnnotation + static class GenericExampleBean { + + private final T value; + + GenericExampleBean(T value) { + this.value = value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } + + } + + static class CustomExampleBean extends GenericExampleBean { + + CustomExampleBean() { + super("custom subclass"); + } + + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface TestAnnotation { + + } + +}