Skip to content

Commit 1145054

Browse files
committed
Introduce ConfigurationBeanNameGenerator for @Bean-annotated methods
Includes FullyQualifiedConfigurationBeanNameGenerator implementation. Closes gh-33448
1 parent b48f65a commit 1145054

File tree

10 files changed

+237
-43
lines changed

10 files changed

+237
-43
lines changed

spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
import java.lang.reflect.Method;
2020
import java.util.Map;
2121

22+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2223
import org.springframework.core.annotation.AnnotatedElementUtils;
2324
import org.springframework.core.annotation.AnnotationAttributes;
25+
import org.springframework.core.type.MethodMetadata;
2426
import org.springframework.util.ConcurrentReferenceHashMap;
2527

2628
/**
@@ -41,11 +43,26 @@ public static boolean isBeanAnnotated(Method method) {
4143
return AnnotatedElementUtils.hasAnnotation(method, Bean.class);
4244
}
4345

46+
public static String determineBeanNameFor(Method beanMethod, ConfigurableBeanFactory beanFactory) {
47+
String beanName = retrieveBeanNameFor(beanMethod);
48+
if (!beanName.isEmpty()) {
49+
return beanName;
50+
}
51+
return (beanFactory.getSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR)
52+
instanceof ConfigurationBeanNameGenerator cbng ?
53+
cbng.deriveBeanName(MethodMetadata.introspect(beanMethod)) : beanMethod.getName());
54+
}
55+
4456
public static String determineBeanNameFor(Method beanMethod) {
57+
String beanName = retrieveBeanNameFor(beanMethod);
58+
return (!beanName.isEmpty() ? beanName : beanMethod.getName());
59+
}
60+
61+
private static String retrieveBeanNameFor(Method beanMethod) {
4562
String beanName = beanNameCache.get(beanMethod);
4663
if (beanName == null) {
47-
// By default, the bean name is the name of the @Bean-annotated method
48-
beanName = beanMethod.getName();
64+
// By default, the bean name is empty (indicating a name to be derived from the method name)
65+
beanName = "";
4966
// Check to see if the user has explicitly set a custom bean name...
5067
AnnotationAttributes bean =
5168
AnnotatedElementUtils.findMergedAnnotationAttributes(beanMethod, Bean.class, false, false);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2002-present 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.context.annotation;
18+
19+
import org.springframework.beans.factory.support.BeanNameGenerator;
20+
import org.springframework.core.type.MethodMetadata;
21+
22+
/**
23+
* Extended variant of {@link BeanNameGenerator} for
24+
* {@link Configuration @Configuration} class purposes, not only covering
25+
* bean name generation for component and configuration classes themselves
26+
* but also for {@link Bean @Bean} methods without a {@link Bean#name() name}
27+
* attribute specified on the annotation itself.
28+
*
29+
* @author Juergen Hoeller
30+
* @since 7.0
31+
* @see AnnotationConfigApplicationContext#setBeanNameGenerator
32+
* @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
33+
*/
34+
public interface ConfigurationBeanNameGenerator extends BeanNameGenerator {
35+
36+
/**
37+
* Derive a default bean name for the given {@link Bean @Bean} method,
38+
* in the absence of a {@link Bean#name() name} attribute specified.
39+
* @param beanMethod the method metadata for the {@link Bean @Bean} method
40+
* @return the default bean name to use
41+
*/
42+
String deriveBeanName(MethodMetadata beanMethod);
43+
44+
}

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@
1818

1919
import java.lang.reflect.Constructor;
2020
import java.lang.reflect.Method;
21-
import java.util.ArrayList;
22-
import java.util.Arrays;
2321
import java.util.HashMap;
24-
import java.util.List;
2522
import java.util.Map;
2623
import java.util.Set;
2724

@@ -199,16 +196,30 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
199196
Assert.state(bean != null, "No @Bean annotation attributes");
200197

201198
// Consider name and any aliases
202-
List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
203-
String beanName = (!names.isEmpty() ? names.remove(0) : methodName);
204-
205-
// Register aliases even when overridden
206-
for (String alias : names) {
207-
this.registry.registerAlias(beanName, alias);
199+
String[] explicitNames = bean.getStringArray("name");
200+
String beanName;
201+
String localBeanName;
202+
if (explicitNames.length > 0 && StringUtils.hasText(explicitNames[0])) {
203+
beanName = explicitNames[0];
204+
localBeanName = beanName;
205+
// Register aliases even when overridden below
206+
for (int i = 1; i < explicitNames.length; i++) {
207+
this.registry.registerAlias(beanName, explicitNames[i]);
208+
}
209+
}
210+
else {
211+
// Default bean name derived from method name.
212+
beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ?
213+
cbng.deriveBeanName(metadata) : methodName);
214+
localBeanName = methodName;
208215
}
209216

217+
ConfigurationClassBeanDefinition beanDef =
218+
new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName);
219+
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
220+
210221
// Has this effectively been overridden before (for example, via XML)?
211-
if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
222+
if (isOverriddenByExistingDefinition(beanMethod, beanName, beanDef)) {
212223
if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {
213224
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
214225
beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +
@@ -217,9 +228,6 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
217228
return;
218229
}
219230

220-
ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName);
221-
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
222-
223231
if (metadata.isStatic()) {
224232
// static @Bean method
225233
if (configClass.getMetadata() instanceof StandardAnnotationMetadata sam) {
@@ -288,7 +296,7 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
288296
new BeanDefinitionHolder(beanDef, beanName), this.registry,
289297
proxyMode == ScopedProxyMode.TARGET_CLASS);
290298
beanDefToRegister = new ConfigurationClassBeanDefinition(
291-
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, beanName);
299+
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, localBeanName);
292300
}
293301

294302
if (logger.isTraceEnabled()) {
@@ -299,7 +307,9 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
299307
}
300308

301309
@SuppressWarnings("NullAway") // Reflection
302-
protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) {
310+
private boolean isOverriddenByExistingDefinition(
311+
BeanMethod beanMethod, String beanName, ConfigurationClassBeanDefinition newBeanDef) {
312+
303313
if (!this.registry.containsBeanDefinition(beanName)) {
304314
return false;
305315
}
@@ -320,9 +330,7 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String
320330
configClass.getMetadata().getAnnotationAttributes(Configuration.class.getName());
321331
if ((attributes != null && (Boolean) attributes.get("enforceUniqueMethods")) ||
322332
!this.registry.isBeanDefinitionOverridable(beanName)) {
323-
throw new BeanDefinitionOverrideException(beanName,
324-
new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName),
325-
existingBeanDef,
333+
throw new BeanDefinitionOverrideException(beanName, newBeanDef, existingBeanDef,
326334
"@Bean method override with same bean name but different method name: " + existingBeanDef);
327335
}
328336
return true;
@@ -400,17 +408,20 @@ private void loadBeanDefinitionsFromImportedResources(
400408
});
401409
}
402410

403-
private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
411+
private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars(
412+
Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
413+
404414
registrars.forEach((registrar, metadata) ->
405415
registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
406416
}
407417

408418
private void loadBeanDefinitionsFromBeanRegistrars(Map<String, BeanRegistrar> registrars) {
409-
Assert.isInstanceOf(ListableBeanFactory.class, this.registry,
410-
"Cannot support bean registrars since " + this.registry.getClass().getName() +
411-
" does not implement BeanDefinitionRegistry");
412-
registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter(this.registry,
413-
(ListableBeanFactory) this.registry, this.environment, registrar.getClass()), this.environment));
419+
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
420+
throw new IllegalStateException("Cannot support bean registrars since " +
421+
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
422+
}
423+
registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter(
424+
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment));
414425
}
415426

416427

@@ -427,32 +438,32 @@ private static class ConfigurationClassBeanDefinition extends RootBeanDefinition
427438

428439
private final MethodMetadata factoryMethodMetadata;
429440

430-
private final String derivedBeanName;
441+
private final String localBeanName;
431442

432443
public ConfigurationClassBeanDefinition(
433-
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) {
444+
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String localBeanName) {
434445

435446
this.annotationMetadata = configClass.getMetadata();
436447
this.factoryMethodMetadata = beanMethodMetadata;
437-
this.derivedBeanName = derivedBeanName;
448+
this.localBeanName = localBeanName;
438449
setResource(configClass.getResource());
439450
setLenientConstructorResolution(false);
440451
}
441452

442453
public ConfigurationClassBeanDefinition(RootBeanDefinition original,
443-
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) {
454+
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String localBeanName) {
444455

445456
super(original);
446457
this.annotationMetadata = configClass.getMetadata();
447458
this.factoryMethodMetadata = beanMethodMetadata;
448-
this.derivedBeanName = derivedBeanName;
459+
this.localBeanName = localBeanName;
449460
}
450461

451462
private ConfigurationClassBeanDefinition(ConfigurationClassBeanDefinition original) {
452463
super(original);
453464
this.annotationMetadata = original.annotationMetadata;
454465
this.factoryMethodMetadata = original.factoryMethodMetadata;
455-
this.derivedBeanName = original.derivedBeanName;
466+
this.localBeanName = original.localBeanName;
456467
}
457468

458469
@Override
@@ -468,7 +479,7 @@ public MethodMetadata getFactoryMethodMetadata() {
468479
@Override
469480
public boolean isFactoryMethod(Method candidate) {
470481
return (super.isFactoryMethod(candidate) && BeanAnnotationHelper.isBeanAnnotated(candidate) &&
471-
BeanAnnotationHelper.determineBeanNameFor(candidate).equals(this.derivedBeanName));
482+
BeanAnnotationHelper.determineBeanNameFor(candidate).equals(this.localBeanName));
472483
}
473484

474485
@Override

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ private static class BeanMethodInterceptor implements MethodInterceptor, Conditi
352352
MethodProxy cglibMethodProxy) throws Throwable {
353353

354354
ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
355-
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
355+
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod, beanFactory);
356356

357357
// Determine whether this bean is a scoped-proxy
358358
if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
@@ -455,7 +455,7 @@ private static class BeanMethodInterceptor implements MethodInterceptor, Conditi
455455
}
456456
Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
457457
if (currentlyInvoked != null) {
458-
String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked);
458+
String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked, beanFactory);
459459
beanFactory.registerDependentBean(beanName, outerBeanName);
460460
}
461461
return beanInstance;

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,20 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.
408408
SingletonBeanRegistry singletonRegistry = null;
409409
if (registry instanceof SingletonBeanRegistry sbr) {
410410
singletonRegistry = sbr;
411-
if (!this.localBeanNameGeneratorSet) {
412-
BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton(
413-
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
414-
if (generator != null) {
415-
this.componentScanBeanNameGenerator = generator;
416-
this.importBeanNameGenerator = generator;
411+
BeanNameGenerator configurationGenerator = (BeanNameGenerator) singletonRegistry.getSingleton(
412+
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
413+
if (configurationGenerator != null) {
414+
if (this.localBeanNameGeneratorSet) {
415+
if (configurationGenerator instanceof ConfigurationBeanNameGenerator &
416+
configurationGenerator != this.importBeanNameGenerator) {
417+
throw new IllegalStateException("Context-level ConfigurationBeanNameGenerator [" +
418+
configurationGenerator + "] must not be overridden with processor-level generator [" +
419+
this.importBeanNameGenerator + "]");
420+
}
421+
}
422+
else {
423+
this.componentScanBeanNameGenerator = configurationGenerator;
424+
this.importBeanNameGenerator = configurationGenerator;
417425
}
418426
}
419427
}

spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedAnnotationBeanNameGenerator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
* <p>Favor this bean naming strategy over {@code AnnotationBeanNameGenerator} if
2929
* you run into naming conflicts due to multiple autodetected components having the
3030
* same non-qualified class name (i.e., classes with identical names but residing in
31-
* different packages).
31+
* different packages). If you need such conflict avoidance for {@link Bean @Bean}
32+
* methods as well, consider {@link FullyQualifiedConfigurationBeanNameGenerator}.
3233
*
3334
* <p>Note that an instance of this class is used by default for configuration-level
3435
* import purposes; whereas, the default for component scanning purposes is a plain
@@ -39,6 +40,7 @@
3940
* @since 5.2.3
4041
* @see org.springframework.beans.factory.support.DefaultBeanNameGenerator
4142
* @see AnnotationBeanNameGenerator
43+
* @see FullyQualifiedConfigurationBeanNameGenerator
4244
* @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR
4345
*/
4446
public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-present 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.context.annotation;
18+
19+
import org.springframework.core.type.MethodMetadata;
20+
21+
/**
22+
* Extended variant of {@link FullyQualifiedAnnotationBeanNameGenerator} for
23+
* {@link Configuration @Configuration} class purposes, not only enforcing
24+
* fully-qualified names for component and configuration classes themselves
25+
* but also fully-qualified default bean names ("className.methodName") for
26+
* {@link Bean @Bean} methods. This only affects methods without an explicit
27+
* {@link Bean#name() name} attribute specified.
28+
*
29+
* <p>This provides an alternative to the default bean name generation for
30+
* {@code @Bean} methods (which uses the plain method name), primarily for use
31+
* in large applications with potential bean name overlaps. Favor this bean
32+
* naming strategy over {@code FullyQualifiedAnnotationBeanNameGenerator} if
33+
* you expect such naming conflicts for {@code @Bean} methods, as long as the
34+
* application does not depend on {@code @Bean} method names as bean names.
35+
* Where the name does matter, make sure to declare {@code @Bean("myBeanName")}
36+
* in such a scenario, even if it repeats the method name as the bean name.
37+
*
38+
* @author Juergen Hoeller
39+
* @since 7.0
40+
* @see AnnotationBeanNameGenerator
41+
* @see FullyQualifiedAnnotationBeanNameGenerator
42+
* @see AnnotationConfigApplicationContext#setBeanNameGenerator
43+
* @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
44+
*/
45+
public class FullyQualifiedConfigurationBeanNameGenerator extends FullyQualifiedAnnotationBeanNameGenerator
46+
implements ConfigurationBeanNameGenerator {
47+
48+
/**
49+
* A convenient constant for a default {@code FullyQualifiedConfigurationBeanNameGenerator}
50+
* instance, as used for configuration-level import purposes.
51+
*/
52+
public static final FullyQualifiedConfigurationBeanNameGenerator INSTANCE =
53+
new FullyQualifiedConfigurationBeanNameGenerator();
54+
55+
56+
@Override
57+
public String deriveBeanName(MethodMetadata beanMethod) {
58+
return beanMethod.getDeclaringClassName() + "." + beanMethod.getMethodName();
59+
}
60+
61+
}

0 commit comments

Comments
 (0)