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 5a9faf928e..957bbead74 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 @@ -37,6 +37,7 @@ import org.jspecify.annotations.Nullable; 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.util.Assert; @@ -280,6 +281,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 10ac3533fa..a393aa9114 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 @@ -1479,6 +1479,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) { @@ -1939,7 +1942,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(); @@ -2097,6 +2105,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/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index d066e9d750..08f25aa40c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -366,8 +366,18 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements } afterSingletonCreation(beanName); } + if (newSingleton) { - addSingleton(beanName, singletonObject); + try { + addSingleton(beanName, singletonObject); + } + catch (IllegalStateException ex) { + // Leniently accept same instance if implicitly appeared. + Object object = this.singletonObjects.get(beanName); + if (singletonObject != object) { + throw ex; + } + } } } return singletonObject; 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 d3305a035e..61cc8d6d23 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() { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java index 34bb56b9fa..4e86a072cd 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -50,10 +50,15 @@ class DefaultSingletonBeanRegistryTests { assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); assertThat(tb2Flag.get()).isTrue(); - assertThat(beanRegistry.getSingleton("tb")).isSameAs(tb); - assertThat(beanRegistry.getSingleton("tb2")).isSameAs(tb2); - assertThat(beanRegistry.getSingletonCount()).isEqualTo(2); - assertThat(beanRegistry.getSingletonNames()).containsExactly("tb", "tb2"); + TestBean tb3 = (TestBean) beanRegistry.getSingleton("tb3", () -> { + TestBean newTb = new TestBean(); + beanRegistry.registerSingleton("tb3", newTb); + return newTb; + }); + assertThat(beanRegistry.getSingleton("tb3")).isSameAs(tb3); + + assertThat(beanRegistry.getSingletonCount()).isEqualTo(3); + assertThat(beanRegistry.getSingletonNames()).containsExactly("tb", "tb2", "tb3"); beanRegistry.destroySingletons(); assertThat(beanRegistry.getSingletonCount()).isZero(); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java index 5bd91dd481..3516eff33b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ExecutorBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ package org.springframework.scheduling.config; import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; @@ -53,6 +54,7 @@ public class ExecutorBeanDefinitionParser extends AbstractSingleBeanDefinitionPa if (StringUtils.hasText(poolSize)) { builder.addPropertyValue("poolSize", poolSize); } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } private void configureRejectionPolicy(Element element, BeanDefinitionBuilder builder) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java index a9429e4901..4eb9e1c143 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/SchedulerBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2025 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. @@ -18,6 +18,7 @@ package org.springframework.scheduling.config; import org.w3c.dom.Element; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; @@ -41,6 +42,7 @@ public class SchedulerBeanDefinitionParser extends AbstractSingleBeanDefinitionP if (StringUtils.hasText(poolSize)) { builder.addPropertyValue("poolSize", poolSize); } + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); } }