Skip to content

Release NotesMaven CentralApache License 2.0Test Workflow

Bali DI for Java

Bali DI for Java is a pure annotation processor which generates source code to automate dependency injection. It supports any compiler which is compliant to Java 8 or later.

This project is a sibling of Bali DI for Scala. As a sibling, it is based on the exact same concepts and aims for feature parity.

NOTE

Bali is also an island between Java and Lombok in Indonesia. For disambiguation, the name of this project is "Bali DI", not just "Bali", where DI is an acronym for dependency injection. In code however, the term "DI" is dropped because there is no ambiguity in this context.

Getting Started

Bali DI for Java is implemented as an annotation processor for the Java compiler. If you use Maven, you need to add the following dependency to your project:

xml
<dependency>
    <groupId>global.namespace.bali</groupId>
    <artifactId>bali-java</artifactId>
    <version>0.12.0</version>
    <scope>provided</scope>
</dependency>

You can find the latest release on GitHub.

NOTE

This is a compile-time-only dependency - there is no runtime dependency of your code on Bali DI for Java!

Sample Code

This project uses a modular build. The module bali-java-sample provides lots of sample code showing how to use the annotations and generated source code. The module is not published on Maven Central however - it is only available as source code. You can browse the source code on GitHub or clone this project.

The module is split into different packages with each package representing an individual, self-contained showcase. For example, the package bali.java.sample.greeting showcases a glorified way to produce a simple "Hello world!" message by using different components with dependency injection.

Demo

The following sample app prints the current date and time using an instance of the generic type Supplier<Date> as its clock:

java
package bali.java.sample.genericclock;

import bali.Cache;
import bali.Module;

import java.util.Date;
import java.util.function.Supplier;

import static java.lang.System.out;

@Module
public interface GenericClockApp { // (1)

    @Cache
    Supplier<Date> clock(); // (2)

    Date get(); // (3)

    default void run() {
        out.printf("It is now %s.\n", clock().get());
    }

    static void main(String... args) {
        GenericClockApp$.new$().run(); // (4)
    }
}

In Bali DI, dependencies get resolved rather than injecting them into components by returning them from abstract, parameterless methods. Abstract, parameterless methods are also used to declare components and their dependencies in module interfaces, which is any interface annotated with @Module.

NOTE

This recursive design enables components to become dependencies themselves and thus, the composition of components alias dependencies into larger dependency graphs. It also enables the composition of modules by declaring them as dependencies of other modules, which is a way to structure multi-module apps. Another way is to use (multiple) inheritance from the interface(s) generated by the annotation processor - see below.

Because returning dependencies from methods is inherently lazy the dependency graph can even be cyclic, which is not possible with the common constructor injection idiom alone because that's eager.

In this case, the module interface GenericClockApp (1) declares the method Supplier<Date> clock() (2). The type Supplier<Date> has a single dependency returned from its abstract, parameterless public method Date get(). The code generated by the annotation processor will forward any calls of this method to the method Date get() (3) in the module interface.

When encountering a module interface, the annotation processor generates two additional source files: The first generated source file wires the dependencies declared in the module interface in another interface. The name of the generated interface is the same as the module interface with a single $ character appended. Therefore, let's call this the companion interface:

java
package bali.java.sample.genericclock;

@bali.Generated(
        processor = "bali.java.AnnotationProcessor",
        round = 1,
        timestamp = "2021-08-03T16:45:20.955+01:00",
        version = "0.11.3"
)
public interface GenericClockApp$ extends GenericClockApp { // (1)

    static GenericClockApp new$() { // (2)
        return new GenericClockApp$$() {
        };
    }

    @bali.Cache(bali.CachingStrategy.THREAD_SAFE)
    @Override
    default java.util.function.Supplier<java.util.Date> clock() { // (3)
        return new java.util.function.Supplier<java.util.Date>() {

            @Override
            public java.util.Date get() {
                return GenericClockApp$.this.get();
            }
        };
    }

    @Override
    default java.util.Date get() { // (4)
        return new java.util.Date();
    }
}

In this case, the companion interface GenericClockApp$ (1) extends the module interface GenericClockApp to wire the dependency graph as explained before, in particular the module method Supplier<Date> clock() (3).

When generating the module method Date get() (4), the annotation processor figures that the return type is not abstract, so it simply returns a new instance. If this doesn't work, you can write your own default method instead.

The companion interface also provides the static method constructor GenericClockApp new$(). The return type is the module interface, not the companion interface, in order to promote loose coupling. You can extend the companion interface however, which is useful for advanced scenarios like multi-module apps.

The second generated source file caches the wired dependencies as required by the module in another abstract class. The name of the generated class is the same as the module interface with a double $ character appended. Therefore, let's call this the companion class:

java
package bali.java.sample.genericclock;

@bali.Generated(
        processor = "bali.java.AnnotationProcessor",
        round = 1,
        timestamp = "2021-08-03T16:45:20.956+01:00",
        version = "0.11.3"
)
public abstract class GenericClockApp$$ implements GenericClockApp$ {

    private volatile java.util.function.Supplier<java.util.Date> clock;

    @Override
    public java.util.function.Supplier<java.util.Date> clock() {
        java.util.function.Supplier<java.util.Date> value;
        if (null == (value = this.clock)) {
            synchronized (this) {
                if (null == (value = this.clock)) {
                    this.clock = value = GenericClockApp$.super.clock();
                }
            }
        }
        return value;
    }
}

You may recognize that this code is a blend of the following design patterns:

  • Abstract Factory

    The module interface GenericClockApp is an abstract factory because clients get access to the clock by calling the abstract method Supplier<Date> clock() without knowing the implementation class. The same is true for the method Date get().

  • Factory Method

    The companion interface GenericClockApp$ implements these factory methods to create the singleton clock and the current time.

  • Template Method

    The interface Supplier<T> defines the template method T get() to return a completely generic instance T.

  • Mediator

    The anonymous inner class new Supplier<>() { /* ... */ } uses a reference to its enclosing companion interface GenericClockApp$ in order to obtain the current Date by calling the module method Date get().

If there were more dependencies, these patterns would be repeatedly applied to the abstract methods of these types.

Advanced Features

The sample code also demonstrates the following advanced features:

  • You can cache the return value of any parameterless method in a module interface or a dependency type by applying the @Cache or @CacheNullable annotation to the method.
  • You can select a caching strategy for non-null return values by applying one of @Cache(DISABLED), @Cache(NOT_THREAD_SAFE), @Cache(THREAD_SAFE) or @Cache(THREAD_LOCAL) to the method, where applying @Cache(THREAD_SAFE) can be abbreviated to just @Cache.
  • The same caching strategies are available for nullable return values by using the @CacheNullable annotation.
  • You can select a default caching strategy for all abstract, parameterless methods in a module interface or a dependency type by applying the @Cache annotation to the type itself instead of an individual method. Note that a default caching strategy only applies to abstract methods.
  • You can also declare abstract methods with (possibly generic) parameters in a module interface in order to use their parameters as dependencies of the return value.
  • You can apply the @Make annotation to abstract methods in module interfaces in order to take advantage of loose coupling and hide implementation types. The value of the annotation must be a subclass or subinterface of the method's return class or interface.
  • You can apply the @Lookup annotation to abstract, parameterless methods in dependencies in order to specify the name of a method, field or parameter in a module interface to use as the dependency of the return value.
  • When applying the @Lookup annotation to an abstract, parameterless method in a module interface, the method does not get implemented in the companion interface. To bind these dependencies, you can declare the module as a dependency in another module interface, allowing you to use module composition to structure a large multi-module app.
  • Alternatively, you can extend one or more companion interface(s) in another module interface, allowing you to use module inheritance to structure a large multi-module app.

Released under the Apache License, Version 2.0.