Building a field based dependency injection container
During the last three months, I was mainly working on my internship project using C# and .Net Core. That was my first experience building an entire project using C#.
What I have missed in C#/.Net Core #
As someone who has used Spring Boot a lot, the first thing that I missed was the
@Autowired
annotation used in Spring to wire up dependencies.
After a quick Google search, I failed to find a good alternative to wire up components easily so I decided to “reinvent the wheel” and build my own dependency injection container.
After getting my dependency injection container to work using C# and .Net Core, I was curious to try and build the same container using Java.
TL;DR #
This post explains how I built
armycontainer
, a minimalistic
field based dependency injection container. You may clone the source code and
read it by yourself first, and come back to this post later to find a detailed
explanation for every line of code that I wrote. I recommend using a decent IDE
such as IntelliJ IDEA to navigate the source
code.
Prerequisites #
I assume that the readers of this post have a good understanding of Java and object-oriented programming. Apart from that, I have to define two concepts: inversion of control and dependency injection. Design patterns are beyond the scope of this post, but I highly recommend Refactoring.Guru for a straightforward introduction.
What is inversion of control #
IoC inverts the flow of control as compared to traditional control flow. In IoC, custom-written portions of a computer program receive the flow of control from a generic framework.
- Wikipedia
In short, every time you’re using a framework, you write code that will be called by the framework; that being said, the control is inverted.
What is dependency injection #
Dependency injection is a technique in which an object receives other objects that it depends on.
- Wikipedia
Initial design without dependency injection #
To illustrate in more details, let’s design a simple application composed of two components only:
Logger
: a centralized logging component;HelloWorld
: a component that depends on theLogger
component to log a “Hello world!” message.
If you’re already familiar with Java, you’ll probably write something similar to the next two classes.
First, the Logger
component.
public class LoggerComponent {
public void log(String line) {
System.out.println(line);
}
}
And next, the HelloWorld
component.
public class HelloWorldComponent {
// Drawback 1
// Tight coupling between HelloWorldComponent and LoggerComponent
private LoggerComponent logger;
public HelloWorldComponent() {
// Drawback 2
// "Newing" a LoggerComponent inside the constructor of HelloWorldComponent
this.logger = new LoggerComponent();
}
public void run() {
logger.log("Hello world!");
}
}
The drawbacks #
This initial design is simple, but has two main drawbacks.
First, declaring a field of type LoggerComponent
introduces a tight coupling
between the HelloWorldComponent
module and the LoggerComponent
module.
Therefore, we can’t swap LoggerComponent
with a new AwesomeLoggerComponent
that exhibits the same behavior as LoggerComponent
. This can’t be easily done
with the actual application design, unless we replace every occurrence of
LoggerComponent
with AwesomeLoggerComponent
in every class that uses
LoggerComponent
.
Second, “newing” a LoggerComponent
inside the constructor of
HelloWorldComponent
creates another tight coupling between
HelloWorldComponent
and LoggerComponent
. We need to be able to create a new
Logger
component without specifying the exact class of the object that will be
created.
Fixing the tight coupling #
To fix the first issue with our design, we may introduce a new interface called
LoggerService
to specify the behavior (the service) that a Logger
should
provide to its dependent classes.
public interface LoggerService {
void log(String line);
}
We have to specify that LoggerComponent
implements the newly added interface.
public class LoggerComponent implements LoggerService {
@Override
public void log(String line) {
System.out.println(line);
}
}
As a result of fixing the first design issue, we end up with a
HelloWorldComponent
class similar to the following one.
public class HelloWorldComponent {
// HelloWorldComponent depends now on LoggerService interface
private LoggerService logger;
public HelloWorldComponent() {
// Drawback 2
// "Newing" a LoggerComponent inside the constructor of HelloWorldComponent
this.logger = new LoggerComponent();
}
public void run() {
logger.log("Hello world!");
}
}
LoggerComponent
is a subtype of LoggerService
and then no further changes
have to be made to the constructor (a new instance of LoggerComponent
can be
assigned to a LoggerService
field).
Now, every call to this.logger
methods depends on the LoggerService
abstraction (or contract). We have lowered the coupling between
HelloWorldComponent
and LoggerComponent
.
Controlling objects creation #
However, we still haven’t fixed the issue of swapping two interchangeable
objects without editing the source code of any of the components. The
construction of HelloWorldComponent
is still tightly coupled to the
construction of LoggerComponent
.
This can be fixed either by using a creational pattern or by injecting dependencies into components.
We opt for the second solution, modifying components in such a way that they can receive the objects that they depend on.
Consequently, we add a new constructor to HelloWorldComponent
that accepts an
instance of LoggerComponent
as an argument. This type of dependency injection
is called constructor injection.
public class HelloWorldComponent {
private LoggerService logger;
// A Logger can now be injected into HelloWorldComponent
public HelloWorldComponent(LoggerService logger) {
this.logger = logger;
}
public void run() {
logger.log("Hello world!");
}
}
What’s wrong with constructor injection #
I did a Google search about the recommended way of injecting dependencies. Most bloggers recommend constructor injection. Check this post out for a brief comparaison between field injection and constructor injection.
Nevertheless, field injection is still a good way to write clean and readable code.
First step: Designing services and components #
Services are public interfaces that define the methods that should be exposed by every component.
Components are concrete classes that implement services. Services can be wired into components fields using annotations.
Logger service #
The Logger
service is implemented as follows in armycontainer
.
@Component(defaultImplementation = LoggerComponent.class)
public interface LoggerService {
void log(String line);
}
@Component annotation #
@Component
is a custom Java annotation that does nothing more then adding
metadata to LoggerService
. I recommend reading this
post to learn how to create
your own custom Java annotations.
@Component
is implemented as follows.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
Class defaultImplementation();
}
defaultImplementation parameter #
The parameter defaultImplementation
stores the default concrete class of the
component that should be used to implement this service, unless another
component was explicitly requested from the container.
HelloWorld service #
In the same way, the HelloWorld
service is implemented as follows.
@Component(defaultImplementation = HelloWorldComponent.class)
public interface HelloWorldService extends Runnable {
}
Runnable interface #
Runnable
is an interface that specifies that a component should be
automatically bootstrapped by the container. Every “runnable” component has to
implement the void run()
method.
public interface Runnable {
void run();
}
Replacing constructor injection by field injection #
One further change is needed in the HelloWorld
component. We should replace
the constructor injection by a field injection. The Spring way of doing so
is by adding an @Autowired
annotation to the field of type LoggerService
.
public class HelloWorldComponent implements HelloWorldService {
@Autowired
private LoggerService logger;
@Override
public void run() {
logger.log("Hello world!");
}
}
Implementing @Autowired annotation #
And eventually, we have to implement the @Autowired
annotation.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}
Second step: Designing the public API #
The developers using armycontainer
need to perform two steps only in order to
use the container: create their own components and build a new container. During
this second step, we design an easy way for our developers to build and start a
new container.
Builder pattern #
I opted for the builder pattern as a creational pattern to construct containers. This pattern provides a clean and readable API for developers to build new containers.
Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.
Container public API #
A minimum code to build a new container looks something like this.
ArmyContainer container = ArmyContainer.ContainerBuilder
.initialize()
.build();
ArmyContainer
is the class that represents a container. A ContainerBuilder
is a public static nested class used to build a new container. The reason why
ContainerBuilder
is nested inside ArmyContainer
is that it needs to have
access to its private constructor. ContainerBuilder
is the only class
authorized to instantiate new containers; and thus the constructor has to be
private.
Starting a container #
A method needs to be exposed by the container to let the user start the container.
container.start();
Third step: Implementing ArmyContainer and ContainerBuilder #
A container encapsulates a list of its services and a list of its components.
Components lifetimes #
In more mature dependency injection containers such as the one used in ASP.NET Core, services may have different lifetimes.
Storing services and components #
In armycontainer
, every service is a singleton. Therefore, a Set
is a most
obvious data structure to store the list of services managed by the container.
A component is mapped to every managed service. Object
data type is used to
store components because we don’t have a more specific common ancestor between
components.
public class ArmyContainer {
private Set<Class> managedServices;
private Map<Class, Object> managedComponents;
}
Currently, the private constructor does nothing more than initializing fields.
public class ArmyContainer {
private ArmyContainer() {
managedServices = new HashSet<>();
managedComponents = new HashMap<>();
}
}
Implementing the start() method #
To start the container, we have to run every runnable component. Java provides
the
isAssignableFrom
method to determine if a class or an interface is a superclass or superinterface
of another class or interface. We may use that method to check if a service
implements Runnable
interface.
The start()
method may be implemented as follows.
public class ArmyContainer {
public void start() {
for (Class service : managedServices) {
if (Runnable.class.isAssignableFrom(service)) {
((Runnable) managedComponents.get(service)).run();
}
}
}
}
Implementing a container builder #
The next stage is to implement the ContainerBuilder
. It is a nested static
class because it needs to be accessed without instantiating the outer
ArmyContainer
class.
public class ArmyContainer {
public static class ContainerBuilder {
}
}
When instantiated, ContainerBuilder
only stores an instance of
ArmyContainer
.
public static class ContainerBuilder {
private ArmyContainer container;
}
Some more other steps may be added to the initializing process of a new
ContainerBuilder
. For this reason, it is better to keep the constructor
private and add a public initialize()
method that can be extended more easily.
public static class ContainerBuilder {
private ContainerBuilder() {
container = new ArmyContainer();
}
public static ContainerBuilder initialize() {
return new ContainerBuilder();
}
}
Steps to build a new container #
Building a container may be composed into three internal steps: finding services, constructing components and wiring up components.
Therefore, the build method may be expressed as follows.
public static class ContainerBuilder {
public ArmyContainer build() throws Exception {
findServices()
.constructComponents()
.wireComponents();
return container;
}
}
How to scan for annotations #
In contrast to .Net Core which provides a native way to use reflection, Java
does not provide any such thing except for manually scanning classpath
.
We will use a library called Reflections to find annotated classes and fields. I recommend reading this guide to learn how to use Reflections.
Finding services #
All our services are annotated with @Component
. The findServices()
needs to
scan the classpath for every interface annotated with @Component
and add it to
container.managedServices
.
public static class ContainerBuilder {
private ContainerBuilder findServices() {
Reflections reflections = new Reflections("com.tildehacker.armycontainer");
Set<Class<?>> services = reflections.getTypesAnnotatedWith(Component.class);
for (Class service : services) {
if (service.isInterface()) container.managedServices.add(service);
}
return this;
}
}
Constructing components #
Constructing a component is made in four steps.
First, we need to retrieve the Component
annotation because the default
implementation class is stored in the annotation itself.
Second, we retrieve the default implementation class of the service.
Third, we retrieve the class constructor.
Finally, we create a new instance of the component and store it in the
container.managedComponents
map.
public static class ContainerBuilder {
private ContainerBuilder constructComponents() throws Exception {
for (Class service : container.managedServices) {
Component annotation = (Component) service.getAnnotation(Component.class);
Class componentClass = annotation.defaultImplementation();
Constructor<?> componentConstructor = componentClass.getConstructor();
container.managedComponents.put(service, componentConstructor.newInstance());
}
return this;
}
}
Wiring components #
The last building step is to wire up components. To perform this step we need to resort to Reflections library.
In fact, every component declares its dependencies using the @Autowired
annotation. Therefore, we need to scan the classpath for every field annotated
with @Autowired
and assign the requested dependency to the field.
Let’s break down the implementation of wireComponents()
method.
Finding fields #
First, we need to iterate through all the fields annotated with @Autowired
.
public static class ContainerBuilder {
private ContainerBuilder wireComponents() throws Exception {
Reflections reflections = new Reflections("com.tildehacker.armycontainer", new FieldAnnotationsScanner());
Set<Field> fields = reflections.getFieldsAnnotatedWith(Autowired.class);
for (Field field : fields) {
// Wire up the requested service here.
}
return this;
}
}
Find the declaring service #
Second, for each annotated field we need to find the service to which it
belongs. For instance, if a field is declared inside a HelloWorldComponent
, it
belongs to the HelloWorldService
.
The following code snippet does not take in consideration components that implement many services.
If no declaring service is found, the field is ignored because we can only wire components into other components.
for (Field field : fields) {
Class injectInService = null;
for (Class implemented : field.getDeclaringClass().getInterfaces()) {
if (container.managedServices.contains(implemented)) {
injectInService = implemented;
break;
}
}
if (injectInService == null) {
continue;
}
}
Find which service to inject #
Third, we need to get the service requested to be wired into the field. If the service is not managed by the container, the field is ignored.
for (Field field : fields) {
// ...
Class toInjectService = field.getType();
if (!container.managedServices.contains(toInjectService)) {
continue;
}
// ...
}
Inject components into fields #
Finally, we inject a concrete instance of the requested service into the annotated field. We need to force object accessibility because fields are often private.
for (Field field : fields) {
// ...
Object injectInComponent = container.managedComponents.get(injectInService);
Object toInjectComponent = container.managedComponents.get(toInjectService);
field.setAccessible(true);
field.set(injectInComponent, toInjectComponent);
}