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:

Indicates that a bean should be given preference when multiple candidates are qualified to autowire a single-valued dependency.
 
Here is a superficial example demonstrating the use case. The interface can’t have been much simpler than this:
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

Categories: JavaSpring

Tomas Zezula

Hello! I'm a technology enthusiast with a knack for solving problems and a passion for making complex concepts accessible. My journey spans across software development, project management, and technical writing. I specialise in transforming rough sketches of ideas to fully launched products, all the while breaking down complex processes into understandable language. I believe a well-designed software development process is key to driving business growth. My focus as a leader and technical writer aims to bridge the tech-business divide, ensuring that intricate concepts are available and understandable to all. As a consultant, I'm eager to bring my versatile skills and extensive experience to help businesses navigate their software integration needs. Whether you're seeking bespoke software solutions, well-coordinated product launches, or easily digestible tech content, I'm here to make it happen. Ready to turn your vision into reality? Let's connect and explore the possibilities together.