Tobias Erdle's Blog

Development Engineer working with different technologies. Former Jakarta MVC & Eclipse Krazo Committer. All views are my own.

Github: erdlet | E-Mail: blog (at) erdlet (punkt) de

Modern web applications with Jakarta EE and Tomcat: Part 3 - Setting up Jakarta Contexts and Dependency Injection

This 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

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.

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