Smart Dependency Injection With Spring: Assignability (Part 2 of 3)
In part two of this three-part series, learn about using inheritance to simplify injecting beans with Spring Framework.
Join the DZone community and get the full member experience.
Join For FreePreface
The Spring Framework is a very powerful framework and provides first class support for dependency injection (DI). This article is the second one in my series dedicated to dependency injection with Spring Framework. My series is split into these three articles:
- Basic usage of DI
- DI with assignability (this article)
- DI with generics
In This Article, You Will Learn:
- How to inject beans by a common interface
- How to inject beans by ancestor (usually abstract) class
- How to inject beans by annotation
Overview
In my previous article, I reviewed the DI basics: different configuration types, injection variants, injection rules, or injected types. Most of it is well known and that article serves just as a summary.
Now, we move the DI usage to the next level with an assignability. We talk about injecting beans with:
- Inheritance: inject beans based on their ancestor
- Implementing an interface: inject beans implementing this interface.
Let's start with an explanation of the used classes in this article.
Domain
Before we start the explanation, we need to know something about the used domain classes. In this article, we use a beverage domain with several classes and interfaces grouped into two parts:
- Carbonated beverages: three classes inheriting from the
AbstractCarbonatedBeverage
class. - Hot beverages: three classes implementing the
HotBeverage
interface.
The relationship between these classes and their relationship is depicted below.
Beverage Interface
The root element in our domain is a Beverage
interface with a single method called getName
. The goal of this method is to return the real name of the beverage.
public interface Beverage {
String getName();
}
AbstractCarbonatedBeverage Class
The first beverage group inherits from AbstractCarbonatedBeverage
abstract class annotated by our custom @Carbonated
annotation.
@Carbonated
public abstract class AbstractCarbonatedBeverage implements Beverage {
}
The @Carbonated
annotation serves as a class marker and has this definition:
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Carbonated {
}
The example of a class implementing the AbstractCarbonatedBeverage
class is the Cola
class defined as:
@Component
public class Cola extends AbstractCarbonatedBeverage {
@Override
public String getName() {
return "Cola";
}
}
Note: The rest of the classes (Beer
and Soda
) are defined in the same way.
HotBeverage Interface
The other part of the beverage domain is grouped around the HotBeverage
interface. This interface serves as a marker (just in a different way) and is defined as:
public interface HotBeverage extends Beverage {
}
The example of the class inheriting from the HotBeverage
abstract class is the Tea
class defined as:
@Component
@Primary
public class Tea implements HotBeverage {
@Override
public String getName() {
return "Tea";
}
}
Note: The Tea
class is the only class having the @Primary
annotation. This means the tea
bean is the default one in the whole beverage group.
You can find all domain classes in my GIT repository.
Injecting Single Bean
As promised in the previous article, we cover all examples of the rules of injection here. All examples below inject beans by the assignable type and not the real type itself (as it is the purpose of this article).
Injection by Name
In this case, we have several beans inheriting from the same class (e.g. AbstractCarbonatedBeverage
class in our case). Therefore the simplest solution is to name the injected property according to the bean qualifier value. We can demonstrate it by injecting the cola
beverage via AbstractCarbonatedBeverage
class:
@Autowired
private AbstractCarbonatedBeverage cola;
We can easily verify the correctness of the injection in the BeverageSingleWiringTest
class. There is the shouldWireBeanByName
method that checks the injected instance and verifies it's really an instance of the Cola
class.
@SpringBootTest(classes = WiringConfig.class)
class BeverageSingleWiringTest {
@Autowired
private AbstractCarbonatedBeverage cola;
@Test
void shouldWireColaByName() {
assertThat(cola.getName()).isEqualTo("Cola");
}
}
Similarly, we can inject beer beverage
:
@Autowired
private AbstractCarbonatedBeverage beer;
@Test
void shouldWireBeerByName() {
assertThat(beer.getName()).isEqualTo("Beer");
}
and soda beverage
.
@Autowired
private AbstractCarbonatedBeverage soda;
@Test
void shouldWireSodaByName() {
assertThat(soda.getName()).isEqualTo("Soda");
}
Note: We are not able to inject any bean implementing the HotBeverage
interface like this, because we have a primary bean defined there (see the next chapter).
Injection of Primary Bean
In some cases, we want to have a default bean for the type (the Beverage
interface here). In our domain, the Tea
class is defined as the primary bean. So we can declare the property type as the Beverage
class and name the property as soda
(even though Spring has soda
bean available) and we still get an instance of the Tea
class.
@Autowired
private Beverage soda;
The obvious test to verify DI behavior represents the shouldWirePrimaryBean
test method looking like this:
@Test
void shouldWirePrimaryBean() {
assertThat(soda.getName()).isEqualTo("Tea");
}
In order to get the desired bean and avoid @Primary
annotation issue, we need to use a qualifier. Let's see this in the following example.
Note: I consider the primary bean to be a little bit tricky. Therefore, I prefer to avoid this approach whenever possible.
Injection by Qualifier
The highest precedence in the injection rule hierarchy is the injection by a qualifier. We have the same case as before (the injection by the Beverage
interface), but this time we also have @Qualifier
annotation with the correct bean name.
@Autowired
@Qualifier("soda")
private Beverage qualifiedBeverage;
Our shouldWireBeanByQualifier
test method verifies, the injected bean is really the soda
bean.
@Test
void shouldWireBeanByQualifier() {
assertThat(qualifiedBeverage.getName()).isEqualTo("Soda");
}
Note: It doesn't matter how we call a property name in our class where we want to inject the bean. The qualifier has the precedence before primary bean or the name rule.
Injection by Annotation
The last option demonstrated here (to narrow the list of available beans to a single instance) is to use some annotation. We have the @Alcoholic
annotation class for this purpose defined like this :
@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Alcoholic {
}
The usage is very simple. We need to specify the @Alcoholic
annotation (at least in the injection point). Here we want to inject the beer
bean as:
@Autowired
@Alcoholic
private Beverage coldBeer;
The verification is similar to all previous test cases above.
@Test
void shouldWireBeanByAnnotation() {
assertThat(coldBeer.getName()).isEqualTo("Beer");
}
Note: You might wonder how it is possible that the @Alcoholic
has a precedence over the primary bean. It's possible due to the @Alcoholic
annotation. As you can see above, this annotation contains @Qualifier
annotation. Therefore, the @Alcoholic
annotation is considered as a qualifier itself.
In the next part, we are going to check the injection of beans.
Injecting Collection of Beans
Now we get to the core part of this series. The injection of the beans is not that common, but it’s very useful in some cases. I use it a lot in order to separate concerns (when I need a "plugin" solution). An example of this approach can be a feature providing some general functionality (e.g. handling generation of XML/JSONs, handling notifications, etc.) on one side and the specific implementation for this feature on the other side (e.g. to generate XML/JSON from a specific entity, process a specific notification event, etc.).
Injecting by Type
As usual, we can inject a collection (i.e. Collection
, List,
or Set
) of beans. An example of this injection is injecting all available beans implementing the Beverage
interface. The shouldWireAllBeverages
test method checks we have got all available beans from the Spring context.
class BeverageCollectionWiringTest {
@Autowired
private Collection<Beverage> beverages;
@Test
void shouldWireAllBeverages() {
assertThat(beverages).hasSize(6);
assertThat(beverages)
.map(Beverage::getName)
.contains("Beer", "Cola", "Soda", "Coffee", "Tea", "Ice Tea");
}
}
We can go further and limit the injected beans to the beans with their class extending AbstractCarbonatedBeverage
class:
@Autowired
private Collection<? extends AbstractCarbonatedBeverage> carbonatedBeverages;
@Test
void shouldWireCarbonatedBeverages() {
assertThat(carbonatedBeverages).hasSize(3);
assertThat(carbonatedBeverages).map(Beverage::getName).contains("Beer", "Cola", "Soda");
}
Note: We can also define the carbonatedBeverages
property as Collection<AbstractCarbonatedBeverage>
. It works both ways.
We can also inject the desired beans as an array. It's just about our preference or a later usage in the code.
@Autowired
private HotBeverage[] hotBeverages;
@Test
void shouldWireHotBeverages() {
assertThat(hotBeverages).hasSize(2);
assertThat(asList(hotBeverages)).map(Beverage::getName).contains("Coffee", "Tea");
}
Injection by Annotation
Our last option here represents injecting beans marked with an annotation (usually the custom one). We have the @Alcoholic
annotation to demonstrate this approach.
@Autowired
@Alcoholic
private Collection<Beverage> alcoholicBeverages;
@Test
void shouldWireAlcoholicBeverages() {
assertThat(alcoholicBeverages).hasSize(1);
assertThat(alcoholicBeverages).map(Beverage::getName).contains("Beer");
}
Note: We have only one alcoholic beverage here, but it works for this as well.
Injecting Map of Beans
Finally, we get to the injection of a map of beans. It works in the exact same way as injecting a collection of beans. The difference is that we also have a bean name (the qualifier of the bean). An example of wiring all carbonated beverages looks like this:
@SpringBootTest(classes = WiringConfig.class)
class BeverageMapWiringTest {
@Autowired
private Map<String, ? extends AbstractCarbonatedBeverage> carbonatedBeverages;
@Test
void shouldWireCarbonatedBeverages() {
assertThat(carbonatedBeverages)
.hasSize(3)
.containsKeys("beer", "cola", "soda");
}
}
Note: There’s no sense to show you all available examples as they are similar to the previous ones. However, you can find all examples in my GitHub repository.
Conclusion
This article has covered DI with assignability. I began with wiring a single bean by interface, abstract class, or any of these combined with an annotation. Next, I presented the very same approach for the collection of beans. In the end, I also provided an example for injecting a map of beans. The complete source code demonstrated above is available in my GitHub repository.
In the next article, I will focus on DI with generics.
Opinions expressed by DZone contributors are their own.
Comments