Smart Dependency Injection With Spring - Generics (Part 3/3)
This article is the last one in my mini-series dedicated to dependency injection with the Spring framework. Using generics 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 powerful framework that provides first-class support for dependency injection (DI). This article is the last one in my mini-series dedicated to dependency injection with Spring Framework.
This series is split into three articles:
- Basic usage of DI
- DI with assignability
- DI with generics (this article)
In this article, you will learn:
- How to inject a single bean defined with a generic class
- How to inject collection or map of beans defined with a generic class
- Some useful tips and issues related to injecting beans with a generic class
Overview
In my previous articles in this series, I reviewed the DI basics and the injection by assignable type. Here, we shed some light on the injection of beans defined by a generic class. For this topic, we need another set of classes. Let's start with them first.
Order Domain
In the last article, we use a beverage domain. This domain re-uses all the classes from the beverage domain. This domain is very simple and straightforward. The relationship between classes and their relationship is depicted below.
BeverageOrder Interface
The root element in the order domain is a BeverageOrder
interface with a single method called takeOrder
. The goal of this method is to accept a beverage bean for order and return some specific message to the handled beverage.
public interface BeverageOrder<T extends Beverage> {
String takeOrder(T beverage);
}
Note: this is a very artificial example serving just for the purpose of this article.
TeaOrder Class
The first class implementing the BeverageOrder
interface is the TeaOrder
class defined as:
@Component
public class TeaOrder implements BeverageOrder<Tea> {
public String takeOrder(Tea beverage) {
return beverage.getName() + " order is taken.";
}
}
Note: the TeaOrder
class uses the Tea
class (from the beverage domain) to accept an order.
Additionally, we can also define beans in JavaConfig as well. The sodaOrder
bean is an example of this approach.
@SpringBootApplication
public class WiringConfig {
@Bean
public BeverageOrder<Soda> sodaOrder() {
return beverage -> beverage.getName() + " is ready to be served.";
}
}
This is all for our order domain. Next, we start with the examples of using these classes for the injection.
Injecting Single Bean
You should be familiar with injecting a single bean now. Here, we just modify our previous examples for injection to use the generic classes.
Note: you can check e.g. these articles if you need to refresh the generics topic:
- https://www.baeldung.com/java-generics
- https://docs.oracle.com/javase/tutorial/java/generics/types.html
Injection by Name
The common way to inject a bean (of course except for the injection by the type) is the injection by the name. Such injection with generics behaves in the same way as before (simple type or assignable type). When you have more beans of the same type (e.g. more classes extending the AbstractCarbonatedBeverage
class) then we can inject the desired bean instance like this (line 5):
@SpringBootTest(classes = WiringConfig.class)
class OrderSingleWiringTest {
@Autowired
private BeverageOrder<? extends AbstractCarbonatedBeverage> sodaOrder;
@Test
void shouldWireBeanByName() {
assertThat(((BeverageOrder<Soda>) sodaOrder).takeOrder(new Soda())).isEqualTo("Soda is ready to be served.");
}
}
As usual, we can verify the injection by checking the returned message from the order (line 9).
Note: we cannot simply inject BeverageOrder<AbstractCarbonatedBeverage>
. More information related to this issue can be found at the end (the final note part).
Injection of Primary Bean
The injection of a bean with the @Primary
annotation works exactly in the same way as in the previous cases (simple type or assignable type), because the generics cannot change the primary bean principle. In other words, we don't need to treat the generics in a special way. You can find the example here.
Injection by Qualifier
As usual, we can inject any bean by its qualifier value (line 2).
@Autowired
@Qualifier("colaOrder")
private BeverageOrder<? extends AbstractCarbonatedBeverage> beverageOrder;
@Test
void shouldWireBeanByQualifier() {
assertThat(((BeverageOrder<Cola>) beverageOrder).takeOrder(new Cola())).isEqualTo("Cola is temporarily not available.");
}
To make it work, we need to tell the correct type to the compiler. Either cast the argument as <T extends AbstractCarbonatedBeverage>
or cast the order bean to the passed bean type. We use the latter option here.
Note: honestly, the generic class has no real value here. We can easily define beverageOrder
like BeverageOrder<?>
or BeverageOrder
with the same result. The qualifier always wins once the type is assignable.
Injection of Generic Type
More interesting is the injection by the generic type (line 2). It works similarly to the qualifier, but we use the generic class instead of the qualifier.
@Autowired
private BeverageOrder<Tea> teaOrder;
Test
void shouldWireBeanByType() {
assertThat(teaOrder.takeOrder(new Tea())).isEqualTo("Tea order is taken.");
}
Injecting Collection of Beans
The injection of a collection of beans defined with the generic class has fewer options than the injection of a collection of assignable beans. The injection by the qualifier, the name, or the primary bean doesn't make sense in this case. We talk about several beans here. Therefore, we cannot apply principles for the single bean.
Moreover, the injection by an annotated type doesn't work. Note: more information about this issue can be found at the end (the final note part). Therefore, we can inject a collection of beans with generics only by their type.
Injecting All Orders
In order to inject all available orders (no matter what beverage they are purposed for), we should inject them as Collection<BeverageOrder<?>>
(line 10) or any other shared ancestor class.
@SpringBootTest(classes = WiringConfig.class)
class OrderCollectionWiringTest {
/**
* The exact name cannot be predicted as it's derived from lambda.
*/
private static final String SODA_ORDER = "WiringConfig$$Lambda$";
@Autowired
private Collection<BeverageOrder<?>> allOrders;
@Test
void shouldWireAllOrders() {
assertThat(allOrders).hasSize(4);
assertThat(allOrders).map(BeverageOrder::getClass).map(Class::getSimpleName)
.contains("BeerOrder", "ColaOrder", "TeaOrder")
.anyMatch(c -> c.startsWith(SODA_ORDER));
}
}
The verification is done as usual by the shouldWireAllOrders
test method. The interesting part here is when we want to check the class name for the bean defined in JavaConfig (sodaOrder
). Such classes don't have a known class in advance. The class is generated at runtime. Therefore, we cannot simply check sodaOrder
class as in other cases. In our case, we use a workaround and simply check sodaOrder
against SODA_ORDER
value (as it's the only bean defined like this).
Note: we can modify the injection also to type Collection<BeverageOrder>
or Collection<BeverageOrder<? extends Beverage>>
, but never as Collection<BeverageOrder<Beverage>>
. The explanation is mentioned at the end of this article.
Injecting Specific Orders
As we have seen many times before, we can reduce the number of injected beans. This time, we want all orders to accept only carbonated beverages. It can be achieved by putting a constraint on the generic class. The example below uses ? extends AbstractCarbonatedBeverage
type to inject all beans handling order for a carbonated beverage (line 2).
@Autowired
private BeverageOrder<? extends AbstractCarbonatedBeverage>[] carbonatedOrders;
@Autowired
private BeverageOrder<Beer> beerOrder;
@Autowired
private BeverageOrder<Cola> colaOrder;
@Autowired
private BeverageOrder<Soda> sodaOrder;
@Test
void shouldWireAllCarnonatedOrders() {
assertThat(carbonatedOrders)
.hasSize(3)
.contains(beerOrder, colaOrder, sodaOrder);
}
Note: the verification is done against whole beans this time. Therefore we need to inject all expected beans by their type (see lines 4-11).
Injecting Map of Beans
The last example shows the injection of a map of beans defined with a generic class. It works in the same way as injecting a collection of beans. An example of wiring all carbonated beverages can look like this (line 2):
@Autowired
private Map<String, BeverageOrder<? extends AbstractCarbonatedBeverage>> carbonatedOrders;
@Test
void shouldWireAllOrders() {
assertThat(carbonatedOrders)
.hasSize(3)
.containsKeys("beerOrder", "colaOrder", "sodaOrder");
}
Final Notes
Now, we know how to use the dependency injection with the generics in Spring Framework. However, there are a couple of issues we should be aware of.
Using the Raw Type
We need to define the injected type precisely. The goal is to allow Spring to find the exact match of our type and the existing beans in its context. Do you think the injection of the BeverageOrder<AbstractCarbonatedBeverage>
would work?
@Autowired
private BeverageOrder<AbstractCarbonatedBeverage> sodaOrder;
Unfortunately no, because the Spring content doesn't have any bean of the type AbstractCarbonatedBeverage
. There are several descendants, but none of them is defined just but this type. Therefore, we get NoSuchBeanDefinitionException
exception when we try to inject this type.
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.github.aha.sat.core.wiring.order.BeverageOrder<com.github.aha.sat.core.wiring.beverage.AbstractCarbonatedBeverage>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1790)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1346)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1300)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:657)
... 74 common frames omitted
Casting
When we inject some beans with the generic type using wildcard (?
), we obviously want to use them in some general way. Otherwise, we can inject them with the specific type, cannot we?
For example, when we use the extended type <? extends AbstractCarbonatedBeverage>
then we want to trigger some of its methods then we need to take care of the casting. It's because the compiler is not capable of doing it on its own. To make it work, we need to specify the correct type to the compiler. Either cast the argument as <T extends AbstractCarbonatedBeverage>
(argument side) or cast the processing bean to accept the passed bean type (provider side).
Argument Side
In order to cast our bean to the desired type, we can introduce e.g. castType
method (lines 9-11) returning the expected T extends AbstractCarbonatedBeverage
type. This makes the compiler happy.
@Autowired
private BeverageOrder<? extends AbstractCarbonatedBeverage> sodaOrder;
@Test
void shouldWireBeanByName() {
assertThat(sodaOrder.takeOrder(castType(new Soda()))).isEqualTo("Soda is ready to be served.");
}
<T extends AbstractCarbonatedBeverage> T castType(AbstractCarbonatedBeverage b) {
return (T) b;
}
Provider Side
An example of the provider side example was used in our examples above (in order to make our examples simple). Here, we need to cast sodaOrder
bean to accept the BeverageOrder<Soda>
type.
assertThat(((BeverageOrder<Soda>) sodaOrder).takeOrder(new Soda())).isEqualTo("Soda is ready to be served.");
Injection by Annotation
In several cases, we could see the injection based on an annotation specified in the injected bean class. Unfortunately, it doesn't work with generics. Please, let me know if you have any working examples.
@Autowired
private BeverageOrder<@Alcoholic ? extends AbstractCarbonatedBeverage> beerOrderBean;
Note: the property name beerOrderBean
is intentional. If we use the name beerOrder
then we will get the correct instance injected by the name.
Conclusion
This article has covered DI with generics. I began the article with the demonstration of different options to inject a single bean with a generic class. Next, I explained the injection of collection and map of beans with generics. In the end, I provided some hints and known issues related to DI with generics.
This is the last article in my mini-series dedicated to the smart dependency injection with Spring Framework. I hope you enjoyed it and found at least some helpful information. As usual, the complete source code presented above is available in my GitHub repository.
Opinions expressed by DZone contributors are their own.
Comments