Register `AutoConfigurations` using fully qualified class name
Update `AbstractApplicationContextRunner` and `Configurations` to allow registration of beans with a specific generated bean name. By default, no name is generated, however, `AutoConfigurations` has been updated to use bean names using the fully qualified class name. The update brings `ApplicationContextRunners` closer the behavior of a standard Spring Boot application where user `@Configuration` classes are usually registered with a simple name and auto-configurations are imported (via an `ImportSelector`) using a fully qualified name. Fixes gh-17963 Co-authored-by: Stéphane Nicoll <stephane.nicoll@broadcom.com> Co-authored-by: Andy Wilkinson <andy.wilkinson@broadcom.com> Co-authored-by: Dmytro Nosan <dimanosan@gmail.com>
This commit is contained in:
parent
a705402e75
commit
26c775eff8
|
|
@ -51,7 +51,7 @@ public class AutoConfigurations extends Configurations implements Ordered {
|
|||
}
|
||||
|
||||
AutoConfigurations(UnaryOperator<String> replacementMapper, Collection<Class<?>> classes) {
|
||||
super(sorter(replacementMapper), classes);
|
||||
super(sorter(replacementMapper), classes, Class::getName);
|
||||
this.replacementMapper = replacementMapper;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ class AutoConfigurationsTests {
|
|||
AutoConfigureA.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanNameShouldUseClassName() {
|
||||
Configurations configurations = AutoConfigurations.of(AutoConfigureA.class, AutoConfigureB.class);
|
||||
assertThat(configurations.getBeanName(AutoConfigureA.class)).isEqualTo(AutoConfigureA.class.getName());
|
||||
}
|
||||
|
||||
private String replaceB(String className) {
|
||||
return (!AutoConfigureB.class.getName().equals(className)) ? className : AutoConfigureB2.class.getName();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.springframework.boot.test.context.runner;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
|
@ -27,6 +28,7 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
|||
import org.springframework.beans.factory.config.BeanDefinitionCustomizer;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.context.annotation.Configurations;
|
||||
|
|
@ -38,12 +40,14 @@ import org.springframework.boot.test.util.TestPropertyValues;
|
|||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
|
||||
import org.springframework.context.annotation.AnnotationConfigRegistry;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* Utility design to run an {@link ApplicationContext} and provide AssertJ style
|
||||
|
|
@ -439,15 +443,27 @@ public abstract class AbstractApplicationContextRunner<SELF extends AbstractAppl
|
|||
this.runnerConfiguration.environmentProperties.applyTo(context);
|
||||
this.runnerConfiguration.beanRegistrations.forEach((registration) -> registration.apply(context));
|
||||
this.runnerConfiguration.initializers.forEach((initializer) -> initializer.initialize(context));
|
||||
Class<?>[] classes = Configurations.getClasses(this.runnerConfiguration.configurations);
|
||||
if (classes.length > 0) {
|
||||
((AnnotationConfigRegistry) context).register(classes);
|
||||
if (!CollectionUtils.isEmpty(this.runnerConfiguration.configurations)) {
|
||||
BiConsumer<Class<?>, String> registrar = getRegistrar(context);
|
||||
for (Configurations configurations : Configurations.collate(this.runnerConfiguration.configurations)) {
|
||||
for (Class<?> beanClass : Configurations.getClasses(configurations)) {
|
||||
String beanName = configurations.getBeanName(beanClass);
|
||||
registrar.accept(beanClass, beanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (refresh) {
|
||||
context.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private BiConsumer<Class<?>, String> getRegistrar(C context) {
|
||||
if (context instanceof BeanDefinitionRegistry registry) {
|
||||
return new AnnotatedBeanDefinitionReader(registry, context.getEnvironment())::registerBean;
|
||||
}
|
||||
return (beanClass, beanName) -> ((AnnotationConfigRegistry) context).register(beanClass);
|
||||
}
|
||||
|
||||
private void accept(ContextConsumer<? super A> consumer, A context) {
|
||||
try {
|
||||
consumer.accept(context);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2012-2024 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.test.context.example.duplicate.first;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Example configuration to showcase handing of duplicate class names.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@Configuration
|
||||
public class EmptyConfig {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2012-2024 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.test.context.example.duplicate.second;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Example configuration to showcase handing of duplicate class names.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@Configuration
|
||||
public class EmptyConfig {
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,9 @@
|
|||
package org.springframework.boot.test.context.runner;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
|
|
@ -27,6 +30,8 @@ import org.springframework.beans.factory.BeanCurrentlyInCreationException;
|
|||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
|
||||
import org.springframework.boot.context.annotation.Configurations;
|
||||
import org.springframework.boot.context.annotation.UserConfigurations;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
|
|
@ -139,6 +144,38 @@ abstract class AbstractApplicationContextRunnerTests<T extends AbstractApplicati
|
|||
get().withUserConfiguration(FooConfig.class).run((context) -> assertThat(context).hasBean("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithUserConfigurationsRegistersDefaultBeanName() {
|
||||
get().withUserConfiguration(FooConfig.class)
|
||||
.run((context) -> assertThat(context).hasBean("abstractApplicationContextRunnerTests.FooConfig"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithUserConfigurationsWhenHasSameShortClassNamedRegistersWithoutBeanName() {
|
||||
get()
|
||||
.withUserConfiguration(org.springframework.boot.test.context.example.duplicate.first.EmptyConfig.class,
|
||||
org.springframework.boot.test.context.example.duplicate.second.EmptyConfig.class)
|
||||
.run((context) -> assertThat(context.getStartupFailure())
|
||||
.isInstanceOf(BeanDefinitionOverrideException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runFullyQualifiedNameConfigurationsRegistersFullyQualifiedBeanName() {
|
||||
get().withConfiguration(FullyQualifiedNameConfigurations.of(FooConfig.class))
|
||||
.run((context) -> assertThat(context).hasBean(FooConfig.class.getName()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithFullyQualifiedNameConfigurationsWhenHasSameShortClassNamedRegistersWithFullyQualifiedBeanName() {
|
||||
get()
|
||||
.withConfiguration(FullyQualifiedNameConfigurations.of(
|
||||
org.springframework.boot.test.context.example.duplicate.first.EmptyConfig.class,
|
||||
org.springframework.boot.test.context.example.duplicate.second.EmptyConfig.class))
|
||||
.run((context) -> assertThat(context)
|
||||
.hasSingleBean(org.springframework.boot.test.context.example.duplicate.first.EmptyConfig.class)
|
||||
.hasSingleBean(org.springframework.boot.test.context.example.duplicate.second.EmptyConfig.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runWithUserNamedBeanShouldRegisterBean() {
|
||||
get().withBean("foo", String.class, () -> "foo").run((context) -> assertThat(context).hasBean("foo"));
|
||||
|
|
@ -384,4 +421,21 @@ abstract class AbstractApplicationContextRunnerTests<T extends AbstractApplicati
|
|||
|
||||
}
|
||||
|
||||
static class FullyQualifiedNameConfigurations extends Configurations {
|
||||
|
||||
protected FullyQualifiedNameConfigurations(Collection<Class<?>> classes) {
|
||||
super(null, classes, Class::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Configurations merge(Set<Class<?>> mergedClasses) {
|
||||
return new FullyQualifiedNameConfigurations(mergedClasses);
|
||||
}
|
||||
|
||||
static FullyQualifiedNameConfigurations of(Class<?>... classes) {
|
||||
return new FullyQualifiedNameConfigurations(List.of(classes));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import java.util.LinkedHashSet;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
|
@ -65,6 +66,8 @@ public abstract class Configurations {
|
|||
|
||||
private final Set<Class<?>> classes;
|
||||
|
||||
private final Function<Class<?>, String> beanNameGenerator;
|
||||
|
||||
/**
|
||||
* Create a new {@link Configurations} instance.
|
||||
* @param classes the configuration classes
|
||||
|
|
@ -74,20 +77,28 @@ public abstract class Configurations {
|
|||
Collection<Class<?>> sorted = sort(classes);
|
||||
this.sorter = null;
|
||||
this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
|
||||
this.beanNameGenerator = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Configurations} instance.
|
||||
* @param sorter a {@link UnaryOperator} used to sort the configurations
|
||||
* @param classes the configuration classes
|
||||
* @param beanNameGenerator an optional function used to generate the bean name
|
||||
* @since 3.4.0
|
||||
*/
|
||||
protected Configurations(UnaryOperator<Collection<Class<?>>> sorter, Collection<Class<?>> classes) {
|
||||
Assert.notNull(sorter, "Sorter must not be null");
|
||||
protected Configurations(UnaryOperator<Collection<Class<?>>> sorter, Collection<Class<?>> classes,
|
||||
Function<Class<?>, String> beanNameGenerator) {
|
||||
Assert.notNull(classes, "Classes must not be null");
|
||||
sorter = (sorter != null) ? sorter : UnaryOperator.identity();
|
||||
Collection<Class<?>> sorted = sorter.apply(classes);
|
||||
this.sorter = sorter;
|
||||
this.sorter = (sorter != null) ? sorter : UnaryOperator.identity();
|
||||
this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
|
||||
this.beanNameGenerator = beanNameGenerator;
|
||||
}
|
||||
|
||||
protected final Set<Class<?>> getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -95,17 +106,13 @@ public abstract class Configurations {
|
|||
* @param classes the classes to sort
|
||||
* @return a sorted set of classes
|
||||
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
|
||||
* {@link #Configurations(UnaryOperator, Collection)}
|
||||
* {@link #Configurations(UnaryOperator, Collection, Function)}
|
||||
*/
|
||||
@Deprecated(since = "3.4.0", forRemoval = true)
|
||||
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
|
||||
return classes;
|
||||
}
|
||||
|
||||
protected final Set<Class<?>> getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge configurations from another source of the same type.
|
||||
* @param other the other {@link Configurations} (must be of the same type as this
|
||||
|
|
@ -128,6 +135,17 @@ public abstract class Configurations {
|
|||
*/
|
||||
protected abstract Configurations merge(Set<Class<?>> mergedClasses);
|
||||
|
||||
/**
|
||||
* Return the bean name that should be used for the given configuration class or
|
||||
* {@code null} to use the default name.
|
||||
* @param beanClass the bean class
|
||||
* @return the bean name
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public String getBeanName(Class<?> beanClass) {
|
||||
return (this.beanNameGenerator != null) ? this.beanNameGenerator.apply(beanClass) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the classes from all the specified configurations in the order that they
|
||||
* would be registered.
|
||||
|
|
@ -145,30 +163,40 @@ public abstract class Configurations {
|
|||
* @return configuration classes in registration order
|
||||
*/
|
||||
public static Class<?>[] getClasses(Collection<Configurations> configurations) {
|
||||
List<Configurations> ordered = new ArrayList<>(configurations);
|
||||
ordered.sort(COMPARATOR);
|
||||
List<Configurations> collated = collate(ordered);
|
||||
List<Configurations> collated = collate(configurations);
|
||||
LinkedHashSet<Class<?>> classes = collated.stream()
|
||||
.flatMap(Configurations::streamClasses)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
return ClassUtils.toClassArray(classes);
|
||||
}
|
||||
|
||||
private static Stream<Class<?>> streamClasses(Configurations configurations) {
|
||||
return configurations.getClasses().stream();
|
||||
}
|
||||
|
||||
private static List<Configurations> collate(List<Configurations> orderedConfigurations) {
|
||||
/**
|
||||
* Collate the given configuration by sorting and merging them.
|
||||
* @param configurations the source configuration
|
||||
* @return the collated configurations
|
||||
* @since 3.4.0
|
||||
*/
|
||||
public static List<Configurations> collate(Collection<Configurations> configurations) {
|
||||
LinkedList<Configurations> collated = new LinkedList<>();
|
||||
for (Configurations item : orderedConfigurations) {
|
||||
if (collated.isEmpty() || collated.getLast().getClass() != item.getClass()) {
|
||||
collated.add(item);
|
||||
for (Configurations configuration : sortConfigurations(configurations)) {
|
||||
if (collated.isEmpty() || collated.getLast().getClass() != configuration.getClass()) {
|
||||
collated.add(configuration);
|
||||
}
|
||||
else {
|
||||
collated.set(collated.size() - 1, collated.getLast().merge(item));
|
||||
collated.set(collated.size() - 1, collated.getLast().merge(configuration));
|
||||
}
|
||||
}
|
||||
return collated;
|
||||
}
|
||||
|
||||
private static List<Configurations> sortConfigurations(Collection<Configurations> configurations) {
|
||||
List<Configurations> sorted = new ArrayList<>(configurations);
|
||||
sorted.sort(COMPARATOR);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static Stream<Class<?>> streamClasses(Configurations configurations) {
|
||||
return configurations.getClasses().stream();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ import java.util.Arrays;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -87,6 +89,18 @@ class ConfigurationsTests {
|
|||
OutputStream.class, String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanNameWhenNoFunctionReturnsNull() {
|
||||
Configurations configurations = new TestConfigurations(Short.class);
|
||||
assertThat(configurations.getBeanName(Short.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanNameWhenFunctionReturnsBeanName() {
|
||||
Configurations configurations = new TestConfigurations(Sorter.instance, List.of(Short.class), Class::getName);
|
||||
assertThat(configurations.getBeanName(Short.class)).isEqualTo(Short.class.getName());
|
||||
}
|
||||
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
static class TestConfigurations extends Configurations {
|
||||
|
||||
|
|
@ -95,7 +109,12 @@ class ConfigurationsTests {
|
|||
}
|
||||
|
||||
TestConfigurations(UnaryOperator<Collection<Class<?>>> sorter, Class<?>... classes) {
|
||||
super(sorter, Arrays.asList(classes));
|
||||
this(sorter, Arrays.asList(classes), null);
|
||||
}
|
||||
|
||||
TestConfigurations(UnaryOperator<Collection<Class<?>>> sorter, Collection<Class<?>> classes,
|
||||
Function<Class<?>, String> beanNameGenerator) {
|
||||
super(sorter, classes, beanNameGenerator);
|
||||
}
|
||||
|
||||
TestConfigurations(Collection<Class<?>> classes) {
|
||||
|
|
@ -117,7 +136,7 @@ class ConfigurationsTests {
|
|||
}
|
||||
|
||||
protected TestSortedConfigurations(Collection<Class<?>> classes) {
|
||||
super(Sorter.instance, classes);
|
||||
super(Sorter.instance, classes, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Reference in New Issue