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.get() 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. 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.
9. 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.