Spring partial auto-wire prototype bean with runtime-defined constructor arguments
The javadoc before ConstructorResolver.autowireConstructor(...)
says
Also applies if explicit constructor argument values ββare specified, mapping all other arguments from beans to bean factory.
but I cannot get it to work. I am getting BeanCreationException
:
Could not resolve matching constructor (hint: specify index / type / name arguments for simple parameters to avoid type ambiguity)
In this example, I have a bean with a constructor that accepts Spring beans as well as String
and int
that will only be known at runtime.
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class BeanWithRuntimeDependencies {
public final DependencyA dependencyA;
public final DependencyB dependencyB;
public final String myString;
public final int myInt;
public BeanWithRuntimeDependencies(
DependencyA dependencyA, DependencyB dependencyB,
String myString, int myInt) {
this.dependencyA = dependencyA;
this.dependencyB = dependencyB;
this.myString = myString;
this.myInt = myInt;
}
}
@Component
public class DependencyA { /* ... */ }
@Component
public class DependencyB { /* ... */ }
and my test:
@RunWith(SpringRunner.class)
@SpringBootTest
public class PrototypeBeanConstructorsApplicationTests {
@Autowired private ApplicationContext context;
@Autowired private DependencyA dependencyA;
@Autowired private DependencyB dependencyB;
@Test
public void getBeanFromContext() {
BeanWithRuntimeDependencies bean =
context.getBean(BeanWithRuntimeDependencies.class, "runtime string", 10);
assertNotNull(bean);
assertEquals(dependencyA, bean.dependencyA);
assertEquals(dependencyB, bean.dependencyB);
assertEquals("runtime string", bean.myString);
assertEquals(10, bean.myInt);
}
}
The source code ConstructorResolver.autowireConstructor(...)
has a comment:
// Explicit arguments given -> arguments length must match exactly.
which seems to contradict his javadoc.
Can this be done? What am I doing wrong?
source to share
It looks like it can't be done the way you are doing.
This is actually a strange situation. According to the following snippet at ConstructorResolver.autowireConstructor(...)
(source line # 207) Spring will not consider your constructor as a candidate for invocation:
...
// Explicit arguments given -> arguments length must match exactly.
if (paramTypes.length != explicitArgs.length) {
continue;
}
And as you correctly pointed out, this is indeed contrary to the javadoc statement:
... mapping all remaining arguments to beans with bean factory
But in any case, implementation means that by default Spring cannot resolve a constructor for an instance of such beans. And you need to create a factory method manually. Something like:
@Configuration
public class Config{
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public BeanWithRuntimeDependencies beanWithRuntimeDependencies(String myString, int myInt){
return new BeanWithRuntimeDependencies(dependencyA(), dependencyB(), myString, myInt);
}
@Bean
public DependencyA dependencyA(){
return new dependencyA();
}
@Bean
public DependencyB dependencyB(){
return new dependencyB();
}
}
Then you can get the bean from the context as you want:
BeanWithRuntimeDependencies bean =
context.getBean(BeanWithRuntimeDependencies.class, "runtime string", 10);
If you don't want to deal with the configuration class and factory, you can simply pass the beans you need to context.getBean()
. You must of course get these beans from the context:
BeanWithRuntimeDependencies bean =
context.getBean(BeanWithRuntimeDependencies.class,
context.getBean(DependencyA.class),
context.getBean(DependencyB.class),
"runtime string", 10);
source to share