mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-05-03 03:43:54 +01:00
Add ConditionalOnBean support for generic @Bean return types
Update `ConditionalOnBean` so support generics when resolving the `@Bean` method. See gh-29500 Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
committed by
Phillip Webb
parent
fcad1fa12a
commit
aa7864ea38
+47
-60
@@ -76,6 +76,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Jakub Kubrynski
|
||||
* @author Stephane Nicoll
|
||||
* @author Andy Wilkinson
|
||||
* @author Uladzislau Seuruk
|
||||
* @see ConditionalOnBean
|
||||
* @see ConditionalOnMissingBean
|
||||
* @see ConditionalOnSingleCandidate
|
||||
@@ -205,13 +206,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
ConfigurableListableBeanFactory beanFactory = getSearchBeanFactory(spec);
|
||||
ClassLoader classLoader = spec.getContext().getClassLoader();
|
||||
boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
|
||||
Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();
|
||||
Set<ResolvableType> parameterizedContainers = spec.getParameterizedContainers();
|
||||
MatchResult result = new MatchResult();
|
||||
Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy,
|
||||
Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(beanFactory, considerHierarchy,
|
||||
spec.getIgnoredTypes(), parameterizedContainers);
|
||||
for (String type : spec.getTypes()) {
|
||||
Map<String, BeanDefinition> typeMatchedDefinitions = getBeanDefinitionsForType(classLoader,
|
||||
considerHierarchy, beanFactory, type, parameterizedContainers);
|
||||
for (ResolvableType type : spec.getTypes()) {
|
||||
Map<String, BeanDefinition> typeMatchedDefinitions = getBeanDefinitionsForType(beanFactory,
|
||||
considerHierarchy, type, parameterizedContainers);
|
||||
Set<String> typeMatchedNames = matchedNamesFrom(typeMatchedDefinitions,
|
||||
(name, definition) -> !ScopedProxyUtils.isScopedTarget(name)
|
||||
&& isCandidate(beanFactory, name, definition, beansIgnoredByType));
|
||||
@@ -296,42 +297,31 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
return true;
|
||||
}
|
||||
|
||||
private Set<String> getNamesOfBeansIgnoredByType(ClassLoader classLoader, ListableBeanFactory beanFactory,
|
||||
boolean considerHierarchy, Set<String> ignoredTypes, Set<Class<?>> parameterizedContainers) {
|
||||
private Set<String> getNamesOfBeansIgnoredByType(ListableBeanFactory beanFactory, boolean considerHierarchy,
|
||||
Set<ResolvableType> ignoredTypes, Set<ResolvableType> parameterizedContainers) {
|
||||
Set<String> result = null;
|
||||
for (String ignoredType : ignoredTypes) {
|
||||
Collection<String> ignoredNames = getBeanDefinitionsForType(classLoader, considerHierarchy, beanFactory,
|
||||
ignoredType, parameterizedContainers)
|
||||
for (ResolvableType ignoredType : ignoredTypes) {
|
||||
Collection<String> ignoredNames = getBeanDefinitionsForType(beanFactory, considerHierarchy, ignoredType,
|
||||
parameterizedContainers)
|
||||
.keySet();
|
||||
result = addAll(result, ignoredNames);
|
||||
}
|
||||
return (result != null) ? result : Collections.emptySet();
|
||||
}
|
||||
|
||||
private Map<String, BeanDefinition> getBeanDefinitionsForType(ClassLoader classLoader, boolean considerHierarchy,
|
||||
ListableBeanFactory beanFactory, String type, Set<Class<?>> parameterizedContainers) throws LinkageError {
|
||||
try {
|
||||
return getBeanDefinitionsForType(beanFactory, considerHierarchy, resolve(type, classLoader),
|
||||
parameterizedContainers);
|
||||
}
|
||||
catch (ClassNotFoundException | NoClassDefFoundError ex) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, BeanDefinition> getBeanDefinitionsForType(ListableBeanFactory beanFactory,
|
||||
boolean considerHierarchy, Class<?> type, Set<Class<?>> parameterizedContainers) {
|
||||
boolean considerHierarchy, ResolvableType type, Set<ResolvableType> parameterizedContainers) {
|
||||
Map<String, BeanDefinition> result = collectBeanDefinitionsForType(beanFactory, considerHierarchy, type,
|
||||
parameterizedContainers, null);
|
||||
return (result != null) ? result : Collections.emptyMap();
|
||||
}
|
||||
|
||||
private Map<String, BeanDefinition> collectBeanDefinitionsForType(ListableBeanFactory beanFactory,
|
||||
boolean considerHierarchy, Class<?> type, Set<Class<?>> parameterizedContainers,
|
||||
boolean considerHierarchy, ResolvableType type, Set<ResolvableType> parameterizedContainers,
|
||||
Map<String, BeanDefinition> result) {
|
||||
result = putAll(result, beanFactory.getBeanNamesForType(type, true, false), beanFactory);
|
||||
for (Class<?> container : parameterizedContainers) {
|
||||
ResolvableType generic = ResolvableType.forClassWithGenerics(container, type);
|
||||
for (ResolvableType parameterizedContainer : parameterizedContainers) {
|
||||
ResolvableType generic = ResolvableType.forClassWithGenerics(parameterizedContainer.resolve(), type);
|
||||
result = putAll(result, beanFactory.getBeanNamesForType(generic, true, false), beanFactory);
|
||||
}
|
||||
if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory) {
|
||||
@@ -550,13 +540,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
|
||||
private final Set<String> names;
|
||||
|
||||
private final Set<String> types;
|
||||
private final Set<ResolvableType> types;
|
||||
|
||||
private final Set<String> annotations;
|
||||
|
||||
private final Set<String> ignoredTypes;
|
||||
private final Set<ResolvableType> ignoredTypes;
|
||||
|
||||
private final Set<Class<?>> parameterizedContainers;
|
||||
private final Set<ResolvableType> parameterizedContainers;
|
||||
|
||||
private final SearchStrategy strategy;
|
||||
|
||||
@@ -570,10 +560,10 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
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<String> types = extractTypes(attributes);
|
||||
Set<ResolvableType> types = resolveWhenPossible(extractTypes(attributes));
|
||||
BeanTypeDeductionException deductionException = null;
|
||||
if (types.isEmpty() && this.names.isEmpty() && this.annotations.isEmpty()) {
|
||||
try {
|
||||
@@ -614,17 +604,18 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
Collections.addAll(result, additional);
|
||||
}
|
||||
|
||||
private Set<Class<?>> resolveWhenPossible(Set<String> classNames) {
|
||||
private Set<ResolvableType> resolveWhenPossible(Set<String> classNames) {
|
||||
if (classNames.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<Class<?>> resolved = new LinkedHashSet<>(classNames.size());
|
||||
Set<ResolvableType> resolved = new LinkedHashSet<>(classNames.size());
|
||||
for (String className : classNames) {
|
||||
try {
|
||||
resolved.add(resolve(className, this.context.getClassLoader()));
|
||||
Class<?> type = resolve(className, this.context.getClassLoader());
|
||||
resolved.add(ResolvableType.forRawClass(type));
|
||||
}
|
||||
catch (ClassNotFoundException | NoClassDefFoundError ex) {
|
||||
// Ignore
|
||||
resolved.add(ResolvableType.NONE);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
@@ -653,48 +644,44 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
return "@" + ClassUtils.getShortName(this.annotationType);
|
||||
}
|
||||
|
||||
private Set<String> deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
private Set<ResolvableType> deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
if (metadata instanceof MethodMetadata && metadata.isAnnotated(Bean.class.getName())) {
|
||||
return deducedBeanTypeForBeanMethod(context, (MethodMetadata) metadata);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
private Set<String> deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) {
|
||||
private Set<ResolvableType> deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) {
|
||||
try {
|
||||
Class<?> returnType = getReturnType(context, metadata);
|
||||
return Collections.singleton(returnType.getName());
|
||||
return Set.of(getReturnType(context, metadata));
|
||||
}
|
||||
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);
|
||||
if (isParameterizedContainer(returnType)) {
|
||||
returnType = getReturnTypeGeneric(metadata, classLoader);
|
||||
ResolvableType returnType = getMethodReturnType(metadata, classLoader);
|
||||
if (isParameterizedContainer(returnType.resolve())) {
|
||||
returnType = returnType.getGeneric();
|
||||
}
|
||||
return returnType;
|
||||
}
|
||||
|
||||
private boolean isParameterizedContainer(Class<?> type) {
|
||||
for (Class<?> parameterizedContainer : this.parameterizedContainers) {
|
||||
if (parameterizedContainer.isAssignableFrom(type)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return (type != null) && this.parameterizedContainers.stream()
|
||||
.map(ResolvableType::resolve)
|
||||
.anyMatch((container) -> container != null && container.isAssignableFrom(type));
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -720,6 +707,10 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
return (this.strategy != null) ? this.strategy : SearchStrategy.ALL;
|
||||
}
|
||||
|
||||
Set<ResolvableType> getTypes() {
|
||||
return this.types;
|
||||
}
|
||||
|
||||
private ConditionContext getContext() {
|
||||
return this.context;
|
||||
}
|
||||
@@ -728,19 +719,15 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
return this.names;
|
||||
}
|
||||
|
||||
protected Set<String> getTypes() {
|
||||
return this.types;
|
||||
}
|
||||
|
||||
private Set<String> getAnnotations() {
|
||||
return this.annotations;
|
||||
}
|
||||
|
||||
private Set<String> getIgnoredTypes() {
|
||||
private Set<ResolvableType> getIgnoredTypes() {
|
||||
return this.ignoredTypes;
|
||||
}
|
||||
|
||||
private Set<Class<?>> getParameterizedContainers() {
|
||||
private Set<ResolvableType> getParameterizedContainers() {
|
||||
return this.parameterizedContainers;
|
||||
}
|
||||
|
||||
@@ -847,13 +834,13 @@ class OnBeanCondition extends FilteringSpringBootCondition implements Configurat
|
||||
this.unmatchedAnnotations.add(annotation);
|
||||
}
|
||||
|
||||
private void recordMatchedType(String type, Collection<String> matchingNames) {
|
||||
this.matchedTypes.put(type, matchingNames);
|
||||
private void recordMatchedType(ResolvableType type, Collection<String> matchingNames) {
|
||||
this.matchedTypes.put(type.toString(), matchingNames);
|
||||
this.namesOfAllMatches.addAll(matchingNames);
|
||||
}
|
||||
|
||||
private void recordUnmatchedType(String type) {
|
||||
this.unmatchedTypes.add(type);
|
||||
private void recordUnmatchedType(ResolvableType type) {
|
||||
this.unmatchedTypes.add(type.toString());
|
||||
}
|
||||
|
||||
boolean isAllMatched() {
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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<ConfigurableApplicationContext> 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<String> genericStringExampleBean() {
|
||||
return new GenericExampleBean<>("genericStringExampleBean");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GenericWithStringTypeArgumentsConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean
|
||||
GenericExampleBean<String> genericStringTypeArgumentsExampleBean() {
|
||||
return new GenericExampleBean<>("genericStringTypeArgumentsExampleBean");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GenericWithIntegerConfig {
|
||||
|
||||
@Bean
|
||||
GenericExampleBean<Integer> genericIntegerExampleBean() {
|
||||
return new GenericExampleBean<>(1_000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GenericWithIntegerTypeArgumentsConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean
|
||||
GenericExampleBean<Integer> genericIntegerTypeArgumentsExampleBean() {
|
||||
return new GenericExampleBean<>(1_000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TypeArgumentsConditionWithValueConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(GenericExampleBean.class)
|
||||
GenericExampleBean<String> genericStringWithValueExampleBean() {
|
||||
return new GenericExampleBean<>("genericStringWithValueExampleBean");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TypeArgumentsConditionWithParameterizedContainerConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(parameterizedContainer = TestParameterizedContainer.class)
|
||||
TestParameterizedContainer<GenericExampleBean<String>> parameterizedContainerGenericExampleBean() {
|
||||
return new TestParameterizedContainer<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation
|
||||
static class GenericExampleBean<T> {
|
||||
|
||||
private final T value;
|
||||
|
||||
GenericExampleBean(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(this.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomExampleBean extends GenericExampleBean<String> {
|
||||
|
||||
CustomExampleBean() {
|
||||
super("custom subclass");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@interface TestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2012-2025 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<ConfigurableApplicationContext> 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<String> genericStringExampleBean() {
|
||||
return new GenericExampleBean<>("genericStringExampleBean");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GenericWithIntegerTypeArgumentsConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
GenericExampleBean<Integer> genericIntegerExampleBean() {
|
||||
return new GenericExampleBean<>(1_000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TypeArgumentsConditionWithValueConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(GenericExampleBean.class)
|
||||
GenericExampleBean<String> genericStringWithValueExampleBean() {
|
||||
return new GenericExampleBean<>("genericStringWithValueExampleBean");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TypeArgumentsConditionWithParameterizedContainerConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(parameterizedContainer = TestParameterizedContainer.class)
|
||||
TestParameterizedContainer<GenericExampleBean<String>> parameterizedContainerGenericExampleBean() {
|
||||
return new TestParameterizedContainer<>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@TestAnnotation
|
||||
static class GenericExampleBean<T> {
|
||||
|
||||
private final T value;
|
||||
|
||||
GenericExampleBean(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.valueOf(this.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomExampleBean extends GenericExampleBean<String> {
|
||||
|
||||
CustomExampleBean() {
|
||||
super("custom subclass");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@interface TestAnnotation {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user