Dagger2: Dependency Injection at Compile Time

Dagger2: Dependency Injection at Compile Time

Dependency Injection is an important programming paradigm that has been an integral part of Java development for over a decade. We have all used dependency injection in one way or another.

What is Dependency Injection? In simple words, it is “a design pattern that allows an object to supply the dependencies of another object”. Loose coupling between objects, easier testing and less boiler plate code are some of the various benefits it provides.

Although Dependency Injection can be used without any additional framework, using one simplifies the code and makes it cleaner, plus increases productivity.

We, at Jobrapido, have a microservices architecture. There are a number of microservices, responsible for various functions. For one such microservice, we needed a Dependency Injection framework. Not something as big as Spring, but something that provides only dependency injection, giving us the freedom to choose the rest of the application stack independently. We therefore decided to use Dagger 2.

Dagger2 is one of the simplest and most lightweight Dependency Injection frameworks. It offers:

  • Smaller Footprint: the application start-up time is very fast, and it is easy to scale up the application and scale it down.
  • Single Responsibility: it provides only dependency injection.
  • Easy Debugging

Dagger2 is provided by Google and was initially developed to be used with Android applications. But it is not just an Android DI framework and it can also be used with any Java application. For the official documentation, please check https://dagger.dev/.

This article covers the main concepts of Dagger 2 and exhibits Dagger 2 integration with a Java microservice. The sample project is available on GitHub: https://github.com/jobrapido/dagger2-sample.
Follow the README for the project description and to run the application.

Dagger2 Concepts

The Dagger2 library is a compile-time code generator, based on Java’s annotation processor API. It exposes various annotations to implement Dependency Injection, which the Dagger2 processor reads and generates the code for. The predominant annotations are explained below.

Modules & Components

The annotations @Module and @Component are used to perform injection.

  • @Inject marks the objects to be injected. Although constructors, fields and methods can be annotated with @Inject, the best practice is to use it with constructors. @Inject-annotated constructors allow for better class validation and testing.
  • @Provides instructs the processor on how to create the objects to be injected.
  • @Module-annotated classes group together the related @Provides functions.
  • @Components tells the processor what modules should be bundled together.       

In the snippet below, the ClientsModule provides the GenderizeClient and NationalizeClient dependencies,

@Module
public class ClientsModule {

    @Provides
    protected GenderizeClient genderizeClient() {
        return new GenderizeClient();
    }

    @Provides
    protected NationalizeClient nationalizeClient() {
        return new NationalizeClient();
    }
}

which are injected into the PersonInfoController.

public class PersonInfoController {

    private final GenderizeClient genderizeClient;
    private final NationalizeClient nationalizeClient;

    @Inject
    public PersonInfoController(final GenderizeClient genderizeClient,
                                final NationalizeClient nationalizeClient) {
        this.genderizeClient = genderizeClient;
        this.nationalizeClient = nationalizeClient;
    }
}

Graphs

A Graph provides an entry point from the plain Java world into the Dagger2 Dependency Injection world.

The ClientsModule and PersonInfoController are bundled together in a graph to enable generating PersonInfoController instance, injecting dependencies provided by ClientsModule.

@Component(modules = {
        ClientsModule.class
})
public interface ApplicationGraph {

    PersonInfoController personInfoController();

}

Scopes

Scopes in Dagger2 instruct the processor on how to instantiate objects, which influences the instance lifecycle at runtime.

  • Default:
    • a new instance is created every time
    • use Default when there is an explicit need for a new object every time
  • Singleton:
    • instance is created only once and re-used for future injections
    • the scope of the instance for this dependency is throughout the graph
  • Reusable:
    • if the @Reusable-scoped object does not have any dependency, a static instance is created and is used across the application (across multiple graphs)
    • if the @Reusable-scoped object has a dependency, it is treated as Singleton and the scope is limited to a single graph
    • @Reusable can be used for heavy objects in a multi-graph application, so that the same static instance can be shared across the graphs

In the code below, the HttpClient is annotated with @Singleton to ascertain that the same instance is used throughout the graph, but a new instance is created for another graph, whereas the Gson is annotated with @Reusable to indicate that the same instance can be used across graphs.

@Module
public class ThirdPartyModule {

    @Singleton
    @Provides
    public HttpClient client() {
        return HttpClient.newHttpClient();
    }
  
    @Reusable
    @Provides
    public Gson gson() {
        return new GsonBuilder().serializeNulls().create();
    }
}

Note: the above code showcases Dagger2 modularization to separate core modules from third party modules. Refer to the generated code for a better insight.

Injections

Now, after having created and injected objects, let us take a look at how to control the way the objects are in fact injected.

  • Direct:
    • object is created when the graph is initialized
    • by default, all injections are Direct
  • Provider:
    • a new object is created every time get() is called, based on the Scope of the object
  • Lazy:
    • object is created when get() is called the first time, after that the same instance is returned every time
    • use Lazy initialization for an expensive object that may not be used
    • Note: the sample project does not use the Lazy initialization because it is a small application, where all the dependencies are used immediately after injection.

Binding It All

Now, let us tie the various Dagger2 concepts to build an application.

In the main class of our sample project, the Dagger-generated implementation DaggerApplicationGraph of the ApplicationGraph is used to create an instance of the graph by calling the create() method. The graph instance is then used to retrieve a fully injected PersonInfoController instance, which is registered with Undertow as a REST API route handler.
Note: refer to undertow.io for more details on integrating Undertow.

public class SampleApp {

    private static final int HTTP_SERVER_PORT = 8080;
    private static final String HTTP_LISTENING_HOST = "0.0.0.0";

    public static void main(String... args) {
        final ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

        final Undertow server = Undertow
                .builder()
                .addHttpListener(HTTP_SERVER_PORT, HTTP_LISTENING_HOST)
                .setHandler(Handlers
                                .routing()
                                .get("/person", applicationGraph.personInfoController()))
                .build();

        server.start();
    }
}
A call to the API triggers handleRequest in the PersonInfoController.
Note: this is a good place to start the code inspection.

Testing with Dagger2

Dagger2 functions very well with constructor-based Dependency Injection, enabling us to write more testable classes.

Another benefit that Dagger2 provides is the use of modules for integration testing.

In the sample app, we wanted to test our code against the HTTP calls without relying on the external APIs, so we created a fake instance of ApplicationGraph (ApplicationTestGraph) that depends on fake ClientsModule (ClientsTestModule), which makes it easier for us to control the test flow.

Conclusions

Dagger2, despite being a mind-switch from runtime Dependency Injection, really fascinated us. In the beginning, it was a little difficult to understand the Dagger2 concepts (because the official documentation is not very detailed) but eventually, after experimenting with various annotations and going through the generated code, we were able to grasp it well.

Dagger2 is one of the many currently trending open source annotation processor libraries. Even we, at Jobrapido, feel that it is worth giving a chance to compile-time code generation. We have an enormous Java codebase and compile-time code generation can help us shrink it, having it focus only on the business logic.

After having understood and seen its benefits in action, we are planning to continue using Dagger2 and hope this article inspires and helps you to dive into the Dagger2 world, if you have not done so already.

Happy Daggering!

Sanchi Goyal - Software Engineer @ Jobrapido
Stefano Ranzini - Software Engineer @ Jobrapido
Please follow and like us: