Always invoke ConfigurationBeanNameGenerator

Previously, if a ConfigurationBeanNameGenerator is used to parse
configuration classes, it is not invoked for `Bean` methods that provide
a specific bean name. This doesn't give a chance to an implementation to
tune such a bean, if required.

This commit updates the signature of ConfigurationBeanNameGenerator to
provide the identified bean name on the `@Bean` declaration,if any, and
to always invoke it.

Closes gh-35505
This commit is contained in:
Stéphane Nicoll
2025-10-01 10:34:38 +02:00
parent 969517bce1
commit 741107969a
6 changed files with 329 additions and 26 deletions
@@ -0,0 +1,199 @@
/*
* 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 java.lang.reflect.Method;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link BeanAnnotationHelper}.
*
* @author Stephane Nicoll
*/
class BeanAnnotationHelperTests {
@BeforeEach
void clearCache() {
BeanAnnotationHelper.clearCaches();
}
@Test
void determineBeanNameWhenNoGeneratorAndNoBeanName() {
String beanName = BeanAnnotationHelper.determineBeanNameFor(
sampleMethod("beanWithoutName"), createBeanFactoryWithBeanNameGenerator(null));
assertThat(beanName).isEqualTo("beanWithoutName");
}
@ParameterizedTest
@ValueSource(strings = { "beanWithName", "beanWithMultipleNames" })
void determineBeanNameWhenNoGeneratorAndBeanName(String methodName) {
String beanName = BeanAnnotationHelper.determineBeanNameFor(
sampleMethod(methodName), createBeanFactoryWithBeanNameGenerator(null));
assertThat(beanName).isEqualTo("specificName");
}
@Test
void determineBeanNameWhenBeanNameGeneratorAndNoBeanName() {
BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class);
String beanName = BeanAnnotationHelper.determineBeanNameFor(
sampleMethod("beanWithoutName"), createBeanFactoryWithBeanNameGenerator(beanNameGenerator));
assertThat(beanName).isEqualTo("beanWithoutName");
verifyNoInteractions(beanNameGenerator);
}
@ParameterizedTest
@ValueSource(strings = { "beanWithName", "beanWithMultipleNames" })
void determineBeanNameWhenBeanNameGeneratorAndBeanName(String methodName) {
BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class);
String beanName = BeanAnnotationHelper.determineBeanNameFor(
sampleMethod(methodName), createBeanFactoryWithBeanNameGenerator(beanNameGenerator));
assertThat(beanName).isEqualTo("specificName");
verifyNoInteractions(beanNameGenerator);
}
@Test
void determineBeanNameWhenConfigurationBeanNameGeneratorAndNoBeanName() {
ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class);
when(beanNameGenerator.deriveBeanName(any(), isNull())).thenReturn("generatedBeanName");
String beanName = BeanAnnotationHelper.determineBeanNameFor(
sampleMethod("beanWithoutName"), createBeanFactoryWithBeanNameGenerator(beanNameGenerator));
assertThat(beanName).isEqualTo("generatedBeanName");
verify(beanNameGenerator).deriveBeanName(any(), isNull());
}
@ParameterizedTest
@ValueSource(strings = { "beanWithName", "beanWithMultipleNames" })
void determineBeanNameWhenConfigurationBeanNameGeneratorAndBeanName(String methodName) {
ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class);
given(beanNameGenerator.deriveBeanName(any(), eq("specificName"))).willReturn("generatedBeanName");
String beanName = BeanAnnotationHelper.determineBeanNameFor(
sampleMethod(methodName), createBeanFactoryWithBeanNameGenerator(beanNameGenerator));
assertThat(beanName).isEqualTo("generatedBeanName");
verify(beanNameGenerator).deriveBeanName(any(), eq("specificName"));
}
@Test
void determineBeanNameInCacheWhenNoGeneratorAndNoBeanName() {
Method method = sampleMethod("beanWithoutName");
ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(null);
String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory);
assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName);
}
@ParameterizedTest
@ValueSource(strings = { "beanWithName", "beanWithMultipleNames" })
void determineBeanNameInCacheWhenNoGeneratorAndBeanName(String methodName) {
Method method = sampleMethod(methodName);
ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(null);
String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory);
assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName);
}
@Test
void determineBeanNameInCacheWhenBeanNameGeneratorAndNoBeanName() {
BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class);
Method method = sampleMethod("beanWithoutName");
ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator);
String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory);
assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName);
verifyNoInteractions(beanNameGenerator);
}
@ParameterizedTest
@ValueSource(strings = { "beanWithName", "beanWithMultipleNames" })
void determineBeanNameInCacheWhenBeanNameGeneratorAndBeanName(String methodName) {
BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class);
Method method = sampleMethod(methodName);
ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator);
String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory);
assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName);
verifyNoInteractions(beanNameGenerator);
}
@Test
void determineBeanNameInCacheWhenConfigurationBeanNameGeneratorAndNoBeanName() {
ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class);
when(beanNameGenerator.deriveBeanName(any(), isNull()))
.thenReturn("generatedBeanName").thenReturn("generatedBeanName");
Method method = sampleMethod("beanWithoutName");
ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator);
String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory);
assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName);
verify(beanNameGenerator, times(2)).deriveBeanName(any(), isNull());
}
@ParameterizedTest
@ValueSource(strings = { "beanWithName", "beanWithMultipleNames" })
void determineBeanNameInCacheWhenConfigurationBeanNameGeneratorAndBeanName(String methodName) {
ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class);
given(beanNameGenerator.deriveBeanName(any(), eq("specificName")))
.willReturn("generatedBeanName").willReturn("generatedBeanName");
Method method = sampleMethod(methodName);
ConfigurableBeanFactory beanFactory = createBeanFactoryWithBeanNameGenerator(beanNameGenerator);
String beanName = BeanAnnotationHelper.determineBeanNameFor(method, beanFactory);
assertThat(BeanAnnotationHelper.determineBeanNameFor(method, beanFactory)).isEqualTo(beanName);
verify(beanNameGenerator, times(2)).deriveBeanName(any(), eq("specificName"));
}
private static Method sampleMethod(String name) {
return Objects.requireNonNull(ReflectionUtils.findMethod(Samples.class, name));
}
private static ConfigurableBeanFactory createBeanFactoryWithBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) {
ConfigurableBeanFactory beanFactory = new DefaultListableBeanFactory();
if (beanNameGenerator != null) {
beanFactory.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator);
}
return beanFactory;
}
static class Samples {
@Bean
private void beanWithoutName() {}
@Bean(name = "specificName")
private void beanWithName() {}
@Bean(name = { "specificName", "specificName2", "specificName3" })
private void beanWithMultipleNames() {}
}
}
@@ -26,6 +26,8 @@ import java.util.Map;
import jakarta.annotation.PostConstruct;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.stubbing.Answer;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.aop.interceptor.SimpleTraceInterceptor;
@@ -50,6 +52,7 @@ import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.ChildBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -65,6 +68,7 @@ import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DescriptiveResource;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.type.MethodMetadata;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -72,11 +76,20 @@ import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
/**
* Tests for {@link ConfigurationClassPostProcessor}.
*
* @author Chris Beams
* @author Juergen Hoeller
* @author Sam Brannen
* @author Stephane Nicoll
*/
class ConfigurationClassPostProcessorTests {
@@ -536,6 +549,67 @@ class ConfigurationClassPostProcessorTests {
pp.postProcessBeanFactory(bf2)); // second invocation for bf2 -- should throw
}
@Test
void beanDefinitionsFromBeanMethodWithoutBeanNameGenerator() {
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(BeanNamesConfig.class));
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
pp.postProcessBeanFactory(beanFactory);
assertThat(beanFactory.getBeanDefinitionNames())
.containsOnly("config", "beanWithoutName", "specificName", "specificNames");
assertThat(beanFactory.getBean("beanWithoutName")).isEqualTo("beanWithoutName");
assertThat(beanFactory.getBean("specificName")).isEqualTo("beanWithName");
assertThat(beanFactory.getBean("specificNames")).isEqualTo("beanWithMultipleNames");
assertThat(beanFactory.getBean("specificNames2")).isEqualTo("beanWithMultipleNames");
assertThat(beanFactory.getBean("specificNames3")).isEqualTo("beanWithMultipleNames");
}
@Test
void beanDefinitionsFromBeanMethodWithBeanNameGenerator() {
BeanNameGenerator beanNameGenerator = mock(BeanNameGenerator.class);
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(BeanNamesConfig.class));
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
pp.setBeanNameGenerator(beanNameGenerator);
pp.postProcessBeanFactory(beanFactory);
assertThat(beanFactory.getBeanDefinitionNames())
.containsOnly("config", "beanWithoutName", "specificName", "specificNames");
assertThat(beanFactory.getBean("beanWithoutName")).isEqualTo("beanWithoutName");
assertThat(beanFactory.getBean("specificName")).isEqualTo("beanWithName");
assertThat(beanFactory.getBean("specificNames")).isEqualTo("beanWithMultipleNames");
assertThat(beanFactory.getBean("specificNames2")).isEqualTo("beanWithMultipleNames");
assertThat(beanFactory.getBean("specificNames3")).isEqualTo("beanWithMultipleNames");
verifyNoInteractions(beanNameGenerator);
}
@Test
void beanDefinitionsFromBeanMethodWithConfigurationBeanNameGenerator() {
ConfigurationBeanNameGenerator beanNameGenerator = mock(ConfigurationBeanNameGenerator.class);
Answer<?> answer = invocation -> {
MethodMetadata methodMetadata = invocation.getArgument(0);
String providedBeanName = invocation.getArgument(1);
return (providedBeanName != null) ? "test.fromBean." + providedBeanName : "test." + methodMetadata.getMethodName();
};
given(beanNameGenerator.deriveBeanName(any(), any())).willAnswer(answer).willAnswer(answer).willAnswer(answer);
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(BeanNamesConfig.class));
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
pp.setBeanNameGenerator(beanNameGenerator);
pp.postProcessBeanFactory(beanFactory);
assertThat(beanFactory.getBeanDefinitionNames())
.containsOnly("config", "test.beanWithoutName", "test.fromBean.specificName", "test.fromBean.specificNames");
assertThat(beanFactory.getBean("test.beanWithoutName")).isEqualTo("beanWithoutName");
assertThat(beanFactory.getBean("test.fromBean.specificName")).isEqualTo("beanWithName");
assertThat(beanFactory.getBean("test.fromBean.specificNames")).isEqualTo("beanWithMultipleNames");
assertThat(beanFactory.getBean("specificNames2")).isEqualTo("beanWithMultipleNames");
assertThat(beanFactory.getBean("specificNames3")).isEqualTo("beanWithMultipleNames");
ArgumentCaptor<MethodMetadata> methodMetadataCaptor = ArgumentCaptor.forClass(MethodMetadata.class);
ArgumentCaptor<String> beanNameCaptor = ArgumentCaptor.forClass(String.class);
verify(beanNameGenerator, times(3)).deriveBeanName(methodMetadataCaptor.capture(), beanNameCaptor.capture());
List<MethodMetadata> beansMethodMetadata = methodMetadataCaptor.getAllValues();
assertThat(beansMethodMetadata).map(MethodMetadata::getMethodName)
.containsExactly("beanWithoutName", "beanWithName", "beanWithMultipleNames");
List<String> beanNames = beanNameCaptor.getAllValues();
assertThat(beanNames).containsExactly(null, "specificName", "specificNames");
}
@Test
void genericsBasedInjection() {
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
@@ -1384,6 +1458,26 @@ class ConfigurationClassPostProcessorTests {
}
}
@Configuration(proxyBeanMethods = false)
public static class BeanNamesConfig {
@Bean
public String beanWithoutName() {
return "beanWithoutName";
}
@Bean(name = "specificName")
public String beanWithName() {
return "beanWithName";
}
@Bean(name = { "specificNames", "specificNames2", "specificNames3" })
public String beanWithMultipleNames() {
return "beanWithMultipleNames";
}
}
public interface RepositoryInterface<T> {
@Override