Modern web applications with Jakarta EE and Tomcat: Part 3 - Setting up Jakarta Contexts and Dependency Injection
19 Jul 2022 (last modified 26 Jul 2022) - Tobias ErdleThis article is the third part of the series "Modern web applications with Jakarta EE and Tomcat" and demonstrates setting up Jakarta Contexts and Dependency Injection and using it in conjunction with Jakarta RESTful Web Services and Jersey.
Topics covered during this article series
- Setting up Jakarta REST with Jersey
- Setting up Jakarta Bean Validation
- Setting up Jakarta Contexts and Dependency Injection (this article)
- Setting up Jakarta Persistence
- Setting up Jakarta MVC for server-side rendered frontends
- Content Negotiation with Jakarta REST
- Unit and integration testing
Jakarta Contexts and Dependency Injection (CDI) in a nutshell
With the help of Jakarta Contexts and Dependency Injection (CDI), dependencies inside the application code can be decoupled and thus lead to more maintainable and testable code. For this CDI uses the principle of Dependency Injection (DI).
Since the origin of the name component "... and Dependency Injection" is clarified, it should still be explained what a Context describes in this case. The specification document describes a Context as an object which ...determines the lifecycle and visbility of instances of all beans with that scope.
This means that there can be multiple contexts in each CDI container and they control the lifecycle of the beans they manage.
For a bean to be assigned to such a context, it needs a Scope. CDI comes with several standard scopes, whose behavior in web applications is briefly presented here.
- @RequestScoped creates a bean which exists during an HTTP request
- @SessionScoped creates a bean which lives during an HTTP session
- @ApplicationScoped creates a bean which has only one instance per CDI container (= Singleton)
- @ConversationScoped creates a bean which spans over a conversation. This scope is helpful for e. g. wizards or multi-step-requests like checkouts.
Now that the basic terminology has been clarified, we can start integrating CDI into the sample application.
For further information, please refer to the Jakarta Contexts and Dependency Injection specification (here for CDI 3.0)
Integrating Jakarta Contexts and Dependency Injection (CDI)
This chapter is going to show how CDI is configured for the usage with Jersey as Jakarta RESTful Web Services implementation. As usual, we start by specifying the correct dependencies in Maven.
Adding the necessary dependencies
To integrate CDI into an application, the CDI API and a conforming implementation, here in the example the reference implementation Weld, are included first.
This means that CDI is available in the application, but it cannot yet be used in conjunction with Jersey. This is due to the fact that Jersey delivers its own dependency injection mechanism with HK2, which must be 'leveraged' via an extension. For this the additional dependency org.glassfish.jersey.ext.cdi:jersey-cdi1x-servlet
is inserted.
<!-- CDI -->
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.jboss.weld.servlet</groupId>
<artifactId>weld-servlet-core</artifactId>
<version>4.0.2.Final</version>
</dependency>
<!-- Jersey Extension -->
<dependency>
<groupId>org.glassfish.jersey.ext.cdi</groupId>
<artifactId>jersey-cdi1x-servlet</artifactId>
<version>3.0.4</version>
</dependency>
19-Jul-2022 12:52:50.005 INFO [RMI TCP Connection(2)-127.0.0.1] org.jboss.weld.environment.servlet.EnhancedListener.onStartup WELD-ENV-001008: Initialize Weld using ServletContainerInitializer
19-Jul-2022 12:52:50.030 INFO [RMI TCP Connection(2)-127.0.0.1] org.jboss.weld.bootstrap.WeldStartup.<clinit> WELD-000900: 4.0.2 (Final)
19-Jul-2022 12:52:50.291 INFO [RMI TCP Connection(2)-127.0.0.1] org.jboss.weld.bootstrap.WeldStartup.startContainer WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
19-Jul-2022 12:52:50.408 INFO [RMI TCP Connection(2)-127.0.0.1] org.jboss.weld.event.ExtensionObserverMethodImpl.checkRequiredTypeAnnotations WELD-000411: Observer method [BackedAnnotatedMethod] public org.glassfish.jersey.ext.cdi1x.internal.ProcessAllAnnotatedTypes.processAnnotatedType(@Observes ProcessAnnotatedType<?>, BeanManager) receives events for all annotated types. Consider restricting events using @WithAnnotations or a generic type with bounds.
19-Jul-2022 12:52:50.470 INFO [RMI TCP Connection(2)-127.0.0.1] org.jboss.weld.environment.tomcat.TomcatContainer.initialize WELD-ENV-001100: Tomcat 7+ detected, CDI injection will be available in Servlets, Filters and Listeners.
If you start the test application you will see in the log that Weld starts and Jerseys CDI Extension is registered.
In addition, the file src/main/webapp/WEB-INF/beans.xml
must now be created and filled according to the following listing. This instructs CDI to include all with beans with a CDI scope in the respective context. As an alternative, bean-discovery-mode=all
can be used, which is not recommended, as this registers all beans as CDI beans, regardless if annotated or not. Not being aware of this, it can be really hard to debug issues when using producers and alternative implementations.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd"
version="3.0"
bean-discovery-mode="annotated">
</beans>
Hint: If this file is missing, the following error is generated:
19-Jul-2022 13:17:04.890 SEVERE [RMI TCP Connection(7)-127.0.0.1] org.apache.catalina.core.StandardContext.loadOnStartup Servlet [de.erdlet.blogging.blog.JakartaRestApplication] in web application [/blogging-app] threw load() exception
org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type PostService with qualifiers @Default
at injection point [BackedAnnotatedField] @Inject de.erdlet.blogging.blog.api.PostsResource.postService
at de.erdlet.blogging.blog.api.PostsResource.postService(PostsResource.java:0)
In order to use CDI in the application, the corresponding classes must now be provided with a scope so that they are assigned to a context.
Annotate the dependencies
In the example, the PostsResource
and the PostService
are now to be managed by CDI. For this purpose, the PostResource
is annotated with @RequestScoped
and the PostService
with @ApplicationScoped
. Additionally, the PostService
is now going to be_injected_ by CDI and not, as before, received by a hard coded method call inside the resource method. Please note, that the GET
method just uses some workaround for demonstration purposes until a later article where Content Negotiation and working with content types is described.
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
@Path("posts")
@RequestScoped
public class PostsResource {
@Inject
PostService postService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public Response index() {
return Response.ok(toTextResult(postService.findAll())).build();
}
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response create(@Valid @BeanParam final PostDTO dto) {
final var post = postService.create(dto);
final var location = URI.create("/blogging-app/posts/" + post.getId());
return Response.created(location).build();
}
// Helper until Content Negotiation is introduced
private String toTextResult(final List<Post> posts) {
return posts.stream().map(Post::toString).collect(Collectors.joining(", "));
}
}
In the PostService
the keyword final
is additionally removed in the class definition, as well as the method getInstance()
and the attribute INSTANCE
, so it looks like this. Please note that, until Jakarta Persistence is added to the application in an upcoming article, the service contains just a list with some dummy values.
@ApplicationScoped
public class PostService {
private List<Post> POSTS = new ArrayList<>();
{
POSTS.add(new Post("My first post!", "This is my first post", LocalDateTime.now()));
POSTS.add(new Post("How to bake a cake", "Baking a cake is simple...", LocalDateTime.now().minusDays(3)));
}
public Optional<Post> findById(final UUID id) {
return POSTS.stream().filter(post -> post.getId().equals(id)).findFirst();
}
public List<Post> findAll() {
return List.copyOf(POSTS);
}
public Post create(final PostDTO dto) {
final var post = new Post(dto.getTitle(), dto.getContent(), LocalDateTime.now());
POSTS.add(post);
return post;
}
}
Verify injection by calling a GET resource
After the above steps have been performed, the GET
or POST
interface can now be used to test whether the CDI bean is really being used. Here the response after a request on the GET
resource has been performed.
GET http://localhost:8080/blogging-app/posts
##############################################################
http://localhost:8080/blogging-app/posts
HTTP/1.1 200
Content-Type: text/plain
Content-Length: 301
Date: Tue, 19 Jul 2022 11:30:30 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Post{id=00ec5ce0-f972-445a-8c5d-c168f6f49052, title='My first post!', content='This is my first post', publishedAt=2022-07-19T13:26:08.637429024}, Post{id=3b1ca252-df83-473f-b3d4-62a3d2c43fdc, title='How to bake a cake', content='Baking a cake is simple...', publishedAt=2022-07-16T13:26:08.637463512}
As you can see, the two records from the dummy list were output as simple text representation. If you now want to test the POST
interface as well, you can execute the following request and look at the extended result when calling the GET
interface again.
POST http://localhost:8080/blogging-app/posts
Content-Type: application/x-www-form-urlencoded
content=MyFirstContent&title=Title from POST
##############################################################
http://localhost:8080/blogging-app/posts
HTTP/1.1 200
Content-Type: text/plain
Content-Length: 442
Date: Tue, 19 Jul 2022 11:33:27 GMT
Keep-Alive: timeout=20
Connection: keep-alive
Post{id=00ec5ce0-f972-445a-8c5d-c168f6f49052, title='My first post!', content='This is my first post', publishedAt=2022-07-19T13:26:08.637429024}, Post{id=3b1ca252-df83-473f-b3d4-62a3d2c43fdc, title='How to bake a cake', content='Baking a cake is simple...', publishedAt=2022-07-16T13:26:08.637463512}, Post{id=29cf357d-d7d5-4e56-b577-1316eaa34416, title='Title from POST', content='MyFirstContent', publishedAt=2022-07-19T13:33:12.001235792}
Conclusion
Again, thanks to the Jersey Extension for CDI it is easy to integrate this specification in an standalone web application. Using CDI enables developers to create loose coupled code with high maintainability regarding inter-class dependencies.
Upcoming topic: Setting up Jakarta Persistence and database migrations
The next article will cover how to set up Jakarta Persistence and handling database access using this specification. Also it'll cover how to add Flyway as a database migration tool for managing database versions.
Resources
Edits
- 26 Juli 2022: Reference to database migration tool removed