diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java index 246735bc41..d23cda6e32 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java @@ -35,6 +35,7 @@ import java.util.Set; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.lang.Nullable; @@ -279,6 +280,25 @@ abstract class AutowireUtils { } } + /** + * Check the default-candidate status for the specified bean. + * @param beanFactory the bean factory + * @param beanName the name of the bean to check + * @return whether the specified bean qualifies as a default candidate + * @since 6.2.4 + * @see AbstractBeanDefinition#isDefaultCandidate() + */ + public static boolean isDefaultCandidate(ConfigurableBeanFactory beanFactory, String beanName) { + try { + BeanDefinition mbd = beanFactory.getMergedBeanDefinition(beanName); + return (!(mbd instanceof AbstractBeanDefinition abd) || abd.isDefaultCandidate()); + } + catch (NoSuchBeanDefinitionException ex) { + // A manually registered singleton instance not backed by a BeanDefinition. + return true; + } + } + /** * Reflective {@link InvocationHandler} for lazy access to the current target object. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 54df257fe6..57c1d1a38f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -1497,6 +1497,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto if (candidateName == null) { candidateName = determineHighestPriorityCandidate(candidates, requiredType.toClass()); } + if (candidateName == null) { + candidateName = determineDefaultCandidate(candidates); + } if (candidateName != null) { Object beanInstance = candidates.get(candidateName); if (beanInstance == null) { @@ -1967,7 +1970,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto if (priorityCandidate != null) { return priorityCandidate; } - // Step 4: pick directly registered dependency + // Step 4: pick unique default-candidate + String defaultCandidate = determineDefaultCandidate(candidates); + if (defaultCandidate != null) { + return defaultCandidate; + } + // Step 5: pick directly registered dependency for (Map.Entry entry : candidates.entrySet()) { String candidateName = entry.getKey(); Object beanInstance = entry.getValue(); @@ -2128,6 +2136,28 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto return null; } + /** + * Return a unique "default-candidate" among remaining non-default candidates. + * @param candidates a Map of candidate names and candidate instances + * (or candidate classes if not created yet) that match the required type + * @return the name of the default candidate, or {@code null} if none found + * @since 6.2.4 + * @see AbstractBeanDefinition#isDefaultCandidate() + */ + @Nullable + private String determineDefaultCandidate(Map candidates) { + String defaultBeanName = null; + for (String candidateBeanName : candidates.keySet()) { + if (AutowireUtils.isDefaultCandidate(this, candidateBeanName)) { + if (defaultBeanName != null) { + return null; + } + defaultBeanName = candidateBeanName; + } + } + return defaultBeanName; + } + /** * Determine whether the given candidate name matches the bean name or the aliases * stored in this bean definition. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java index a758c5c48c..244f67a53c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java @@ -1658,12 +1658,40 @@ class DefaultListableBeanFactoryTests { bd2.setPrimary(true); lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); + lbf.registerSingleton("bd3", new TestBean()); TestBean bean = lbf.getBean(TestBean.class); assertThat(bean.getBeanName()).isEqualTo("bd2"); assertThat(lbf.containsSingleton("bd1")).isFalse(); } + @Test + void getBeanByTypeWithUniqueNonDefaultDefinition() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setDefaultCandidate(false); + bd1.setLazyInit(true); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerBeanDefinition("bd2", bd2); + + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isEqualTo("bd2"); + assertThat(lbf.containsSingleton("bd1")).isFalse(); + } + + @Test + void getBeanByTypeWithUniqueNonDefaultSingleton() { + RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); + bd1.setDefaultCandidate(false); + bd1.setLazyInit(true); + lbf.registerBeanDefinition("bd1", bd1); + lbf.registerSingleton("bd2", new TestBean()); + + TestBean bean = lbf.getBean(TestBean.class); + assertThat(bean.getBeanName()).isNull(); + assertThat(lbf.containsSingleton("bd1")).isFalse(); + } + @Test @SuppressWarnings("rawtypes") void getFactoryBeanByTypeWithPrimary() {