Handling dependency injection using Java 9 modularity
How to decouple your Java code using a mix of dependency injection, encapsulation, and services.
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.
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.
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.
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
.
.
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.