Spring Bean Scopes: Singleton with Prototypes
Introduction
This is the second post on the series about Spring Bean Scopes. In the previous tutorial we saw that there were issues rising when a Prototype scoped Bean was injected in a Singleton scoped Bean. The main problem is that autowired Prototypes will be injected when the Singleton Bean is instantiated (which happens only once) thus even though they are prototypes in reality they’ll behave as singletons.
The next code highlights this behavior:
1@Test
2public void getAutowiredSample_na_shouldBeSameInstance() {
3 // Given
4 ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5 ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7 // When
8 Sample autowiredSample1 = singleton1.getAutowiredSample();
9 Sample autowiredSample2 = singleton2.getAutowiredSample();
10
11 // Then
12 assertEquals(singleton1, singleton2);
13 assertEquals(autowiredSample1, autowiredSample2);
14 assertEquals(autowiredSample1.getUuid(), autowiredSample2.getUuid());
15}
In the previous example, although the object requested with getAutowiredSample
is defined with a Prototype scoped Bean, the instance of the object returned in both requests is the same.
To overcome this behavior there are several options available.
@Lookup annotation
The @Lookup
annotation allows us to annotate getter methods for a specific Bean. The Spring container will override these methods at runtime and redirect them to the BeanFactory performing a regular getBean
call, returning whatever bean the getBean method would be returning (Singleton, Prototype, Request, Session…). In this case, as the defined Bean has a Prototype scope it will return a new instance for each Bean request.
The next code snippet shows how to use the @Lookup
annotation:
1@Lookup
2public Sample getSampleUsingLookup() {
3 return null;
4}
The problem with this approach is that it will only work with @Component
annotated beans instantiated using component scan or autodetection. It won’t work with @Bean
annotated beans.
The next test shows the behavior of the @Lookup
annotation in a @Component
singleton Bean:
1@Test
2public void getSampleUsingLookupInComponent_na_shouldBeDifferent() {
3 // Given
4 SingletonWithLookupPrototype singleton1 = applicationContext.getBean(SingletonWithLookupPrototype.class);
5 SingletonWithLookupPrototype singleton2 = applicationContext.getBean(SingletonWithLookupPrototype.class);
6
7 // When
8 Sample lookedupSample1 = singleton1.getSampleUsingLookup();
9 Sample lookedupSample2 = singleton2.getSampleUsingLookup();
10
11 // Then
12 assertEquals(singleton1, singleton2);
13 assertNotNull(singleton1);
14 assertNotNull(singleton2);
15 assertNotEquals(lookedupSample1, lookedupSample2);
16 assertNotEquals(lookedupSample1.getUuid(), lookedupSample2.getUuid());
17}
As shown in the previous snippet, this is the behavior we are expecting. Two Singleton Beans which are the same instance even if requested twice, return different instances of the Sample
class which is defined in a Bean with Prototype scope.
There is a problem with this approach, as already stated, it will only work with Singleton Beans declared using the @Component
annotation. If the Bean is declared using an approach similar to the following:
1@Bean
2@Scope(SCOPE_SINGLETON)
3public ClassWithSamplePrototypes singletonWithSamplePrototypes(Sample autowiredSample, ObjectFactory<Sample> sampleObjectFactory) {
4 return new ClassWithSamplePrototypes(autowiredSample, sampleObjectFactory);
5}
The previous test will fail, as the getSampleUsingLoopup()
won’t be overridden, thus returning null. We can check this behavior with this modified test:
1@Test
2public void getSampleUsingLookupInRegularBean_na_shouldBeNull() {
3 // Given
4 ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5 ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7 // When
8 Sample lookedupSample1 = singleton1.getSampleUsingLookup();
9 Sample lookedupSample2 = singleton2.getSampleUsingLookup();
10
11 // Then
12 assertEquals(singleton1, singleton2);
13 assertNull(lookedupSample1);
14 assertNull(lookedupSample2);
15}
Using ApplicationContext
Another approach to overcome the scoped bean injection problem is to inject ApplicationContext into the Singleton scoped bean. Whenever an instance of a Prototype scoped bean is needed, the singleton should request it to the ApplicationContext
instance.
We can declare the Singleton scoped class as follows:
1/** ... **/
2public class ClassWithSamplePrototypes implements ApplicationContextAware {
3/** ... **/
4 private ApplicationContext applicationContext;
5
6 @Override
7 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
8 this.applicationContext = applicationContext;
9 }
10/** ... **/
11 public Sample getSampleUsingApplicationContext() {
12 return applicationContext.getBean(Sample.class);
13 }
14/** ... **/
15}
We can test the expected behavior using the following code snippet:
1@Test
2public void getSampleUsingApplicationContext_na_shouldBeDifferent() {
3 // Given
4 ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5 ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7 // When
8 Sample applicationContextSample1 = singleton1.getSampleUsingApplicationContext();
9 Sample applicationContextSample2 = singleton2.getSampleUsingApplicationContext();
10
11 // Then
12 assertEquals(singleton1, singleton2);
13 assertNotNull(singleton1);
14 assertNotNull(singleton2);
15 assertNotEquals(applicationContextSample1, applicationContextSample2);
16 assertNotEquals(applicationContextSample1.getUuid(), applicationContextSample2.getUuid());
17}
Although this approach works, it clearly violates SOLID by contradicting the inversion of control design principle. Dependencies are requested directly to the Spring container instead of being injected to the class, so the code is tightly coupled to Spring. e.g. there’s no other way to run the previous unit test without using SpringJUnit4ClassRunner
.
Spring’s ObjectFactory interface
Finally but, in my opinion, the cleanest way to solve the scoped bean injection problem is to use the Spring’s ObjectFactory
interface. With this approach, instead of injecting the Bean itself we inject a factory for the Bean using this interface.
1/** ... **/
2public class ClassWithSamplePrototypes implements ApplicationContextAware {
3/** ... **/
4 private final ObjectFactory<Sample> sampleObjectFactory;
5 @Autowired
6 public ClassWithSamplePrototypes(Sample autowiredSample, ObjectFactory<Sample> sampleObjectFactory) {
7 this.autowiredSample = autowiredSample;
8 this.sampleObjectFactory = sampleObjectFactory;
9 }
10/** ... **/
11 public Sample getSampleUsingObjectFactory() {
12 return sampleObjectFactory.getObject();
13 }
14/** ... **/
15}
Instead of injecting the bean during the Singleton scope class instantiation, we inject an interface for a Bean ObjectFactory
for a given type of Bean. Depending on the type and scope configured for the ObjectFactory
, the getObject
method will return an object instance accordingly.
The next test shows the described expected behavior:
1@Test
2public void getSampleUsingObjectFactory_na_shouldBeDifferent() {
3 // Given
4 ClassWithSamplePrototypes singleton1 = applicationContext.getBean(ClassWithSamplePrototypes.class);
5 ClassWithSamplePrototypes singleton2 = applicationContext.getBean(ClassWithSamplePrototypes.class);
6
7 // When
8 Sample objectFactorySample1 = singleton1.getSampleUsingObjectFactory();
9 Sample objectFactorySample2 = singleton2.getSampleUsingObjectFactory();
10
11 // Then
12 assertEquals(singleton1, singleton2);
13 assertNotNull(singleton1);
14 assertNotNull(singleton2);
15 assertNotEquals(objectFactorySample1, objectFactorySample2);
16 assertNotEquals(objectFactorySample1.getUuid(), objectFactorySample2.getUuid());
17}
Using ObjectFactory
won’t violate any OOP principle as you can always inject it using a custom ObjectFactory
implementation:
1new ClassWithSamplePrototypes(new Sample(),
2 () -> new Sample());
Conclusion
This post shows how to solve the scoped bean injection problems that arises when injecting a Prototype scoped Bean into a Singleton scoped Bean. We’ve seen different approaches to solve the problem and the advantages and disadvantages for each of them.
The full source code for this post can be found at GitHub.
Comments in "Spring Bean Scopes: Singleton with Prototypes"
That's an excellent article and I have followed it and it worked for me.
Now I have a scenario where I want to access the particular prototype bean through singleton bean or let's say I want to track all prototype beans through singleton bean.
Is it possible?
Thanks for your time