Dependency Injection (DI) is one of the central techniques in today's applications and targets Separation of concerns. Not only makes this testing easier, but you are also not in charge to know how to construct the instance of a requested class. With Java/Jakarta EE we have a specification which (besides other topics) covers this: Contexts and Dependency Injection (short CDI). CDI is also part of the Eclipse MicroProfile project and many other Java/Jakarta EE specifications already use it internally or plan to use it.
Learn more about the Contexts and Dependency Injection (CDI) specification, its annotations and how to use it in this blog post. Please note that I won't cover every aspect of this spec and rather concentrate on the most important parts. For more in-depth knowledge, have a look at the following book.
Specification profile: Contexts and Dependency Injection (CDI)
- Current version: 2.0
- GitHub repository
- Specification homepage
- Basic use case: provide a typesafe dependency injection mechanism
Basic dependency injection with CDI
The main use case for CDI is to provide a typesafe dependency injection mechanism. To make a Java class injectable and managed by the CDI container, you just need a default no-args constructor or a constructor with a @Inject
annotation.
If you use no further annotations, you have to tell CDI to scan your project for all available beans. You can achieve this which a beans.xml
file inside src/main/resources/webapp/WEB-INF
using the bean-discovery-mode
:
1 2 3 4 5 6 | <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="all"> </beans> |
Using this setup, the following BookService
can inject an instance of the IsbnValidator
class:
1 2 3 4 5 | public class IsbnValidator { public boolean validateIsbn(String isbn) { return isbn.replace("-", "").length() < 13); } } |
1 2 3 4 5 6 7 | public class BookService { @Inject private IsbnValidator isbnValidator; // work with the instance } |
You can inject beans via either field-, setter-, constructor-injection or request a bean manually from the CDI runtime:
1 2 3 4 5 | public void storeBook(String bookName, String isbn) { if (CDI.current().select(IsbnValidator.class).get().validateIsbn(isbn)) { logger.info("Store book with name: " + bookName); } } |
Once CDI manages a bean, the instances have a well-defined lifecycle and are bound to a scope. You can interact with the lifecycle of a bean while using e.g. @PostConstruct
or @PreDestroy
. The default scope, if you don't specify any (like in the example above), is the pseudo-scope @Dependent
. With this scope, an instance of your bean is bound to the scope of the bean it gets injected to and won't be shared.
However, you can specify the scope of your bean using the available scopes in CDI:
@RequestScoped
– bound to an HTTP request@SessionScoped
– bound to the HTTP session of a user@ApplicationScoped
– like a Singleton, one instance per application@ConversationScoped
– bound to a conversation context e.g. wizard-like web app
If you need a more dynamic approach for creating a bean that is managed by CDI you can use the @Produces
annotation. This gives you access to the InjectionPoint
which contains metadata about the class who requested an instance:
1 2 3 4 5 6 7 | public class LoggerProducer { @Produces public Logger produceLogger(InjectionPoint injectionPoint) { return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); } } |
Using qualifiers to specify beans
In the previous chapter, we looked at the simplest scenario where we just have one possible bean to inject. Imagine the following scenario where have multiple implementations of an interface:
1 2 3 | public interface BookDistributor { void distributeBook(String bookName); } |
1 2 3 4 5 6 7 | public class BookPlaneDistributor implements BookDistributor { @Override public void distributeBook(String bookName) { System.out.println("Distributing book by plane"); } } |
1 2 3 4 5 6 7 | public class BookShipDistributor implements BookDistributor { @Override public void distributeBook(String bookName) { System.out.println("Distributing book by ship"); } } |
If we now request a bean of the type BookDistributor, which instance do we get? The BookPlaneDistributor
or an instance of BookShipDistributor
?
1 2 3 4 5 6 | public class BookStorage { @Inject // this will fail private BookDistributor bookDistributor; } |
… well, we get nothing but an exception, as the CDI runtime doesn't know which implementation to inject:
1 2 3 4 5 6 | WELD-001409: Ambiguous dependencies for type BookDistributor with qualifiers @Default at injection point [BackedAnnotatedField] @Inject private de.rieckpil.blog.qualifiers.BookStorage.bookDistributors at de.rieckpil.blog.qualifiers.BookStorage.bookDistributors(BookStorage.java:0) Possible dependencies: - Managed Bean [class de.rieckpil.blog.qualifiers.BookShipDistributor] with qualifiers [@Any @Default], - Managed Bean [class de.rieckpil.blog.qualifiers.BookPlaneDistributor] with qualifiers [@Any @Default] |
The stack trace contains an important hint on how to fix such a scenario. If we don't further qualify a bean our beans have the default qualifiers @Any
and @Default
. In the scenario above the BookStorage class requests for a BookDistributor
and also does not specify anything else, meaning it will get the @Default
bean. As there are two beans with this default behavior, dependency injection is not possible (without further adjustments) here.
To fix the error above, we have to introduce qualifiers and further specify which concrete bean we want. A qualifier is a Java annotation including @Qualifier
:
1 2 3 4 5 | @Qualifier @Retention(RUNTIME) @Target({TYPE, METHOD, FIELD, PARAMETER}) public @interface PlaneDistributor { } |
Once we have this annotation, we can use it both for the implementation and at the injection point:
1 2 3 4 5 6 7 8 | @PlaneDistributor public class BookPlaneDistributor implements BookDistributor { @Override public void distributeBook(String bookName) { System.out.println("Distributing book by plane"); } } |
1 2 3 | @Inject @PlaneDistributor private BookDistributor bookPlaneDistributor; |
… and now have a proper injection of our requested bean.
Above all, you can also always request for all instances matching a Java type using the Instance<T>
wrapper class:
1 2 3 4 5 6 7 8 9 | public class BookStorage { @Inject private Instance<BookDistributor> bookDistributors; public void distributeBookToCustomer(String bookName) { bookDistributors.forEach(b -> b.distributeBook(bookName)); } } |
Enrich functionality with decorators & interceptors
With CDI we have two mechanisms to enrich/extend the functionality of a class without changing the implementation: Decorators and Interceptors.
Decorators allow a type-safe way to decorate your actual implementation. Given the following example of an Account
interface and one implementation:
1 2 3 4 | public interface Account { Double getBalance(); void withdrawMoney(Double amount); } |
1 2 3 4 5 6 7 8 9 10 11 12 | public class CustomerAccount implements Account { @Override public Double getBalance() { return 42.0; } @Override public void withdrawMoney(Double amount) { System.out.println("Withdraw money from customer: " + amount); } } |
We can now write a decorator to make special checks if the amount of money to withdraw meets a threshold:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Decorator public abstract class LargeWithdrawDecorator implements Account { @Inject @Delegate private Account account; @Override public void withdrawMoney(Double amount) { if (amount >= 100.0) { System.out.println("A large amount of money gets withdrawn!!!"); // e.g. do further checks } account.withdrawMoney(amount); } } |
With interceptors, we get a more generic approach and don't have the same method signature as the intercepted class, rather an InvocationContext
. This offers more flexibility as we can reuse our interceptor on multiple classes/methods. A lot of cross-cutting logic in Java/Jakarta EE like transactions and security is actually achieved with interceptors. For an example on how to write interceptors, have a look at one of my previous blog posts.
Both decorators and interceptors are inactive by default. To activate them, you either have to specify them in your beans.xml
file:
1 2 3 4 5 6 7 8 9 | <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="all"> <decorators> <class>de.rieckpil.blog.decorators.LargeWithdrawDecorator</class> </decorators> </beans> |
or using the @Priority
annotation and specify the priority value:
1 2 3 4 | @Decorator @Priority(100) public abstract class LargeWithdrawDecorator implements Account { } |
Decouple components with CDI events
Last but not least, the CDI specification provides a sophisticated event notification model. You can use this to decouple your components and use the Observer pattern to notify all listeners once a new event is available.
The event notification in CDI is available both in a synchronous and asynchronous way. The payload of the event can be any Java class and you can use qualifiers to further specialize an event. Firing an event is as simple as the following:
1 2 3 4 5 6 7 8 9 | public class BookRequestPublisher { @Inject private Event<BookRequest> bookRequestEvent; public void publishNewRequest() { this.bookRequestEvent.fire(new BookRequest("MicroProfile 3.0", 1)); } } |
Observing such an event requires the @Observes
annotation on the receiver-side:
1 2 3 4 5 | public class BookRequestListener { public void onBookRequest(@Observes BookRequest bookRequest) { System.out.println("New book request incoming: " + bookRequest.toString()); } } |
Using the asynchronous way, you receive a CompletionStage<T>
as a result and can add further processing steps or handle errors:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class BookRequestPublisher { @Inject private Event<BookRequest> bookRequestEvent; public void publishNewRequest() { this.bookRequestEvent .fireAsync(new BookRequest("MicroProfile 3.0", 1)) .handle((request, error) -> { if (error == null) { System.out.println("Successfully fired async event"); return request; } else { System.out.println("Error occured during async event"); return null; } }) .thenAccept(r -> System.out.println(r)); } } |
Listening to async events requires the @ObservesAsync
annotation instead of @Observes
:
1 2 3 | public void onBookRequestAsync(@ObservesAsync BookRequest bookRequest) { System.out.println("New book request incoming async: " + bookRequest.toString()); } |
YouTube video for using CDI 2.0 specification
Watch the following YouTube video of my Getting started with Eclipse MicroProfile series to see CDI 2.0 in action:
If you are looking for resources to learn more advanced CDI concepts in-depth, have a look at this book.
You can find the source code with further instructions to run this example on GitHub.
Have fun using the CDI specification,
Phil