Java – Spring Java Config: how do you create a prototype-scoped @Bean with runtime arguments

javaprototypescopespringspring-java-config

Using Spring's Java Config, I need to acquire/instantiate a prototype-scoped bean with constructor arguments that are only obtainable 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;
    }
}

Notice name is final: it can only be supplied via a constructor, and guarantees immutability. The other dependencies are implementation-specific dependencies of the Thing class, and shouldn't be known to (tightly coupled to) the request handler implementation.

This code works perfectly well with Spring XML config, for example:

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

How do I achieve the same thing with Java config? The following does not work using Spring 3.x:

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

Now, I could create a Factory, e.g.:

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

But that defeats the entire 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 would be able to avoid:

  • defining a Factory interface
  • defining a Factory implementation
  • writing tests for the Factory implementation

That's a ton of work (relatively speaking) for something so trivial that Spring already supports via XML config.

Best Answer

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);
[...]