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:

  1. Logger: a centralized logging component;
  2. HelloWorld: a component that depends on the Logger 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);
}

all tags