Spring series: @Primary
With a recent upgrade of my favorite framework I was thinking a short recap wouldn’t harm. This very first post, from a series of articles on Spring’s features I find interesting, deals with intricacies of injecting the right bean out of a range of suitable candidates. If you are keen to learn about the trade-offs when using @Primary keep reading.
The annotation @Primary does exactly what its name implies, i.e. allows to flag a bean as a default primary candidate for the dependency injection. Quoting the docs:
interface IMessage { String getMessage(); }
Next, there are two implementations. Please note that the first one, HelloWorldMessage, is marked as primary. Whereas the other bean, SpringMessage, is supposed to be loaded lazily – that is going to gain importance a bit further down this post.
import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; @Primary @Service public class HelloWorldMessage implements IMessage { @Override public String getMessage() { return ″Hello world!″; } }
import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @Service(″springMessage″) @Lazy public class SpringMessage implements IMessage { @Override public String getMessage() { return ″Spring is fun!″; } }
Bear in mind that for the @Primary to work you need to allow for component scanning, hence the minimal XML config below.
<context:annotation-config /> <context:component-scan base-package=″org.zezutom.springexamples″/>
At this point we are done with our little coding exercise. Let’s write some tests proving it all works as expected.
import junit.framework.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(″classpath:spring-config.xml″) public class MessageAppTests { @Autowired private IMessage message; @Test public void helloWorldShouldBeTheDefaultMessage() { Assert.assertEquals( String.format(″Hello world!″), message.getMessage()); } }
The test uses the returned message to verify that the correct candidate has been injected. It comes as no surprise that the test is a pass. The exact details can be gathered from the application logs:
1400 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Returning cached instance of singleton bean 'helloWorldMessage' 1402 [main] DEBUG org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'org.zezutom.springexamples.beanpreference.test.MessageAppTests' to bean named 'helloWorldMessage'
The logs however reveal one interesting fact. The other bean, i.e. the non-preferred candidate, has been instantiated too, even though it was supposed to be loaded only on demand!
1400 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating instance of bean 'springMessage' 1400 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Eagerly caching bean 'springMessage' to allow for resolving potential circular references 1402 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Finished creating instance of bean 'springMessage'
I couldn’t find any reasonable explanation for this behavior. In my opinion it is simply a bug. Using @Primary seems to disable another important feature, the @Lazy. Bear that in mind.
Speaking about limitations, there is another gotcha to be aware of. At times, prototype scoped beans come in handy. To get to a new instance every single time, the bean has to be obtained directly from the application context. Let’s add another test:
.. // Auto wiring the app context is comfortable and desired @Autowired private ApplicationContext context; .. // Note that the IMessage bean is now being obtained // directly from the application context @Test public void primaryBeanShouldBePickedByTheGetBeanCall() { Assert.assertEquals(″Hello world!″, context.getBean(IMessage.class).getMessage()); } ..
Now, if you assume the test will pass you are right… Provided you are lucky enough to work with one of the more recent versions of the Spring framework. In my experience, any version below 3.2.6 gives out about a non-unique bean:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.zezutom.springexamples.primary.IMessage] is defined: expected single bean but found 2: helloWorldMessage,springMessage
So please be aware of the fact that in the older framework releases the @Primary is ignored when the bean is accessed via the getBean method of the application context itself.
That’s it for today. Next time, I will follow up on the topic, talking about related annotations @Qualifier and @Resource.
Source Code
Next: Part 2 – @Qualifier and @Resource