diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 27d1265071c..32db0408cb0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -331,6 +331,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, return beanNames; } + /** + * Determine the primary candidate in the given set of bean names. + *

Honors both primary and fallback semantics. + * @return the name of the primary candidate, or {@code null} if none found + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determinePrimaryCandidate(Map, Class) + */ @Nullable private static String determinePrimaryCandidate( ConfigurableListableBeanFactory beanFactory, Set candidateBeanNames, Class beanType) { @@ -340,6 +346,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, } String primaryBeanName = null; + // First pass: identify unique primary candidate for (String candidateBeanName : candidateBeanNames) { if (beanFactory.containsBeanDefinition(candidateBeanName)) { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName); @@ -352,6 +359,21 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, } } } + // Second pass: identify unique non-fallback candidate + if (primaryBeanName == null) { + for (String candidateBeanName : candidateBeanNames) { + if (beanFactory.containsBeanDefinition(candidateBeanName)) { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName); + if (!beanDefinition.isFallback()) { + if (primaryBeanName != null) { + // More than one non-fallback bean found among candidates. + return null; + } + primaryBeanName = candidateBeanName; + } + } + } + } return primaryBeanName; } diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests.java new file mode 100644 index 00000000000..037ea0d5291 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.convention; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Fallback; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that {@link TestBean @TestBean} can be used to override a bean by-type + * when there are multiple candidates and only one that is not a fallback. + * + * @author Sam Brannen + * @since 6.2.1 + */ +@ExtendWith(SpringExtension.class) +@DirtiesContext +class TestBeanWithMultipleExistingBeansAndOneNonFallbackIntegrationTests { + + @TestBean + ExampleService service; + + @Autowired + List services; + + + static ExampleService service() { + return () -> "overridden"; + } + + + @Test + void test() { + assertThat(service.greeting()).isEqualTo("overridden"); + assertThat(services).extracting(ExampleService::greeting) + .containsExactlyInAnyOrder("overridden", "two", "three"); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + ExampleService one() { + return () -> "one"; + } + + @Bean + @Fallback + ExampleService two() { + return () -> "two"; + } + + @Bean + @Fallback + ExampleService three() { + return () -> "three"; + } + } + +}