Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Fail to inject to a parameterized type with FactoryBean #1292

@ttddyy

Description

@ttddyy

About

This is another corner case of FactoryBean usage but essentially used by @JsonTest slice test.

The following sample is a simplified version of what JsonTestersAutoConfiguration does.

Code

This sample test works in pure JVM but fails with AOT generated code in JVM.

@SpringBootTest
public class MyFactoryBeanTest {

    @Autowired
    MyClass<String> myClass;   // Injecting to a parameterized instance

    @Test
    void myTest() {
        assertThat(this.myClass.value).isEqualTo("HELLO");
    }

    static class MyClass<T> {
        T value;
        public MyClass(T value) {
            this.value = value;
        }
    }

    static class MyFactory<T, M> implements FactoryBean<T> {
        private Class<?> type;
        private M value;

        public MyFactory(Class<?> type, M value) {
            this.type = type;
            this.value = value;
        }

        @Override
        @SuppressWarnings("unchecked")
        public T getObject() throws Exception {
            return (T) this.value;
        }

        @Override
        public Class<?> getObjectType() {
            return this.type;
        }
    }

    @Configuration(proxyBeanMethods = false)
    static class MyConfig {
        @Bean
        FactoryBean<MyClass<?>> foo() {  // fail with no matching bean
            return new MyFactory<>(MyClass.class, new MyClass<>("HELLO"));
        }
    }
}

Failure

...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.webmvc.test3.MyFactoryBeanTest$MyClass<java.lang.String>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1790)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1346)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:656)
	... 71 more

Observation

In short, what I found is a difference in the beantype in BD.

In JVM mode, the BD(ConfigurationClassBeanDefinitionReader$ConfigurationClassBeanDefinition) for the "foo" bean does not have targetType initially. Then, the GenericTypeAwareAutowireCandidateResolver resolves it by calling beanFactory.getType("foo") which in turn calls MyFactory#getObjectType(). Then, the injection matches.

However, in AOT JVM mode, the following bean definition is generated. This BD explicitly specifies the "beanType" to ResolvableType.forClassWithGenerics(MyFactoryBeanTest.MyClass.class, Object.class)), which doesn't match when resolving the injection point.

BeanDefinitionRegistrar.of("foo", 
        ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(MyFactoryBeanTest.MyClass.class, Object.class)))
    .withFactoryMethod(MyFactoryBeanTest.MyConfig.class, "foo")
    .instanceSupplier(() -> context.getBean(MyFactoryBeanTest.MyConfig.class).foo()).register(context);

One worked modification is to specify the exact "beanType"(MyClass<String>) on this generated code.

BeanDefinitionRegistrar.of("foo",
        ResolvableType.forClassWithGenerics(MyFactoryBeanTest.MyClass.class, String.class))  // <===
    .withFactoryMethod(MyFactoryBeanTest.MyConfig.class, "foo")
    .instanceSupplier(() -> context.getBean(MyFactoryBeanTest.MyConfig.class).foo()).register(context);

However, I think putting logic to generate the above code would be a problem.
To retrieve the actual parameterized type, String in this case, would require instantiating the factory beans at build time.
I don't think it is a direction that currently AOT is heading.

Another worked solution is to remove the targetType from the generated BD, and let the autowirng to the injection point solve the target type at runtime. This would be the same behavior as the JVM mode.

BeanDefinitionRegistrar.of("foo",
        ResolvableType.forClassWithGenerics(FactoryBean.class, ResolvableType.forClassWithGenerics(MyFactoryBeanTest.MyClass.class, Object.class)))
    .withFactoryMethod(MyFactoryBeanTest.MyConfig.class, "foo")
    .customize(rbd -> rbd.setTargetType((Class<?>) null))    // <=== nullify the targetType
    .instanceSupplier(() -> context.getBean(MyFactoryBeanTest.MyConfig.class).foo()).register(context);

This goes to the same path of JVM mode, calls beanFactory.getType("foo"), and resolves to a proper type.
If going this path, probably BeanDefinitionRegistrar can have a method that does not specify the targetType or use a customizer to override with null like the above snippet.

cc @snicoll

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions