Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments?
In a @Configuration
class, a @Bean
method like so
@Bean
@Scope("prototype")
public Thing thing(String name) {
return new Thing(name);
}
is used to register a bean definition and provide the factory for creating the bean. The bean that it defines is only instantiated upon request using arguments that are determined either directly or through scanning that ApplicationContext
.
In the case of a prototype
bean, a new object is created every time and therefore the corresponding @Bean
method is also executed.
You can retrieve a bean from the ApplicationContext
through its BeanFactory#getBean(String name, Object... args)
method which states
Allows for specifying explicit constructor arguments / factory method arguments, overriding the specified default arguments (if any) in the bean definition.
Parameters:
args arguments to use if creating a prototype using explicit arguments to a static factory method. It is invalid to use a non-null args value in any other case.
In other words, for this prototype
scoped bean, you are providing the arguments that will be used, not in the constructor of the bean class, but in the @Bean
method invocation. (This method has very weak type guarantees since it uses a name lookup for the bean.)
Alternatively, you can use the typed BeanFactory#getBean(Class requiredType, Object... args)
method which looks up the bean by type.
This is at least true for Spring versions 4+.
Note that, if you don't want to start with the ApplicationContext
or BeanFactory
for your bean retrieval, you can inject an ObjectProvider
(since Spring 4.3).
A variant of
ObjectFactory
designed specifically for injection points, allowing for programmatic optionality and lenient not-unique handling.
and use its getObject(Object... args)
method
Return an instance (possibly shared or independent) of the object managed by this factory.
Allows for specifying explicit construction arguments, along the lines of
BeanFactory.getBean(String, Object)
.
For example,
@Autowired
private ObjectProvider<Thing> things;
[...]
Thing newThing = things.getObject(name);
[...]
With Spring > 4.0 and Java 8 you can do this more type-safely:
@Configuration
public class ServiceConfig {
@Bean
public Function<String, Thing> thingFactory() {
return name -> thing(name); // or this::thing
}
@Bean
@Scope(value = "prototype")
public Thing thing(String name) {
return new Thing(name);
}
}
Usage:
@Autowired
private Function<String, Thing> thingFactory;
public void onRequest(Request request) {
//request is already validated
String name = request.getParameter("name");
Thing thing = thingFactory.apply(name);
// ...
}
So now you can get your bean at runtime. This is a factory pattern of course, but you can save some time on writing specific class like ThingFactory
(however you will have to write custom @FunctionalInterface
to pass more than two parameters).
Since Spring 4.3, there is new way to do it, which was sewed for that issue.
ObjectProvider - It enables you just to add it as a dependency to your "argumented" Prototype scoped bean and to instantiate it using the argument.
Here is a simple example of how to use it:
@Configuration
public class MyConf {
@Bean
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public MyPrototype createPrototype(String arg) {
return new MyPrototype(arg);
}
}
public class MyPrototype {
private String arg;
public MyPrototype(String arg) {
this.arg = arg;
}
public void action() {
System.out.println(arg);
}
}
@Component
public class UsingMyPrototype {
private ObjectProvider<MyPrototype> myPrototypeProvider;
@Autowired
public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
this.myPrototypeProvider = myPrototypeProvider;
}
public void usePrototype() {
final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
myPrototype.action();
}
}
This will of course print hello string when calling usePrototype.