Pages

Autowiring ambiguity

I have a hierarchy of Spring Components rooted in the interface Dessert. Let's see what could go wrong when I try to autowire on them.

Here is the base interface:
public interface Dessert {
    default double getPrice() {
        return 0;
    }
}
It has three empty implementing classes named Cake, Cookies, and IceCream. For instance, here is the third one:
@Component
public class IceCream implements Dessert {
}
In my test case, I autowire a Dessert that I am going to use in the tests, like this:
@Autowired
Dessert dessert;

@Test
public void test() {
    assertThat(dessert.getPrice(), is(0.0));
}
Spring has to know which Dessert to create and wire, and this could be done by class configuration. In this case, the tester should know it, by annotating as SpringApplicationConfiguration the class, that should be defined and annotated as a Spring Configuration file, like this:
@Configuration
@ComponentScan(basePackageClasses = Dessert.class)
public class DessertConfig {
}
Do not forget to specify the component scan annotation, otherwise Spring won't see the beans, and would complain something like:
org.springframework.beans.factory.BeanCreationException:
 Error creating bean with name 'dd.sia.restaurant.DessertTest':
 Injection of autowired dependencies failed; nested exception is
  org.springframework.beans.factory.BeanCreationException:
Could not autowire field:
 dd.sia.restaurant.Dessert dd.sia.restaurant.DessertTest.dessert;
nested exception is
 org.springframework.beans.factory.NoSuchBeanDefinitionException:
 No qualifying bean of type [dd.sia.restaurant.Dessert] found for dependency:
 expected at least 1 bean which qualifies as autowire candidate for this dependency.
 Dependency annotations:
 {@org.springframework.beans.factory.annotation.Autowired(required=true)}
However, activating the ComponentScan is only the first step. Since we have three Components based on Dessert, we should say to Spring which one to choose. If we don't we are going to have a different, albeit similar, exception:
org.springframework.beans.factory.BeanCreationException:
 Error creating bean with name 'dd.sia.restaurant.DessertTest':
 Injection of autowired dependencies failed; nested exception is
 org.springframework.beans.factory.BeanCreationException:
Could not autowire field:
 dd.sia.restaurant.Dessert dd.sia.restaurant.DessertTest.dessert;
nested exception is 
 org.springframework.beans.factory.NoUniqueBeanDefinitionException:
 No qualifying bean of type [dd.sia.restaurant.Dessert] is defined:
 expected single matching bean but found 3: cake,cookies,iceCream
One way to solve this issue is annotating a Component as primary:
@Component
@Primary
public class Cake implements Dessert {
}
In this way, Spring knows that Cake is the Dessert it should pick up when it is in doubt.

We can always bypass this default, specifying a default in the autowiring:
@Autowired
@Qualifier("cookies")
Dessert dessert;
Now Spring knows that we want cookies and won't pay attention to the primary annotation.
If we don't like the name generated by Spring for the component, that is, the class name but starting lowercase, we can set it as we please, using again the Qualifier annotation, this time on the Component itself. So, for instance, I can qualify Cookies as "yummy" and use this qualification when autowiring to ask Spring to select it.

Reference: Advanced wiring, of Spring in Action, Fourth Edition by Craig Walls. Chapter three, section three, Addressing ambiguity in autowiring.

I pushed the code I have written on this stuff to GitHub. See the package restaurant for the Java classes in the Spring application, and the DessertTest for the JUnit testcase.

No comments:

Post a Comment