Injecting Prototype Beans into a Singleton Instance in Spring

1. Overview

In this quick article, we’re going to show different approaches of
injecting prototype beans into a singleton instance. We’ll discuss the
use cases and the advantages/disadvantages of each scenario.

By default, Spring beans are singletons. The problem arises when we try
to wire beans of different scopes. For example, a prototype bean into a
singleton. This is known as the scoped bean injection problem.

To learn more about bean scopes, this write-up
is a good place to start
.

2. Prototype Bean Injection Problem

In order to describe the problem, let’s configure the following beans:

@Configuration
public class AppConfig {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public PrototypeBean prototypeBean() {
        return new PrototypeBean();
    }

    @Bean
    public SingletonBean singletonBean() {
        return new SingletonBean();
    }
}

Notice that the first bean has a prototype scope, the other one is a
singleton.

Now, let’s inject the prototype-scoped bean into the singleton – and
then expose if via the getPrototypeBean() method:

public class SingletonBean {

    // ..

    @Autowired
    private PrototypeBean prototypeBean;

    public SingletonBean() {
        logger.info("Singleton instance created");
    }

    public PrototypeBean getPrototypeBean() {
        logger.info(String.valueOf(LocalTime.now()));
        return prototypeBean;
    }
}

Then, let’s load up the ApplicationContext and get the singleton bean
twice:

public static void main(String[] args) throws InterruptedException {
    AnnotationConfigApplicationContext context
      = new AnnotationConfigApplicationContext(AppConfig.class);

    SingletonBean firstSingleton = context.getBean(SingletonBean.class);
    PrototypeBean firstPrototype = firstSingleton.getPrototypeBean();

    // get singleton bean instance one more time
    SingletonBean secondSingleton = context.getBean(SingletonBean.class);
    PrototypeBean secondPrototype = secondSingleton.getPrototypeBean();

    isTrue(firstPrototype.equals(secondPrototype), "The same instance should be returned");
}

Here’s the output from the console:

Singleton Bean created
Prototype Bean created
11:06:57.894
// should create another prototype bean instance here
11:06:58.895

Both beans were initialized only once, at the startup of the
application context.

3. Injecting ApplicationContext

We can also inject the ApplicationContext directly into a bean.

To achieve this, either use the @Autowire annotation or implement the
ApplicationContextAware interface:

public class SingletonAppContextBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public PrototypeBean getPrototypeBean() {
        return applicationContext.getBean(PrototypeBean.class);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
      throws BeansException {
        this.applicationContext = applicationContext;
    }
}

Every time the getPrototypeBean() method is called, a new instance of
PrototypeBean will be returned from the ApplicationContext.

However, this approach has serious disadvantages. It contradicts the
principle of inversion of control, as we request the dependencies from
the container directly.

Also, we fetch the prototype bean from the applicationContext within
the SingletonAppcontextBean class. This means coupling the code to
the Spring Framework
.

4. Method Injection

Another way to solve the problem is method injection with the @Lookup
annotation
:

@Component
public class SingletonLookupBean {

    @Lookup
    public PrototypeBean getPrototypeBean() {
        return null;
    }
}

Spring will override the getPrototypeBean() method annotated with
@Lookup. It then registers the bean into the application context.
Whenever we request the getPrototypeBean() method, it returns a new
PrototypeBean instance.

It will use CGLIB to generate the bytecode responsible for fetching
the PrototypeBean from the application context.

5. javax.inject API

The setup along with required dependencies are described in this
Spring wiring
article.

Here’s the singleton bean:

public class SingletonProviderBean {

    @Autowired
    private Provider<PrototypeBean> myPrototypeBeanProvider;

    public PrototypeBean getPrototypeInstance() {
        return myPrototypeBeanProvider.get();
    }
}

We use Provider interface to inject the prototype bean. For each
getPrototypeInstance() method call, the
myPrototypeBeanProvider.g_et()_ method returns a new instance of
PrototypeBean.

6. Scoped Proxy

By default, Spring holds a reference to the real object to perform the
injection. Here, we create a proxy object to wire the real object with
the dependent one.

Each time the method on the proxy object is called, the proxy decides
itself whether to create a new instance of the real object or reuse the
existing one.

To set up this, we modify the Appconfig class to add a new @Scope
annotation:

@Scope(
  value = ConfigurableBeanFactory.SCOPE_PROTOTYPE,
  proxyMode = ScopedProxyMode.TARGET_CLASS)

By default, Spring uses CGLIB library to directly subclass the objects.
To avoid CGLIB usage, we can configure the proxy mode with
ScopedProxyMode.INTERFACES, to use the JDK dynamic proxy instead.

7. ObjectFactory Interface

Spring provides the
ObjectFactory<T>
interface to produce on demand objects of the given type:

public class SingletonObjectFactoryBean {

    @Autowired
    private ObjectFactory<PrototypeBean> prototypeBeanObjectFactory;

    public PrototypeBean getPrototypeInstance() {
        return prototypeBeanObjectFactory.getObject();
    }
}

Let’s have a look at getPrototypeInstance() method; getObject()
returns a brand new instance of PrototypeBean for each request. Here,
we have more control over initialization of the prototype.

Also, the ObjectFactory is a part of the framework; this means
avoiding additional setup in order to use this option.

8. Create a Bean at Runtime Using java.util.Function

Another option is to create the prototype bean instances at runtime,
which also allows us to add parameters to the instances.

To see an example of this, let’s add a name field to our PrototypeBean
class:

public class PrototypeBean {
    private String name;

    public PrototypeBean(String name) {
        this.name = name;
        logger.info("Prototype instance " + name + " created");
    }

    //...
}

Next, we’ll inject a bean factory into our singleton bean by making use
of the java.util.Function interface:

public class SingletonFunctionBean {

    @Autowired
    private Function<String, PrototypeBean> beanFactory;

    public PrototypeBean getPrototypeInstance(String name) {
        PrototypeBean bean = beanFactory.apply(name);
        return bean;
    }

}

Finally, we have to define the factory bean, prototype and singleton
beans in our configuration:

@Configuration
public class AppConfig {
    @Bean
    public Function<String, PrototypeBean> beanFactory() {
        return name -> prototypeBeanWithParam(name);
    }

    @Bean
    @Scope(value = "prototype")
    public PrototypeBean prototypeBeanWithParam(String name) {
       return new PrototypeBean(name);
    }

    @Bean
    public SingletonFunctionBean singletonFunctionBean() {
        return new SingletonFunctionBean();
    }
    //...
}

9. Testing

Let’s now write a simple JUnit test to exercise the case with
ObjectFactory interface:

@Test
public void givenPrototypeInjection_WhenObjectFactory_ThenNewInstanceReturn() {

    AbstractApplicationContext context
     = new AnnotationConfigApplicationContext(AppConfig.class);

    SingletonObjectFactoryBean firstContext
     = context.getBean(SingletonObjectFactoryBean.class);
    SingletonObjectFactoryBean secondContext
     = context.getBean(SingletonObjectFactoryBean.class);

    PrototypeBean firstInstance = firstContext.getPrototypeInstance();
    PrototypeBean secondInstance = secondContext.getPrototypeInstance();

    assertTrue("New instance expected", firstInstance != secondInstance);
}

After successfully launching the test, we can see that each time
getPrototypeInstance() method called, a new prototype bean instance
created.

10. Conclusion

In this short tutorial, we learned several ways to inject the prototype
bean into the singleton instance.

As always, the complete code for this tutorial can be found on
GitHub
project
.

Leave a Reply

Your email address will not be published.