diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index dac7b7c381..f065ddc346 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -1942,6 +1942,10 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac @Override @Nullable public Constructor>[] getPreferredConstructors() { + Constructor>[] fromAttribute = super.getPreferredConstructors(); + if (fromAttribute != null) { + return fromAttribute; + } return ConstructorResolver.determinePreferredConstructors(getBeanClass()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java index a81613d3bd..d8b4f3a41a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinition.java @@ -125,6 +125,20 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess */ public static final int DEPENDENCY_CHECK_ALL = 3; + /** + * The name of an attribute that can be + * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a + * {@link org.springframework.beans.factory.config.BeanDefinition} so that + * bean definitions can indicate one or more preferred constructors. This is + * analogous to {@code @Autowired} annotated constructors on the bean class. + *
The attribute value may be a single {@link java.lang.reflect.Constructor} + * reference or an array thereof. + * @since 6.1 + * @see org.springframework.beans.factory.annotation.Autowired + * @see org.springframework.beans.factory.support.RootBeanDefinition#getPreferredConstructors() + */ + public static final String PREFERRED_CONSTRUCTORS_ATTRIBUTE = "preferredConstructors"; + /** * Constant that indicates the container should attempt to infer the * {@link #setDestroyMethodName destroy method name} for a bean as opposed to diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java index 0d1faf1f55..de460275b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java @@ -1698,13 +1698,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp */ ResolvableType getTypeForFactoryBeanFromAttributes(AttributeAccessor attributes) { Object attribute = attributes.getAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE); + if (attribute == null) { + return ResolvableType.NONE; + } if (attribute instanceof ResolvableType resolvableType) { return resolvableType; } if (attribute instanceof Class> clazz) { return ResolvableType.forClass(clazz); } - return ResolvableType.NONE; + throw new IllegalArgumentException("Invalid value type for attribute '" + + FactoryBean.OBJECT_TYPE_ATTRIBUTE + "': " + attribute.getClass()); } /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index b88f1aa14f..4fd9635823 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -384,13 +384,28 @@ public class RootBeanDefinition extends AbstractBeanDefinition { /** * Determine preferred constructors to use for default construction, if any. * Constructor arguments will be autowired if necessary. + *
As of 6.1, the default implementation of this method takes the + * {@link #PREFERRED_CONSTRUCTORS_ATTRIBUTE} attribute into account. + * Subclasses are encouraged to preserve this through a {@code super} call, + * either before or after their own preferred constructor determination. * @return one or more preferred constructors, or {@code null} if none * (in which case the regular no-arg default constructor will be called) * @since 5.1 */ @Nullable public Constructor>[] getPreferredConstructors() { - return null; + Object attribute = getAttribute(PREFERRED_CONSTRUCTORS_ATTRIBUTE); + if (attribute == null) { + return null; + } + if (attribute instanceof Constructor> constructor) { + return new Constructor>[] {constructor}; + } + if (attribute instanceof Constructor>[]) { + return (Constructor>[]) attribute; + } + throw new IllegalArgumentException("Invalid value type for attribute '" + + PREFERRED_CONSTRUCTORS_ATTRIBUTE + "': " + attribute.getClass()); } /** 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 b9ed5deebe..7f7ab56685 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 @@ -65,6 +65,7 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionOverrideException; import org.springframework.beans.factory.support.ChildBeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ConstructorDependenciesBean; @@ -1370,10 +1371,10 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("rod2", bd2); lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) - .withMessageContaining("rod") - .withMessageContaining("rod2"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)) + .withMessageContaining("rod") + .withMessageContaining("rod2"); } @Test @@ -1432,6 +1433,57 @@ class DefaultListableBeanFactoryTests { assertThat(bean.getSpouse()).isNull(); } + @Test + void autowirePreferredConstructors() { + lbf.registerBeanDefinition("spouse1", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("spouse2", new RootBeanDefinition(TestBean.class)); + RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependenciesBean.class); + bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); + lbf.registerBeanDefinition("bean", bd); + lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); + + ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); + Object spouse1 = lbf.getBean("spouse1"); + Object spouse2 = lbf.getBean("spouse2"); + assertThat(bean.getSpouse1()).isSameAs(spouse1); + assertThat(bean.getSpouse2()).isSameAs(spouse2); + } + + @Test + void autowirePreferredConstructorsFromAttribute() { + lbf.registerBeanDefinition("spouse1", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("spouse2", new RootBeanDefinition(TestBean.class)); + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(ConstructorDependenciesBean.class); + bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, + ConstructorDependenciesBean.class.getConstructors()); + lbf.registerBeanDefinition("bean", bd); + lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); + + ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); + Object spouse1 = lbf.getBean("spouse1"); + Object spouse2 = lbf.getBean("spouse2"); + assertThat(bean.getSpouse1()).isSameAs(spouse1); + assertThat(bean.getSpouse2()).isSameAs(spouse2); + } + + @Test + void autowirePreferredConstructorFromAttribute() throws Exception { + lbf.registerBeanDefinition("spouse1", new RootBeanDefinition(TestBean.class)); + lbf.registerBeanDefinition("spouse2", new RootBeanDefinition(TestBean.class)); + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setBeanClass(ConstructorDependenciesBean.class); + bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE, + ConstructorDependenciesBean.class.getConstructor(TestBean.class)); + lbf.registerBeanDefinition("bean", bd); + lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer()); + + ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class); + Object spouse = lbf.getBean("spouse1"); + assertThat(bean.getSpouse1()).isSameAs(spouse); + assertThat(bean.getSpouse2()).isNull(); + } + @Test void dependsOnCycle() { RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class); @@ -1441,11 +1493,11 @@ class DefaultListableBeanFactoryTests { bd2.setDependsOn("tb1"); lbf.registerBeanDefinition("tb2", bd2); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.preInstantiateSingletons()) - .withMessageContaining("Circular") - .withMessageContaining("'tb2'") - .withMessageContaining("'tb1'"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.preInstantiateSingletons()) + .withMessageContaining("Circular") + .withMessageContaining("'tb2'") + .withMessageContaining("'tb1'"); } @Test @@ -1460,11 +1512,11 @@ class DefaultListableBeanFactoryTests { bd3.setDependsOn("tb1"); lbf.registerBeanDefinition("tb3", bd3); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy( - lbf::preInstantiateSingletons) - .withMessageContaining("Circular") - .withMessageContaining("'tb3'") - .withMessageContaining("'tb1'"); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(lbf::preInstantiateSingletons) + .withMessageContaining("Circular") + .withMessageContaining("'tb3'") + .withMessageContaining("'tb1'"); } @Test @@ -1562,9 +1614,9 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(TestBean.class)) - .withMessageContaining("more than one 'primary'"); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(TestBean.class)) + .withMessageContaining("more than one 'primary'"); } @Test @@ -1607,10 +1659,10 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(TestBean.class)) - .withMessageContaining("Multiple beans found with the same priority") - .withMessageContaining("5"); // conflicting priority + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(TestBean.class)) + .withMessageContaining("Multiple beans found with the same priority") + .withMessageContaining("5"); // conflicting priority } @Test @@ -1815,9 +1867,9 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); - assertThatExceptionOfType(NoUniqueBeanDefinitionException.class).isThrownBy(() -> - lbf.getBean(ConstructorDependency.class, 42)) - .withMessageContaining("more than one 'primary'"); + assertThatExceptionOfType(NoUniqueBeanDefinitionException.class) + .isThrownBy(() -> lbf.getBean(ConstructorDependency.class, 42)) + .withMessageContaining("more than one 'primary'"); } @Test @@ -2004,10 +2056,10 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withMessageContaining("test") - .withMessageContaining("spouse"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withMessageContaining("test") + .withMessageContaining("spouse"); } @Test @@ -2071,10 +2123,10 @@ class DefaultListableBeanFactoryTests { lbf.registerBeanDefinition("test", bd); lbf.registerBeanDefinition("spouse", bd2); - assertThatExceptionOfType(UnsatisfiedDependencyException.class).isThrownBy(() -> - lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) - .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) - .withMessageContaining("5"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> lbf.autowire(DependenciesBean.class, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true)) + .withCauseExactlyInstanceOf(NoUniqueBeanDefinitionException.class) + .withMessageContaining("5"); } @Test @@ -2337,20 +2389,20 @@ class DefaultListableBeanFactoryTests { void beanDefinitionWithInterface() { lbf.registerBeanDefinition("test", new RootBeanDefinition(ITestBean.class)); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("test")) - .withMessageContaining("interface") - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("test")) + .withMessageContaining("interface") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); } @Test void beanDefinitionWithAbstractClass() { lbf.registerBeanDefinition("test", new RootBeanDefinition(AbstractBeanFactory.class)); - assertThatExceptionOfType(BeanCreationException.class).isThrownBy(() -> - lbf.getBean("test")) - .withMessageContaining("abstract") - .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> lbf.getBean("test")) + .withMessageContaining("abstract") + .satisfies(ex -> assertThat(ex.getBeanName()).isEqualTo("test")); } @Test diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java index 85da53b39b..178287ac48 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -575,6 +575,10 @@ public class GenericApplicationContext extends AbstractApplicationContext implem @Override @Nullable public Constructor>[] getPreferredConstructors() { + Constructor>[] fromAttribute = super.getPreferredConstructors(); + if (fromAttribute != null) { + return fromAttribute; + } Class> clazz = getBeanClass(); Constructor> primaryCtor = BeanUtils.findPrimaryConstructor(clazz); if (primaryCtor != null) {