Skip to content

Commit f8cb0fa

Browse files
committed
Custom resolution of preferred constructors for createBean(Class)
Avoids side effects of traditional AUTOWIRE_CONSTRUCTOR algorithm. Closes gh-30041
1 parent c56c16d commit f8cb0fa

File tree

8 files changed

+110
-13
lines changed

8 files changed

+110
-13
lines changed

spring-beans/src/main/java/org/springframework/beans/BeanUtils.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -246,7 +246,8 @@ public static <T> Constructor<T> getResolvableConstructor(Class<T> clazz) {
246246
// A single public constructor
247247
return (Constructor<T>) ctors[0];
248248
}
249-
else if (ctors.length == 0){
249+
else if (ctors.length == 0) {
250+
// No public constructors -> check non-public
250251
ctors = clazz.getDeclaredConstructors();
251252
if (ctors.length == 1) {
252253
// A single non-public constructor, e.g. from a non-public record type

spring-beans/src/main/java/org/springframework/beans/factory/config/AutowireCapableBeanFactory.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,10 +130,10 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
130130
* {@link BeanPostProcessor BeanPostProcessors}.
131131
* <p>Note: This is intended for creating a fresh instance, populating annotated
132132
* fields and methods as well as applying all standard bean initialization callbacks.
133-
* Constructor resolution is done via {@link #AUTOWIRE_CONSTRUCTOR}, also influenced
134-
* by {@link SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors}.
135-
* It does <i>not</i> imply traditional by-name or by-type autowiring of properties;
136-
* use {@link #createBean(Class, int, boolean)} for those purposes.
133+
* Constructor resolution is based on Kotlin primary / single public / single non-public,
134+
* with a fallback to the default constructor in ambiguous scenarios, also influenced
135+
* by {@link SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors}
136+
* (e.g. for annotation-driven constructor selection).
137137
* @param beanClass the class of the bean to create
138138
* @return the new bean instance
139139
* @throws BeansException if instantiation or wiring failed

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) {
315315
@SuppressWarnings("unchecked")
316316
public <T> T createBean(Class<T> beanClass) throws BeansException {
317317
// Use non-singleton bean definition, to avoid registering bean as dependent bean.
318-
RootBeanDefinition bd = new RootBeanDefinition(beanClass);
319-
bd.setAutowireMode(AUTOWIRE_CONSTRUCTOR);
318+
RootBeanDefinition bd = new CreateFromClassBeanDefinition(beanClass);
320319
bd.setScope(SCOPE_PROTOTYPE);
321320
bd.allowCaching = ClassUtils.isCacheSafe(beanClass, getBeanClassLoader());
322321
return (T) createBean(beanClass.getName(), bd, null);
@@ -1909,6 +1908,36 @@ Log getLogger() {
19091908
}
19101909

19111910

1911+
/**
1912+
* {@link RootBeanDefinition} subclass for {@code #createBean} calls with
1913+
* flexible selection of a Kotlin primary / single public / single non-public
1914+
* constructor candidate in addition to the default constructor.
1915+
* @see BeanUtils#getResolvableConstructor(Class)
1916+
*/
1917+
@SuppressWarnings("serial")
1918+
private static class CreateFromClassBeanDefinition extends RootBeanDefinition {
1919+
1920+
public CreateFromClassBeanDefinition(Class<?> beanClass) {
1921+
super(beanClass);
1922+
}
1923+
1924+
public CreateFromClassBeanDefinition(CreateFromClassBeanDefinition original) {
1925+
super(original);
1926+
}
1927+
1928+
@Override
1929+
@Nullable
1930+
public Constructor<?>[] getPreferredConstructors() {
1931+
return ConstructorResolver.determinePreferredConstructors(getBeanClass());
1932+
}
1933+
1934+
@Override
1935+
public RootBeanDefinition cloneBeanDefinition() {
1936+
return new CreateFromClassBeanDefinition(this);
1937+
}
1938+
}
1939+
1940+
19121941
/**
19131942
* Special DependencyDescriptor variant for Spring's good old autowire="byType" mode.
19141943
* Always optional; never considering the parameter name for choosing a primary candidate.
@@ -1941,7 +1970,7 @@ private static class FactoryBeanMethodTypeFinder implements MethodCallback {
19411970
}
19421971

19431972
@Override
1944-
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
1973+
public void doWith(Method method) throws IllegalArgumentException {
19451974
if (isFactoryBeanMethod(method)) {
19461975
ResolvableType returnType = ResolvableType.forMethodReturnType(method);
19471976
ResolvableType candidate = returnType.as(FactoryBean.class).getGeneric();

spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,54 @@ static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectio
12221222
return old;
12231223
}
12241224

1225+
/**
1226+
* See {@link BeanUtils#getResolvableConstructor(Class)} for alignment.
1227+
* This variant adds a lenient fallback to the default constructor if available, similar to
1228+
* {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors}.
1229+
*/
1230+
@Nullable
1231+
static Constructor<?>[] determinePreferredConstructors(Class<?> clazz) {
1232+
Constructor<?> primaryCtor = BeanUtils.findPrimaryConstructor(clazz);
1233+
1234+
Constructor<?> defaultCtor;
1235+
try {
1236+
defaultCtor = clazz.getDeclaredConstructor();
1237+
}
1238+
catch (NoSuchMethodException ex) {
1239+
defaultCtor = null;
1240+
}
1241+
1242+
if (primaryCtor != null) {
1243+
if (defaultCtor != null && !primaryCtor.equals(defaultCtor)) {
1244+
return new Constructor<?>[] {primaryCtor, defaultCtor};
1245+
}
1246+
else {
1247+
return new Constructor<?>[] {primaryCtor};
1248+
}
1249+
}
1250+
1251+
Constructor<?>[] ctors = clazz.getConstructors();
1252+
if (ctors.length == 1) {
1253+
// A single public constructor, potentially in combination with a non-public default constructor
1254+
if (defaultCtor != null && !ctors[0].equals(defaultCtor)) {
1255+
return new Constructor<?>[] {ctors[0], defaultCtor};
1256+
}
1257+
else {
1258+
return ctors;
1259+
}
1260+
}
1261+
else if (ctors.length == 0) {
1262+
// No public constructors -> check non-public
1263+
ctors = clazz.getDeclaredConstructors();
1264+
if (ctors.length == 1) {
1265+
// A single non-public constructor, e.g. from a non-public record type
1266+
return ctors;
1267+
}
1268+
}
1269+
1270+
return null;
1271+
}
1272+
12251273

12261274
/**
12271275
* Private inner class for holding argument combinations.

spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,6 +2126,23 @@ void createBeanWithDisposableBean() {
21262126
assertThat(tb.wasDestroyed()).isTrue();
21272127
}
21282128

2129+
@Test
2130+
void createBeanWithNonDefaultConstructor() {
2131+
lbf.registerBeanDefinition("otherTestBean", new RootBeanDefinition(TestBean.class));
2132+
TestBeanRecipient tb = lbf.createBean(TestBeanRecipient.class);
2133+
assertThat(lbf.containsSingleton("otherTestBean")).isTrue();
2134+
assertThat(tb.testBean).isEqualTo(lbf.getBean("otherTestBean"));
2135+
lbf.destroyBean(tb);
2136+
}
2137+
2138+
@Test
2139+
void createBeanWithPreferredDefaultConstructor() {
2140+
lbf.registerBeanDefinition("otherTestBean", new RootBeanDefinition(TestBean.class));
2141+
TestBean tb = lbf.createBean(TestBean.class);
2142+
assertThat(lbf.containsSingleton("otherTestBean")).isFalse();
2143+
lbf.destroyBean(tb);
2144+
}
2145+
21292146
@Test
21302147
void configureBean() {
21312148
MutablePropertyValues pvs = new MutablePropertyValues();

spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.beans.BeanWrapper;
2323
import org.springframework.beans.MutablePropertyValues;
2424
import org.springframework.beans.PropertyAccessorFactory;
25+
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
2526
import org.springframework.context.ApplicationContext;
2627
import org.springframework.context.ApplicationContextAware;
2728
import org.springframework.lang.Nullable;
@@ -86,8 +87,9 @@ public void setSchedulerContext(SchedulerContext schedulerContext) {
8687
@Override
8788
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
8889
Object job = (this.applicationContext != null ?
90+
// to be replaced with createBean(Class) in 6.1
8991
this.applicationContext.getAutowireCapableBeanFactory().createBean(
90-
bundle.getJobDetail().getJobClass()) :
92+
bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) :
9193
super.createJobInstance(bundle));
9294

9395
if (isEligibleForPropertyPopulation(job)) {

spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -558,7 +558,7 @@ public <T> void registerBean(@Nullable String beanName, Class<T> beanClass,
558558

559559

560560
/**
561-
* {@link RootBeanDefinition} marker subclass for {@code #registerBean} based
561+
* {@link RootBeanDefinition} subclass for {@code #registerBean} based
562562
* registrations with flexible autowiring for public constructors.
563563
*/
564564
@SuppressWarnings("serial")

spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private SpringContainedBean<?> createBean(
144144
try {
145145
if (lifecycleOptions.useJpaCompliantCreation()) {
146146
return new SpringContainedBean<>(
147-
this.beanFactory.createBean(beanType),
147+
this.beanFactory.createBean(beanType, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false),
148148
this.beanFactory::destroyBean);
149149
}
150150
else {

0 commit comments

Comments
 (0)