-
Notifications
You must be signed in to change notification settings - Fork 346
Fail to inject to a parameterized type with FactoryBean #1292
Description
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