Spring Java configuration: how do you prototype @Bean with runtime arguments?

Using Spring Java Config, I need to get / create an instance of a bean with a prototype with constructor arguments that are available only at runtime. Consider the following code example (simplified for brevity):

@Autowired private ApplicationContext appCtx; public void onRequest(Request request) { //request is already validated String name = request.getParameter("name"); Thing thing = appCtx.getBean(Thing.class, name); //System.out.println(thing.getName()); //prints name } 

where the Thing class is defined as follows:

 public class Thing { private final String name; @Autowired private SomeComponent someComponent; @Autowired private AnotherComponent anotherComponent; public Thing(String name) { this.name = name; } public String getName() { return this.name; } } 

name notification name is final : it can only be provided through the constructor and guarantees immutability. Other dependencies are implementation-dependent dependencies of the Thing class and should not be known (closely related) to the implementation of the request handler.

This code works great with Spring XML configuration, for example:

 <bean id="thing", class="com.whatever.Thing" scope="prototype"> <!-- other post-instantiation properties omitted --> </bean> 

How can I achieve the same using Java configuration? The following does not work using Spring 3.x:

 @Bean @Scope("prototype") public Thing thing(String name) { return new Thing(name); } 

Now I can create a Factory, for example:

 public interface ThingFactory { public Thing createThing(String name); } 

But this negates the whole point of using Spring to replace the ServiceLocator and Factory design pattern, which would be ideal for this use case.

If Spring Java Config could do this, I could avoid:

  • factory interface definition
  • Factory implementation definition
  • writing tests to implement Factory

This is a ton of work (relatively speaking) for something so trivial that Spring already supports through XML configuration.

+120
java scope spring prototype spring-java-config
Mar 03 '14 at 19:43
source share
6 answers

In the @Configuration class, @Bean method, like so

 @Bean @Scope("prototype") public Thing thing(String name) { return new Thing(name); } 

used to register a component definition and create a factory to create a component . The component that it defines is created only by request, using arguments that are determined either directly or by scanning this ApplicationContext .

In the case of the prototype component, a new object is created each time, and therefore the corresponding @Bean method is also executed.

You can extract the component from the ApplicationContext using BeanFactory#getBean(String name, Object... args) which states:

Allows you to specify constructor arguments / factory method arguments, overriding the specified default arguments (if any) in the component definition.

Options:

args arguments to use when creating a prototype using explicit arguments to the static factory method. It is not allowed to use a nonzero args value in any other case.

In other words, for this component with prototype you use the arguments that will be used, not in the constructor of the bean class, but in the @Bean method.

This is at least true for Spring 4+ versions.

+85
Mar 04 '14 at 21:59
source share

With Spring> 4.0 and Java 8, you can do this more 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); } } 

Using:

 @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 a bean at runtime. This, of course, is a factory pattern, but you can save some time writing a specific class, such as ThingFactory (however, you will need to write a custom @FunctionalInterface to pass more than two parameters).

+42
Apr 24 '17 at 18:08
source share

UPDATED for comment

First off, I'm not sure why you say β€œthis doesn't work” for something that works fine in Spring 3.x. I suspect something must be wrong with your configuration.

It works:

- Configuration file:

 @Configuration public class ServiceConfig { // only here to demo execution order private int count = 1; @Bean @Scope(value = "prototype") public TransferService myFirstService(String param) { System.out.println("value of count:" + count++); return new TransferServiceImpl(aSingletonBean(), param); } @Bean public AccountRepository aSingletonBean() { System.out.println("value of count:" + count++); return new InMemoryAccountRepository(); } } 

- test file to run:

 @Test public void prototypeTest() { // create the spring container using the ServiceConfig @Configuration class ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class); Object singleton = ctx.getBean("aSingletonBean"); System.out.println(singleton.toString()); singleton = ctx.getBean("aSingletonBean"); System.out.println(singleton.toString()); TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One"); System.out.println(transferService.toString()); transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two"); System.out.println(transferService.toString()); } 

Using Spring 3.2.8 and Java 7 produces this output:

 value of count:1 com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d value of count:2 Using name value of: simulated Dynamic Parameter One com.spring3demo.account.service.TransferServiceImpl@634d6f2c value of count:3 Using name value of: simulated Dynamic Parameter Two com.spring3demo.account.service.TransferServiceImpl@70bde4a2 

Thus, the "Singleton" Bean is requested twice. However, as you would expect, Spring creates it only once. The second time, he sees that he has a Bean and simply returns an existing object. The constructor (@Bean method) is not called a second time. As a sign of respect, when a 'Prototype' Bean request is requested from the same context object twice, we see that the link changes the output AND, that the constructor method (@Bean) is called twice.

So, the question is how to introduce a singleton into a prototype. The configuration class above shows how to do this! You must pass all such references to the constructor. This will allow the created class to be pure POJO, as well as make the contained reference objects immutable, as it should be. Thus, the transfer service might look something like this:

 public class TransferServiceImpl implements TransferService { private final String name; private final AccountRepository accountRepository; public TransferServiceImpl(AccountRepository accountRepository, String name) { this.name = name; // system out here is only because this is a dumb test usage System.out.println("Using name value of: " + this.name); this.accountRepository = accountRepository; } .... } 

If you write Unit Tests, you will be so happy that you created classes without any @Autowired. If you need stand-alone components, save them in java configuration files.

This will call the method below in BeanFactory. Pay attention to the description of how this is intended for your specific use case.

 /** * Return an instance, which may be shared or independent, of the specified bean. * <p>Allows for specifying explicit constructor arguments / factory method arguments, * overriding the specified default arguments (if any) in the bean definition. * @param name the name of the bean to retrieve * @param 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. * @return an instance of the bean * @throws NoSuchBeanDefinitionException if there is no such bean definition * @throws BeanDefinitionStoreException if arguments have been given but * the affected bean isn't a prototype * @throws BeansException if the bean could not be created * @since 2.5 */ Object getBean(String name, Object... args) throws BeansException; 
+15
Mar 04 '14 at 23:02
source share

Since spring 4.3, a new way to do this has appeared that has been stitched for this problem.

ObjectProvider - it allows you to simply add it as a dependency to your "reasoned" bean with a prototype and create an instance 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, of course, will print a welcome string when calling usePrototype.

+8
Aug 25 '18 at 23:13
source share

You can achieve a similar effect by simply using the inner class :

 @Component class ThingFactory { private final SomeBean someBean; ThingFactory(SomeBean someBean) { this.someBean = someBean; } Thing getInstance(String name) { return new Thing(name); } class Thing { private final String name; Thing(String name) { this.name = name; } void foo() { System.out.format("My name is %s and I can " + "access bean from outer class %s", name, someBean); } } } 
0
Nov 02 '18 at 21:02
source share

Late answer with a slightly different approach. This is a continuation of this recent question , which concerns the question itself.

Yes, as already mentioned, you can declare a prototype component that takes a parameter in the @Configuration class, which allows you to create a new component with each injection.
This will make this @Configuration class a factory, and in order not to give this factory too much responsibility, it should not include other beans.

 @Configuration public class ServiceFactory { @Bean @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) public Thing thing(String name) { return new Thing(name); } } 

But you can also add this configuration component to create Thing :

 @Autowired private ServiceFactory serviceFactory; public void onRequest(Request request) { //request is already validated String name = request.getParameter("name"); Thing thing = serviceFactory.thing(name); // create a new bean at each invocation // ... } 

It is both type-safe and concise.

-one
Aug 11 '19 at 11:03
source share



All Articles