Skip to content

Commit 26c775e

Browse files
philwebbsnicollwilkinsonanosan
committed
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 <[email protected]> Co-authored-by: Andy Wilkinson <[email protected]> Co-authored-by: Dmytro Nosan <[email protected]>
1 parent a705402 commit 26c775e

File tree

8 files changed

+207
-26
lines changed

8 files changed

+207
-26
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/AutoConfigurations.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ protected AutoConfigurations(Collection<Class<?>> classes) {
5151
}
5252

5353
AutoConfigurations(UnaryOperator<String> replacementMapper, Collection<Class<?>> classes) {
54-
super(sorter(replacementMapper), classes);
54+
super(sorter(replacementMapper), classes, Class::getName);
5555
this.replacementMapper = replacementMapper;
5656
}
5757

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/AutoConfigurationsTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ void whenHasReplacementForClassShouldReplaceClass() {
5454
AutoConfigureA.class);
5555
}
5656

57+
@Test
58+
void getBeanNameShouldUseClassName() {
59+
Configurations configurations = AutoConfigurations.of(AutoConfigureA.class, AutoConfigureB.class);
60+
assertThat(configurations.getBeanName(AutoConfigureA.class)).isEqualTo(AutoConfigureA.class.getName());
61+
}
62+
5763
private String replaceB(String className) {
5864
return (!AutoConfigureB.class.getName().equals(className)) ? className : AutoConfigureB2.class.getName();
5965
}

spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.function.BiConsumer;
2223
import java.util.function.Consumer;
2324
import java.util.function.Function;
2425
import java.util.function.Supplier;
@@ -27,6 +28,7 @@
2728
import org.springframework.beans.factory.config.BeanDefinitionCustomizer;
2829
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
2930
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
31+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3032
import org.springframework.beans.factory.support.BeanNameGenerator;
3133
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3234
import org.springframework.boot.context.annotation.Configurations;
@@ -38,12 +40,14 @@
3840
import org.springframework.context.ApplicationContext;
3941
import org.springframework.context.ApplicationContextInitializer;
4042
import org.springframework.context.ConfigurableApplicationContext;
43+
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
4144
import org.springframework.context.annotation.AnnotationConfigRegistry;
4245
import org.springframework.context.support.GenericApplicationContext;
4346
import org.springframework.core.ResolvableType;
4447
import org.springframework.core.env.Environment;
4548
import org.springframework.core.io.DefaultResourceLoader;
4649
import org.springframework.util.Assert;
50+
import org.springframework.util.CollectionUtils;
4751

4852
/**
4953
* Utility design to run an {@link ApplicationContext} and provide AssertJ style
@@ -439,15 +443,27 @@ private void configureContext(C context, boolean refresh) {
439443
this.runnerConfiguration.environmentProperties.applyTo(context);
440444
this.runnerConfiguration.beanRegistrations.forEach((registration) -> registration.apply(context));
441445
this.runnerConfiguration.initializers.forEach((initializer) -> initializer.initialize(context));
442-
Class<?>[] classes = Configurations.getClasses(this.runnerConfiguration.configurations);
443-
if (classes.length > 0) {
444-
((AnnotationConfigRegistry) context).register(classes);
446+
if (!CollectionUtils.isEmpty(this.runnerConfiguration.configurations)) {
447+
BiConsumer<Class<?>, String> registrar = getRegistrar(context);
448+
for (Configurations configurations : Configurations.collate(this.runnerConfiguration.configurations)) {
449+
for (Class<?> beanClass : Configurations.getClasses(configurations)) {
450+
String beanName = configurations.getBeanName(beanClass);
451+
registrar.accept(beanClass, beanName);
452+
}
453+
}
445454
}
446455
if (refresh) {
447456
context.refresh();
448457
}
449458
}
450459

460+
private BiConsumer<Class<?>, String> getRegistrar(C context) {
461+
if (context instanceof BeanDefinitionRegistry registry) {
462+
return new AnnotatedBeanDefinitionReader(registry, context.getEnvironment())::registerBean;
463+
}
464+
return (beanClass, beanName) -> ((AnnotationConfigRegistry) context).register(beanClass);
465+
}
466+
451467
private void accept(ContextConsumer<? super A> consumer, A context) {
452468
try {
453469
consumer.accept(context);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.context.example.duplicate.first;
18+
19+
import org.springframework.context.annotation.Configuration;
20+
21+
/**
22+
* Example configuration to showcase handing of duplicate class names.
23+
*
24+
* @author Stephane Nicoll
25+
*/
26+
@Configuration
27+
public class EmptyConfig {
28+
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.context.example.duplicate.second;
18+
19+
import org.springframework.context.annotation.Configuration;
20+
21+
/**
22+
* Example configuration to showcase handing of duplicate class names.
23+
*
24+
* @author Stephane Nicoll
25+
*/
26+
@Configuration
27+
public class EmptyConfig {
28+
29+
}

spring-boot-project/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
package org.springframework.boot.test.context.runner;
1818

1919
import java.io.IOException;
20+
import java.util.Collection;
21+
import java.util.List;
22+
import java.util.Set;
2023
import java.util.UUID;
2124
import java.util.concurrent.atomic.AtomicBoolean;
2225

@@ -27,6 +30,8 @@
2730
import org.springframework.beans.factory.BeanDefinitionStoreException;
2831
import org.springframework.beans.factory.ObjectProvider;
2932
import org.springframework.beans.factory.annotation.Autowired;
33+
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
34+
import org.springframework.boot.context.annotation.Configurations;
3035
import org.springframework.boot.context.annotation.UserConfigurations;
3136
import org.springframework.boot.context.properties.ConfigurationProperties;
3237
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -139,6 +144,38 @@ void runWithConfigurationsShouldRegisterConfigurations() {
139144
get().withUserConfiguration(FooConfig.class).run((context) -> assertThat(context).hasBean("foo"));
140145
}
141146

147+
@Test
148+
void runWithUserConfigurationsRegistersDefaultBeanName() {
149+
get().withUserConfiguration(FooConfig.class)
150+
.run((context) -> assertThat(context).hasBean("abstractApplicationContextRunnerTests.FooConfig"));
151+
}
152+
153+
@Test
154+
void runWithUserConfigurationsWhenHasSameShortClassNamedRegistersWithoutBeanName() {
155+
get()
156+
.withUserConfiguration(org.springframework.boot.test.context.example.duplicate.first.EmptyConfig.class,
157+
org.springframework.boot.test.context.example.duplicate.second.EmptyConfig.class)
158+
.run((context) -> assertThat(context.getStartupFailure())
159+
.isInstanceOf(BeanDefinitionOverrideException.class));
160+
}
161+
162+
@Test
163+
void runFullyQualifiedNameConfigurationsRegistersFullyQualifiedBeanName() {
164+
get().withConfiguration(FullyQualifiedNameConfigurations.of(FooConfig.class))
165+
.run((context) -> assertThat(context).hasBean(FooConfig.class.getName()));
166+
}
167+
168+
@Test
169+
void runWithFullyQualifiedNameConfigurationsWhenHasSameShortClassNamedRegistersWithFullyQualifiedBeanName() {
170+
get()
171+
.withConfiguration(FullyQualifiedNameConfigurations.of(
172+
org.springframework.boot.test.context.example.duplicate.first.EmptyConfig.class,
173+
org.springframework.boot.test.context.example.duplicate.second.EmptyConfig.class))
174+
.run((context) -> assertThat(context)
175+
.hasSingleBean(org.springframework.boot.test.context.example.duplicate.first.EmptyConfig.class)
176+
.hasSingleBean(org.springframework.boot.test.context.example.duplicate.second.EmptyConfig.class));
177+
}
178+
142179
@Test
143180
void runWithUserNamedBeanShouldRegisterBean() {
144181
get().withBean("foo", String.class, () -> "foo").run((context) -> assertThat(context).hasBean("foo"));
@@ -384,4 +421,21 @@ static class ProfileConfig {
384421

385422
}
386423

424+
static class FullyQualifiedNameConfigurations extends Configurations {
425+
426+
protected FullyQualifiedNameConfigurations(Collection<Class<?>> classes) {
427+
super(null, classes, Class::getName);
428+
}
429+
430+
@Override
431+
protected Configurations merge(Set<Class<?>> mergedClasses) {
432+
return new FullyQualifiedNameConfigurations(mergedClasses);
433+
}
434+
435+
static FullyQualifiedNameConfigurations of(Class<?>... classes) {
436+
return new FullyQualifiedNameConfigurations(List.of(classes));
437+
}
438+
439+
}
440+
387441
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/annotation/Configurations.java

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.LinkedList;
2626
import java.util.List;
2727
import java.util.Set;
28+
import java.util.function.Function;
2829
import java.util.function.UnaryOperator;
2930
import java.util.stream.Collectors;
3031
import java.util.stream.Stream;
@@ -65,6 +66,8 @@ public abstract class Configurations {
6566

6667
private final Set<Class<?>> classes;
6768

69+
private final Function<Class<?>, String> beanNameGenerator;
70+
6871
/**
6972
* Create a new {@link Configurations} instance.
7073
* @param classes the configuration classes
@@ -74,38 +77,42 @@ protected Configurations(Collection<Class<?>> classes) {
7477
Collection<Class<?>> sorted = sort(classes);
7578
this.sorter = null;
7679
this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
80+
this.beanNameGenerator = null;
7781
}
7882

7983
/**
8084
* Create a new {@link Configurations} instance.
8185
* @param sorter a {@link UnaryOperator} used to sort the configurations
8286
* @param classes the configuration classes
87+
* @param beanNameGenerator an optional function used to generate the bean name
8388
* @since 3.4.0
8489
*/
85-
protected Configurations(UnaryOperator<Collection<Class<?>>> sorter, Collection<Class<?>> classes) {
86-
Assert.notNull(sorter, "Sorter must not be null");
90+
protected Configurations(UnaryOperator<Collection<Class<?>>> sorter, Collection<Class<?>> classes,
91+
Function<Class<?>, String> beanNameGenerator) {
8792
Assert.notNull(classes, "Classes must not be null");
93+
sorter = (sorter != null) ? sorter : UnaryOperator.identity();
8894
Collection<Class<?>> sorted = sorter.apply(classes);
89-
this.sorter = sorter;
95+
this.sorter = (sorter != null) ? sorter : UnaryOperator.identity();
9096
this.classes = Collections.unmodifiableSet(new LinkedHashSet<>(sorted));
97+
this.beanNameGenerator = beanNameGenerator;
98+
}
99+
100+
protected final Set<Class<?>> getClasses() {
101+
return this.classes;
91102
}
92103

93104
/**
94105
* Sort configuration classes into the order that they should be applied.
95106
* @param classes the classes to sort
96107
* @return a sorted set of classes
97108
* @deprecated since 3.4.0 for removal in 3.6.0 in favor of
98-
* {@link #Configurations(UnaryOperator, Collection)}
109+
* {@link #Configurations(UnaryOperator, Collection, Function)}
99110
*/
100111
@Deprecated(since = "3.4.0", forRemoval = true)
101112
protected Collection<Class<?>> sort(Collection<Class<?>> classes) {
102113
return classes;
103114
}
104115

105-
protected final Set<Class<?>> getClasses() {
106-
return this.classes;
107-
}
108-
109116
/**
110117
* Merge configurations from another source of the same type.
111118
* @param other the other {@link Configurations} (must be of the same type as this
@@ -128,6 +135,17 @@ protected Configurations merge(Configurations other) {
128135
*/
129136
protected abstract Configurations merge(Set<Class<?>> mergedClasses);
130137

138+
/**
139+
* Return the bean name that should be used for the given configuration class or
140+
* {@code null} to use the default name.
141+
* @param beanClass the bean class
142+
* @return the bean name
143+
* @since 3.4.0
144+
*/
145+
public String getBeanName(Class<?> beanClass) {
146+
return (this.beanNameGenerator != null) ? this.beanNameGenerator.apply(beanClass) : null;
147+
}
148+
131149
/**
132150
* Return the classes from all the specified configurations in the order that they
133151
* would be registered.
@@ -145,30 +163,40 @@ public static Class<?>[] getClasses(Configurations... configurations) {
145163
* @return configuration classes in registration order
146164
*/
147165
public static Class<?>[] getClasses(Collection<Configurations> configurations) {
148-
List<Configurations> ordered = new ArrayList<>(configurations);
149-
ordered.sort(COMPARATOR);
150-
List<Configurations> collated = collate(ordered);
166+
List<Configurations> collated = collate(configurations);
151167
LinkedHashSet<Class<?>> classes = collated.stream()
152168
.flatMap(Configurations::streamClasses)
153169
.collect(Collectors.toCollection(LinkedHashSet::new));
154170
return ClassUtils.toClassArray(classes);
155171
}
156172

157-
private static Stream<Class<?>> streamClasses(Configurations configurations) {
158-
return configurations.getClasses().stream();
159-
}
160-
161-
private static List<Configurations> collate(List<Configurations> orderedConfigurations) {
173+
/**
174+
* Collate the given configuration by sorting and merging them.
175+
* @param configurations the source configuration
176+
* @return the collated configurations
177+
* @since 3.4.0
178+
*/
179+
public static List<Configurations> collate(Collection<Configurations> configurations) {
162180
LinkedList<Configurations> collated = new LinkedList<>();
163-
for (Configurations item : orderedConfigurations) {
164-
if (collated.isEmpty() || collated.getLast().getClass() != item.getClass()) {
165-
collated.add(item);
181+
for (Configurations configuration : sortConfigurations(configurations)) {
182+
if (collated.isEmpty() || collated.getLast().getClass() != configuration.getClass()) {
183+
collated.add(configuration);
166184
}
167185
else {
168-
collated.set(collated.size() - 1, collated.getLast().merge(item));
186+
collated.set(collated.size() - 1, collated.getLast().merge(configuration));
169187
}
170188
}
171189
return collated;
172190
}
173191

192+
private static List<Configurations> sortConfigurations(Collection<Configurations> configurations) {
193+
List<Configurations> sorted = new ArrayList<>(configurations);
194+
sorted.sort(COMPARATOR);
195+
return sorted;
196+
}
197+
198+
private static Stream<Class<?>> streamClasses(Configurations configurations) {
199+
return configurations.getClasses().stream();
200+
}
201+
174202
}

0 commit comments

Comments
 (0)