Affects:
- Spring Framework 6.0.3
- JDK 17
Overview
Bean with 2 constructors cannot find the correct one in org.springframework.beans.factory.support.ConstructorResolver
Demo
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.*;
import org.springframework.util.Assert;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public class Demo {
public static void main(String[] args) {
Set<String> set = Collections.singleton("dfsdfsd");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(Pojo.class);
builder.addConstructorArgValue(set);
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
try {
Class<?> clazz = Class.forName("org.springframework.beans.factory.support.ConstructorResolver");
Constructor<?> constructor = clazz.getDeclaredConstructor(AbstractAutowireCapableBeanFactory.class);
constructor.setAccessible(true);
Object obj = constructor.newInstance(new DefaultListableBeanFactory());
Method method = clazz.getMethod("resolveConstructorOrFactoryMethod", String.class, RootBeanDefinition.class);
method.setAccessible(true);
Object res = method.invoke(obj, "xx", beanDefinition);
Assert.notNull(res, "Should not be null");
} catch (Exception e) {
System.out.println("Catch reflection fail for " + e.getMessage());
}
}
public class Pojo {
Set<String> a;
public Pojo(String... a) {
this(new LinkedHashSet<>(Arrays.asList(a)));
}
public Pojo(Set<String> a) {
this.a = a;
}
public Set<String> getA() {
return this.a;
}
}
}
Throws IllegalStateException:
java.lang.IllegalStateException: No constructor or factory method candidate found for Root bean: class [com.tuya.demo.Demo$Pojo]; scope=; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodNames=null; destroyMethodNames=null and argument types [java.util.Collections$SingletonSet<?>]
Comment From: tw11509
Because one of constructor argument is Set with parameterized type:
public Pojo(Set<String> a) {
this.a = a;
}
When ConstructorResolver tried to resolve type of given Set in ConstructorArgumentValues, it would get ResolvableType with type "class java.util.Collections$SingletonSet<?>".
Because of type erasure, we cannot get really parameterized type of set is at runtime.
Then ConstructorResolver tried to resolve whether any constructors on Pojo match valueTypes from argument values, it would find nothing, because construct with parameter type "java.util.Set
Finally, Exception be thrown.
Comment From: liujunlou
@tw11509
... Because of type erasure, we cannot get really parameterized type of set is at runtime.
Then ConstructorResolver tried to resolve whether any constructors on Pojo match valueTypes from argument values, it would find nothing, because construct with parameter type "java.util.Set
" not match "java.util.Collections$SingletonSet<?>". ...
first, thx for reply
I find ConstructorResolver.FallbackMode
in spring-beans ConstructorResolver
, I think ASSIGNABLE_ELEMENT
is used for type erasure of Set , but I found the type of element is Object which should be String.
Comment From: simonbasle
hi @liujunlou, can you clarify your use case and perhaps provide a reproducer that is a little bit higher-level (like an integration test using the Spring TestContext Framework)?
Comment From: sbrannen
I've created two integration tests that demonstrate typical use cases for constructor resolution.
@SpringJUnitConfig
class ConstructorResolutionTests1 {
@Test
void test(@Autowired Pojo pojo) {
assertThat(pojo.getA()).containsExactlyInAnyOrder("enigma", "puzzle");
assertThat(pojo.setConstructorInvoked).isTrue();
}
@Configuration
@Import(Pojo.class)
static class Config {
@Bean
Set<String> set() {
return Set.of("enigma", "puzzle");
}
}
public static class Pojo {
boolean setConstructorInvoked = false;
Set<String> a;
public Pojo(String... a) {
this(new LinkedHashSet<>(Arrays.asList(a)));
}
@Autowired
public Pojo(Set<String> a) {
this.a = a;
this.setConstructorInvoked = true;
}
public Set<String> getA() {
return this.a;
}
}
}
ConstructorResolutionTests1
passes unmodified due to the presence of @Autowired
on the desired Pojo
constructor. If you remove @Autowired
from the Pojo(Set)
constructor the test will fail because Spring cannot infer which constructor to invoke, and that's the expected behavior.
@SpringJUnitConfig
class ConstructorResolutionTests2 {
@Test
void test(GenericApplicationContext context) {
AutowireCapableBeanFactory beanFactory = context.getAutowireCapableBeanFactory();
Pojo pojo = (Pojo) beanFactory.autowire(Pojo.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
assertThat(pojo.getA()).containsExactlyInAnyOrder("enigma", "puzzle");
assertThat(pojo.setConstructorInvoked).isTrue();
}
@Configuration
static class Config {
@Bean
Set<String> set() {
return Set.of("enigma", "puzzle");
}
}
public static class Pojo {
boolean setConstructorInvoked = false;
Set<String> a;
public Pojo(String... a) {
this(new LinkedHashSet<>(Arrays.asList(a)));
}
public Pojo(Set<String> a) {
this.a = a;
this.setConstructorInvoked = true;
}
public Set<String> getA() {
return this.a;
}
}
}
ConstructorResolutionTests2
also passes and demonstrates that constructor resolution works without @Autowired
on the Pojo(Set)
constructor.
As @simonbasle pointed out, we need to know your concrete use case in order to assess if anything should be done to support it.
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: spring-projects-issues
Closing due to lack of requested feedback. If you would like us to look at this issue, please provide the requested information and we will re-open the issue.