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" not match "java.util.Collections$SingletonSet<?>".

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.