Browse Source

Introduce ConfigurationBeanNameGenerator for @Bean-annotated methods

Includes FullyQualifiedConfigurationBeanNameGenerator implementation.

Closes gh-33448
pull/35196/head
Juergen Hoeller 5 months ago
parent
commit
1145054971
  1. 21
      spring-context/src/main/java/org/springframework/context/annotation/BeanAnnotationHelper.java
  2. 44
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationBeanNameGenerator.java
  3. 73
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java
  4. 4
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassEnhancer.java
  5. 20
      spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java
  6. 4
      spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedAnnotationBeanNameGenerator.java
  7. 61
      spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.java
  8. 34
      spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java
  9. 14
      spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java
  10. 5
      spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java

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

@ -19,8 +19,10 @@ package org.springframework.context.annotation; @@ -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 { @@ -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);

44
spring-context/src/main/java/org/springframework/context/annotation/ConfigurationBeanNameGenerator.java

@ -0,0 +1,44 @@ @@ -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);
}

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

@ -18,10 +18,7 @@ package org.springframework.context.annotation; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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

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

@ -352,7 +352,7 @@ class ConfigurationClassEnhancer { @@ -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 { @@ -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;

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

@ -408,12 +408,20 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo @@ -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;
}
}
}

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

@ -28,7 +28,8 @@ import org.springframework.util.Assert; @@ -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; @@ -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 {

61
spring-context/src/main/java/org/springframework/context/annotation/FullyQualifiedConfigurationBeanNameGenerator.java

@ -0,0 +1,61 @@ @@ -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();
}
}

34
spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java

@ -67,6 +67,21 @@ class AnnotationConfigApplicationContextTests { @@ -67,6 +67,21 @@ class AnnotationConfigApplicationContextTests {
assertThat(beans).hasSize(1);
}
@Test
void scanAndRefreshWithFullyQualifiedBeanNames() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.setBeanNameGenerator(FullyQualifiedConfigurationBeanNameGenerator.INSTANCE);
context.scan("org.springframework.context.annotation6");
context.refresh();
context.getBean(ConfigForScanning.class.getName());
context.getBean(ConfigForScanning.class.getName() + ".testBean"); // contributed by ConfigForScanning
context.getBean(ComponentForScanning.class.getName());
context.getBean(Jsr330NamedForScanning.class.getName());
Map<String, Object> beans = context.getBeansWithAnnotation(Configuration.class);
assertThat(beans).hasSize(1);
}
@Test
void registerAndRefresh() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@ -74,7 +89,22 @@ class AnnotationConfigApplicationContextTests { @@ -74,7 +89,22 @@ class AnnotationConfigApplicationContextTests {
context.refresh();
context.getBean("testBean");
context.getBean("name");
assertThat(context.getBean("name")).isEqualTo("foo");
assertThat(context.getBean("prefixName")).isEqualTo("barfoo");
Map<String, Object> beans = context.getBeansWithAnnotation(Configuration.class);
assertThat(beans).hasSize(2);
}
@Test
void registerAndRefreshWithFullyQualifiedBeanNames() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.setBeanNameGenerator(FullyQualifiedConfigurationBeanNameGenerator.INSTANCE);
context.register(Config.class, NameConfig.class);
context.refresh();
context.getBean(Config.class.getName() + ".testBean");
assertThat(context.getBean(NameConfig.class.getName() + ".name")).isEqualTo("foo");
assertThat(context.getBean(NameConfig.class.getName() + ".prefixName")).isEqualTo("barfoo");
Map<String, Object> beans = context.getBeansWithAnnotation(Configuration.class);
assertThat(beans).hasSize(2);
}
@ -598,6 +628,8 @@ class AnnotationConfigApplicationContextTests { @@ -598,6 +628,8 @@ class AnnotationConfigApplicationContextTests {
static class NameConfig {
@Bean String name() { return "foo"; }
@Bean(autowireCandidate = false) String prefixName() { return "bar" + name(); }
}
@Configuration

14
spring-core/src/main/java/org/springframework/core/type/MethodMetadata.java

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.core.type;
import java.lang.reflect.Method;
/**
* Interface that defines abstract access to the annotations of a specific
* method, in a form that does not require that method's class to be loaded yet.
@ -71,4 +73,16 @@ public interface MethodMetadata extends AnnotatedTypeMetadata { @@ -71,4 +73,16 @@ public interface MethodMetadata extends AnnotatedTypeMetadata {
*/
boolean isOverridable();
/**
* Factory method to create a new {@link MethodMetadata} instance
* for the given method using standard reflection.
* @param method the method to introspect
* @return a new {@link MethodMetadata} instance
* @since 7.0
*/
static MethodMetadata introspect(Method method) {
return StandardMethodMetadata.from(method);
}
}

5
spring-core/src/main/java/org/springframework/core/type/StandardMethodMetadata.java

@ -166,4 +166,9 @@ public class StandardMethodMetadata implements MethodMetadata { @@ -166,4 +166,9 @@ public class StandardMethodMetadata implements MethodMetadata {
return this.introspectedMethod.toString();
}
static MethodMetadata from(Method introspectedMethod) {
return new StandardMethodMetadata(introspectedMethod, true);
}
}

Loading…
Cancel
Save