Handling dependency injection using Java 9 modularity

How to decouple your Java code using a mix of dependency injection, encapsulation, and services.

By Paul Bakker
January 11, 2018
Fun Fun (source: StockSnap)

In this post we will look at how we can mix the Java 9 module system, dependency injection and services to accomplish decoupling between modules.

It’s almost hard to imagine a Java code base without the use of a dependency injection framework. And for good reason, dependency injection can help a lot with achieving decoupling. Decoupling is about hiding implementations. Decoupling is key to make code maintainable and easy to extend. In Java, this effectively comes down to programming against interfaces instead of concrete types.

Learn faster. Dig deeper. See farther.

Join the O'Reilly online learning platform. Get a free trial today and find answers on the fly, or master something new and useful.

Learn more

Let’s look at a real example. The EasyText application that we use throughout the Java 9 Modularity book is an application that analyzes the complexity of a given text. The application comes in different forms; a CLI and GUI. There are also different algorithms to calculate text complexity. The CLI and GUI are both separate modules, and each analysis algorithm is also a separate module. The CLI and GUI modules obviously depend on the analyzers, but they should only be using the Analyzer interface. The CLI and GUI should not need any knowledge of how the algorithms are implemented.

In the long run this decoupling makes the code easier to maintain because it’s clear what part of the code is doing what, and we can make changes without touching or fully understanding the rest of the system. This is one of the core concepts of modularity. Modular code makes it easier to make changes to individual parts of the system, improving both maintainability and extensibility. Note that we don’t need a module system to design for modularity, but a module system makes this a lot easier.

Whenever we split a system like this into modules, we run into a practical problem. How do we achieve decoupling between the CLI/GUI and analyzers? Because at some point we do need to create an instance of an implementation class. The now almost-classic answer to that is dependency injection, also known as inversion of control. Using dependency injection, our CLI/GUI code would only declare that it needs instances of the Analyzer interface, typically by using annotations. The actual instantiation of the implementation classes and binding to the CLI/GUI code is done by a dependency injection framework. Some of the popular frameworks include Spring and Guice. We’ll use Guice in the remainder of this article, but Java 9 Modularity also contains an extensive example based on Spring.

Dependency injection vs. encapsulation with modules

Java 9 and its module system brings decoupling to a new level. Previously, we could program to interfaces, but we couldn’t really hide our implementation classes. Prior to Java 9, Java didn’t really have the ability to encapsulate classes in a module (or to declare a module for that matter). This changes with Java 9’s module system, but also introduces some new problems for dependency injection frameworks.

Looking at how dependency injection frameworks work internally, the framework needs either deep reflection access or readability on both the implementation classes that need to be injected, and deep reflection access to the classes that it should inject those instance into. When working with modules this system doesn’t work well. Implementation classes should be encapsulated in its module, which means that code outside the module can’t access those classes (even when using reflection). A dependency injection framework is just another module following the same rules of the module system, which means the framework does not have access to those classes. This means we would have to loosen up our encapsulation, which isn’t a good thing.

Let’s look at a typical Guice setup.

  public static void main(String... args) throws IOException {
      Injector injector = Guice.createInjector(
              new ColemanModule(),
              new KincaidModule(),
              new NextgenSyllableCounterModule(),
              new NaiveSyllableCounterModule()
              );
    
      CLI cli = injector.getInstance(CLI.class);
      cli.analyze(args[0]);
   }

In this main method we bootstrap the Guice framework with several Guice modules (not to be confused with Java 9 modules!). Each module provides one or more implementations for interfaces that we want to inject. For example the ColemanModule could look like the following.

public class ColemanModule extends AbstractModule{

    @Override
    protected void configure() {
        Multibinder.newSetBinder(binder(), Analyzer.class)
                .addBinding().to(ColemanAnalyzer.class);
    }
}

Finally, our CLI code is defined with the @Inject annotation to instruct Guice that it should inject dependencies when creating an instance of this class.

public class CLI {

    private final Set<Analyzer> analyzers;

    @Inject
    public CLI(Set<Analyzer> analyzers) {
        this.analyzers = analyzers;
    }
    
    //The rest of the code is not listed here

The main method lives in a module together with the CLI class. The ColemanAnalyzer implementation class and the ColemanModule live together in a module as well. Ideally, we would encapsulate both these classes, because they are both implementation classes. Our CLI module should not depend on them directly. Unfortunately this is not possible. We will have to exports the package containing ColemanModule, because we need it to bootstrap Guice. Second, we will have to opens the package containing ColemanAnalyzer and also the package containing CLI, because Guice needs deep reflection to instantiate the classes. We now have coupling between the CLI module and every analyzer module, as described in the figure below! This is very bad news.

Figure 1. Dependencies between modules, which shows the tight coupling

Are these new problems an indication that modules are hard to work with? Not at all! Modules finally give us a way to encapsulate code, which is a major step forward in what we can do when designing for decoupling. Existing frameworks weren’t designed for this new power, which may require some changes in how we use these frameworks, but we will see there’s an excellent workaround when it comes to Guice.

Before we solve this problem for Guice, let’s take a look what the module system offers itself to work with encapsulated implementation types across modules.

Using services as an alternative to dependency injection

The module system has a built in feature to decouple modules. Services provide a way for a module to declare that it provides an implementation of an interface. Other modules can declare that they use this interface. The module system will pass the implementations to the module that uses the service, without the need for the module to read the implementation type, or even require a dependency on the providing module.

The following is a module descriptor that declares that it provides a service implementation. Note that the Analyzer interface is just a plain Java interface, and the ColemanAnalyzer class is just a plain Java class implementing the Analyzer interface.

module easytext.analysis.coleman {
   requires easytext.analysis.api;

   provides javamodularity.easytext.analysis.api.Analyzer with  
         javamodularity.easytext.analysis.coleman.Coleman;
}

The CLI module must declare that it uses the Analyzer service. It also requires the module that exports the Analyzer interface, but does not require the Coleman module.

module easytext.cli {
   requires easytext.analysis.api;

   uses javamodularity.easytext.analysis.api.Analyzer;
}

In the CLI code we can now use the ServiceLoader API to get the implementations provided by other modules. This can be zero or more implementations, depending on which analyzer modules are installed.

Iterable<Analyzer> analyzers =.    
   ServiceLoader.load(Analyzer.class);

for(Analyzer analyzer: analyzers) { 
  System.out.println(analyzer.getName() + ": " +   
     analyzer.analyze(sentences));
}

The new design based on services is described in the figure below. It shows that
services are a great way to decouple modules, and because this approach is built specifically for the module system, it doesn’t require the same sacrifices to encapsulation as using a dependency injection framework like Guice. Services are not exactly the same as dependency injection, because the ServiceLoader API does a lookup of implementations instead of dependencies being injected, but using this approach solves the same problem. For many applications, services may be a better alternative than relying on external frameworks.

Figure 2. Decoupling using services

What if we still want to use Guice, because we need to work with existing Guice based code, or simply love the declarative nature of dependency injection? Can we align it better with the module system? It turns out that combining services with Guice is actually an elegant solution!

Mixing dependency injection and services

We’ve seen that the main problem with using Guice is that we create direct coupling between the CLI/GUI module and the modules that provide analyzers. The reason is that we need the Guice AbstractModule classes to bootstrap Guice. What if we can eliminate this step by providing the AbstractModule classes as services?

module easytext.algorithm.coleman {
   requires easytext.algorithm.api;
   requires guice;
   requires guice.multibindings;

   provides com.google.inject.AbstractModule with javamodularity.easytext.algorithm.coleman.guice.ColemanModule;

   opens javamodularity.easytext.algorithm.coleman;
}

The implementation package still needs to be open for deep reflection, because Guice needs to be able to instantiate the classes. This isn’t a huge problem, because it doesn’t really introduce any coupling in our code that shouldn’t be there.

On the CLI/GUI side we can now bootstrap Guice by looking for AbstractModule implementations using the ServiceLoader. No more coupling to the implementations modules!

Injector injector = Guice.createInjector(
                ServiceLoader.load(AbstractModule.class));

CLI cli = injector.getInstance(CLI.class);

We’ve briefly discussed how dependency injection helps decoupling code, and how existing dependency injection frameworks such as Guice can be somewhat problematic to use when we encapsulate our code in modules. Services can be a built-in alternative to dependency, and a mix of services and Guice work well with dependency injection with modules, without giving up on encapsulation.

Getting the source code

The complete source code for this example is available on GitHub. There are two branches, one with the use of services as shown in the last example, and one without.

Post topics: Software Engineering
Share: