Introduce ConfigurationBeanNameGenerator for @Bean-annotated methods

Includes FullyQualifiedConfigurationBeanNameGenerator implementation.

Closes gh-33448
This commit is contained in:
Juergen Hoeller
2025-07-13 16:08:05 +02:00
parent b48f65aed6
commit 1145054971
10 changed files with 237 additions and 43 deletions
@@ -19,8 +19,10 @@ package org.springframework.context.annotation;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.MethodMetadata;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
@@ -41,11 +43,26 @@ abstract class BeanAnnotationHelper {
return AnnotatedElementUtils.hasAnnotation(method, Bean.class);
}
public static String determineBeanNameFor(Method beanMethod, ConfigurableBeanFactory beanFactory) {
String beanName = retrieveBeanNameFor(beanMethod);
if (!beanName.isEmpty()) {
return beanName;
}
return (beanFactory.getSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR)
instanceof ConfigurationBeanNameGenerator cbng ?
cbng.deriveBeanName(MethodMetadata.introspect(beanMethod)) : beanMethod.getName());
}
public static String determineBeanNameFor(Method beanMethod) {
String beanName = retrieveBeanNameFor(beanMethod);
return (!beanName.isEmpty() ? beanName : beanMethod.getName());
}
private static String retrieveBeanNameFor(Method beanMethod) {
String beanName = beanNameCache.get(beanMethod);
if (beanName == null) {
// By default, the bean name is the name of the @Bean-annotated method
beanName = beanMethod.getName();
// By default, the bean name is empty (indicating a name to be derived from the method name)
beanName = "";
// Check to see if the user has explicitly set a custom bean name...
AnnotationAttributes bean =
AnnotatedElementUtils.findMergedAnnotationAttributes(beanMethod, Bean.class, false, false);
@@ -0,0 +1,44 @@
/*
* Copyright 2002-present 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.context.annotation;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.type.MethodMetadata;
/**
* Extended variant of {@link BeanNameGenerator} for
* {@link Configuration @Configuration} class purposes, not only covering
* bean name generation for component and configuration classes themselves
* but also for {@link Bean @Bean} methods without a {@link Bean#name() name}
* attribute specified on the annotation itself.
*
* @author Juergen Hoeller
* @since 7.0
* @see AnnotationConfigApplicationContext#setBeanNameGenerator
* @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
*/
public interface ConfigurationBeanNameGenerator extends BeanNameGenerator {
/**
* Derive a default bean name for the given {@link Bean @Bean} method,
* in the absence of a {@link Bean#name() name} attribute specified.
* @param beanMethod the method metadata for the {@link Bean @Bean} method
* @return the default bean name to use
*/
String deriveBeanName(MethodMetadata beanMethod);
}
@@ -18,10 +18,7 @@ package org.springframework.context.annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -199,16 +196,30 @@ class ConfigurationClassBeanDefinitionReader {
Assert.state(bean != null, "No @Bean annotation attributes");
// Consider name and any aliases
List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
String beanName = (!names.isEmpty() ? names.remove(0) : methodName);
// Register aliases even when overridden
for (String alias : names) {
this.registry.registerAlias(beanName, alias);
String[] explicitNames = bean.getStringArray("name");
String beanName;
String localBeanName;
if (explicitNames.length > 0 && StringUtils.hasText(explicitNames[0])) {
beanName = explicitNames[0];
localBeanName = beanName;
// Register aliases even when overridden below
for (int i = 1; i < explicitNames.length; i++) {
this.registry.registerAlias(beanName, explicitNames[i]);
}
}
else {
// Default bean name derived from method name.
beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ?
cbng.deriveBeanName(metadata) : methodName);
localBeanName = methodName;
}
ConfigurationClassBeanDefinition beanDef =
new ConfigurationClassBeanDefinition(configClass, metadata, localBeanName);
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
// Has this effectively been overridden before (for example, via XML)?
if (isOverriddenByExistingDefinition(beanMethod, beanName)) {
if (isOverriddenByExistingDefinition(beanMethod, beanName, beanDef)) {
if (beanName.equals(beanMethod.getConfigurationClass().getBeanName())) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "Bean name derived from @Bean method '" + beanMethod.getMetadata().getMethodName() +
@@ -217,9 +228,6 @@ class ConfigurationClassBeanDefinitionReader {
return;
}
ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName);
beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
if (metadata.isStatic()) {
// static @Bean method
if (configClass.getMetadata() instanceof StandardAnnotationMetadata sam) {
@@ -288,7 +296,7 @@ class ConfigurationClassBeanDefinitionReader {
new BeanDefinitionHolder(beanDef, beanName), this.registry,
proxyMode == ScopedProxyMode.TARGET_CLASS);
beanDefToRegister = new ConfigurationClassBeanDefinition(
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, beanName);
(RootBeanDefinition) proxyDef.getBeanDefinition(), configClass, metadata, localBeanName);
}
if (logger.isTraceEnabled()) {
@@ -299,7 +307,9 @@ class ConfigurationClassBeanDefinitionReader {
}
@SuppressWarnings("NullAway") // Reflection
protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) {
private boolean isOverriddenByExistingDefinition(
BeanMethod beanMethod, String beanName, ConfigurationClassBeanDefinition newBeanDef) {
if (!this.registry.containsBeanDefinition(beanName)) {
return false;
}
@@ -320,9 +330,7 @@ class ConfigurationClassBeanDefinitionReader {
configClass.getMetadata().getAnnotationAttributes(Configuration.class.getName());
if ((attributes != null && (Boolean) attributes.get("enforceUniqueMethods")) ||
!this.registry.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionOverrideException(beanName,
new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName),
existingBeanDef,
throw new BeanDefinitionOverrideException(beanName, newBeanDef, existingBeanDef,
"@Bean method override with same bean name but different method name: " + existingBeanDef);
}
return true;
@@ -400,17 +408,20 @@ class ConfigurationClassBeanDefinitionReader {
});
}
private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars(
Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
registrars.forEach((registrar, metadata) ->
registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
}
private void loadBeanDefinitionsFromBeanRegistrars(Map<String, BeanRegistrar> registrars) {
Assert.isInstanceOf(ListableBeanFactory.class, this.registry,
"Cannot support bean registrars since " + this.registry.getClass().getName() +
" does not implement BeanDefinitionRegistry");
registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter(this.registry,
(ListableBeanFactory) this.registry, this.environment, registrar.getClass()), this.environment));
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
throw new IllegalStateException("Cannot support bean registrars since " +
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
}
registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter(
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment));
}
@@ -427,32 +438,32 @@ class ConfigurationClassBeanDefinitionReader {
private final MethodMetadata factoryMethodMetadata;
private final String derivedBeanName;
private final String localBeanName;
public ConfigurationClassBeanDefinition(
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) {
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String localBeanName) {
this.annotationMetadata = configClass.getMetadata();
this.factoryMethodMetadata = beanMethodMetadata;
this.derivedBeanName = derivedBeanName;
this.localBeanName = localBeanName;
setResource(configClass.getResource());
setLenientConstructorResolution(false);
}
public ConfigurationClassBeanDefinition(RootBeanDefinition original,
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String derivedBeanName) {
ConfigurationClass configClass, MethodMetadata beanMethodMetadata, String localBeanName) {
super(original);
this.annotationMetadata = configClass.getMetadata();
this.factoryMethodMetadata = beanMethodMetadata;
this.derivedBeanName = derivedBeanName;
this.localBeanName = localBeanName;
}
private ConfigurationClassBeanDefinition(ConfigurationClassBeanDefinition original) {
super(original);
this.annotationMetadata = original.annotationMetadata;
this.factoryMethodMetadata = original.factoryMethodMetadata;
this.derivedBeanName = original.derivedBeanName;
this.localBeanName = original.localBeanName;
}
@Override
@@ -468,7 +479,7 @@ class ConfigurationClassBeanDefinitionReader {
@Override
public boolean isFactoryMethod(Method candidate) {
return (super.isFactoryMethod(candidate) && BeanAnnotationHelper.isBeanAnnotated(candidate) &&
BeanAnnotationHelper.determineBeanNameFor(candidate).equals(this.derivedBeanName));
BeanAnnotationHelper.determineBeanNameFor(candidate).equals(this.localBeanName));
}
@Override
@@ -352,7 +352,7 @@ class ConfigurationClassEnhancer {
MethodProxy cglibMethodProxy) throws Throwable {
ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod, beanFactory);
// Determine whether this bean is a scoped-proxy
if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
@@ -455,7 +455,7 @@ class ConfigurationClassEnhancer {
}
Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod();
if (currentlyInvoked != null) {
String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked);
String outerBeanName = BeanAnnotationHelper.determineBeanNameFor(currentlyInvoked, beanFactory);
beanFactory.registerDependentBean(beanName, outerBeanName);
}
return beanInstance;
@@ -408,12 +408,20 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
SingletonBeanRegistry singletonRegistry = null;
if (registry instanceof SingletonBeanRegistry sbr) {
singletonRegistry = sbr;
if (!this.localBeanNameGeneratorSet) {
BeanNameGenerator generator = (BeanNameGenerator) singletonRegistry.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (generator != null) {
this.componentScanBeanNameGenerator = generator;
this.importBeanNameGenerator = generator;
BeanNameGenerator configurationGenerator = (BeanNameGenerator) singletonRegistry.getSingleton(
AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
if (configurationGenerator != null) {
if (this.localBeanNameGeneratorSet) {
if (configurationGenerator instanceof ConfigurationBeanNameGenerator &
configurationGenerator != this.importBeanNameGenerator) {
throw new IllegalStateException("Context-level ConfigurationBeanNameGenerator [" +
configurationGenerator + "] must not be overridden with processor-level generator [" +
this.importBeanNameGenerator + "]");
}
}
else {
this.componentScanBeanNameGenerator = configurationGenerator;
this.importBeanNameGenerator = configurationGenerator;
}
}
}
@@ -28,7 +28,8 @@ import org.springframework.util.Assert;
* <p>Favor this bean naming strategy over {@code AnnotationBeanNameGenerator} if
* you run into naming conflicts due to multiple autodetected components having the
* same non-qualified class name (i.e., classes with identical names but residing in
* different packages).
* different packages). If you need such conflict avoidance for {@link Bean @Bean}
* methods as well, consider {@link FullyQualifiedConfigurationBeanNameGenerator}.
*
* <p>Note that an instance of this class is used by default for configuration-level
* import purposes; whereas, the default for component scanning purposes is a plain
@@ -39,6 +40,7 @@ import org.springframework.util.Assert;
* @since 5.2.3
* @see org.springframework.beans.factory.support.DefaultBeanNameGenerator
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedConfigurationBeanNameGenerator
* @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR
*/
public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
@@ -0,0 +1,61 @@
/*
* Copyright 2002-present 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.context.annotation;
import org.springframework.core.type.MethodMetadata;
/**
* Extended variant of {@link FullyQualifiedAnnotationBeanNameGenerator} for
* {@link Configuration @Configuration} class purposes, not only enforcing
* fully-qualified names for component and configuration classes themselves
* but also fully-qualified default bean names ("className.methodName") for
* {@link Bean @Bean} methods. This only affects methods without an explicit
* {@link Bean#name() name} attribute specified.
*
* <p>This provides an alternative to the default bean name generation for
* {@code @Bean} methods (which uses the plain method name), primarily for use
* in large applications with potential bean name overlaps. Favor this bean
* naming strategy over {@code FullyQualifiedAnnotationBeanNameGenerator} if
* you expect such naming conflicts for {@code @Bean} methods, as long as the
* application does not depend on {@code @Bean} method names as bean names.
* Where the name does matter, make sure to declare {@code @Bean("myBeanName")}
* in such a scenario, even if it repeats the method name as the bean name.
*
* @author Juergen Hoeller
* @since 7.0
* @see AnnotationBeanNameGenerator
* @see FullyQualifiedAnnotationBeanNameGenerator
* @see AnnotationConfigApplicationContext#setBeanNameGenerator
* @see AnnotationConfigUtils#CONFIGURATION_BEAN_NAME_GENERATOR
*/
public class FullyQualifiedConfigurationBeanNameGenerator extends FullyQualifiedAnnotationBeanNameGenerator
implements ConfigurationBeanNameGenerator {
/**
* A convenient constant for a default {@code FullyQualifiedConfigurationBeanNameGenerator}
* instance, as used for configuration-level import purposes.
*/
public static final FullyQualifiedConfigurationBeanNameGenerator INSTANCE =
new FullyQualifiedConfigurationBeanNameGenerator();
@Override
public String deriveBeanName(MethodMetadata beanMethod) {
return beanMethod.getDeclaringClassName() + "." + beanMethod.getMethodName();
}
}