Bali DI Annotations

Bali DI is a code generator for dependency injection.

License

License

GroupId

GroupId

global.namespace.bali
ArtifactId

ArtifactId

bali-annotations
Last Version

Last Version

0.7.2
Release Date

Release Date

Type

Type

jar
Description

Description

Bali DI Annotations
Bali DI is a code generator for dependency injection.

Download bali-annotations

How to add to project

<!-- https://jarcasting.com/artifacts/global.namespace.bali/bali-annotations/ -->
<dependency>
    <groupId>global.namespace.bali</groupId>
    <artifactId>bali-annotations</artifactId>
    <version>0.7.2</version>
</dependency>
// https://jarcasting.com/artifacts/global.namespace.bali/bali-annotations/
implementation 'global.namespace.bali:bali-annotations:0.7.2'
// https://jarcasting.com/artifacts/global.namespace.bali/bali-annotations/
implementation ("global.namespace.bali:bali-annotations:0.7.2")
'global.namespace.bali:bali-annotations:jar:0.7.2'
<dependency org="global.namespace.bali" name="bali-annotations" rev="0.7.2">
  <artifact name="bali-annotations" type="jar" />
</dependency>
@Grapes(
@Grab(group='global.namespace.bali', module='bali-annotations', version='0.7.2')
)
libraryDependencies += "global.namespace.bali" % "bali-annotations" % "0.7.2"
[global.namespace.bali/bali-annotations "0.7.2"]

Dependencies

There are no dependencies for this project. It is a standalone project that does not depend on any other jars.

Project Modules

There are no modules declared in this project.

Release Notes Maven Central Apache License 2.0 Test Workflow

Bali DI

Bali DI is a Java code generator for dependency injection. As a pure annotation processor, it works with any Java compiler which is compliant to Java 8 or later.

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 provides an annotation processor for the Java compiler to do its magic. If you use Maven, you need to add the following dependency to your project:

<dependency>
    <groupId>global.namespace.bali</groupId>
    <artifactId>bali-java</artifactId>
    <!-- see https://github.com/christian-schlichtherle/bali-di/releases/latest -->
    <version>0.10.2</version>
    <scope>provided</scope>
</dependency>

Note that this is a compile-time-only dependency - there is no runtime dependency of your code on Bali DI!

Sample Code

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

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

Please forgive me for not providing real-world samples, but I believe learning a new tool is simpler if you don't have to learn about a specific problem domain first.

Enjoy!

Motivation

Does the world really need yet another tool for an idiom as simple as dependency injection? Well, I've been frustrated with existing popular solutions like Spring, Guice, PicoContainer, Weld/CDI, MacWire etc in various projects for years, so I think the answer is "yes, please"!

Most of these tools try to resolve dependencies at runtime, which is already the first mistake: By deferring the dependency resolution to runtime, you need to compile, unit-test, integration-test, package and start up your app first. But chances are that no matter what your test coverage is, you may still find out that your app is not working properly or not even starting up because something is wrong with its dependency graph.

Furthermore, Spring is well-known for being slow to start-up because it typically scans the entire byte code of your app for its annotations. Of course, you can avoid that entirely by going down XML configuration hell - oh my!

Then again, if something is not working because you forgot to sprinkle your code with a qualifier annotation in order to discriminate two dependencies of the same type but with different semantics (say, a String representing a username and a password - yeah, don't do that), then you may spend a lot of time debugging and analyzing this problem.

By the way, good luck with debugging IOC containers: If your code is not called because you forgot to add an annotation somewhere then debugging it is pointless because... as I said, it's not called.

For worse, dependency injection at runtime is not even type-safe when it comes to generic classes due to type erasure. For example, your component may get a List<String> injected when it actually wants a List<User>.

Last but not least, all of these tools (even Macwire, which is for Scala) support dependency injection into constructors, methods and fields. For Java, this means you either have to write a lot of boiler plate code, for example the constructor plus the fields if you want to use constructor injection (which is the least bad of the three options), or your code gets hardwired to your DI tool by sprinkling it with even more annotations, for example Spring's @Autowired on fields (again - don't do that).

A New Approach

To resolve these issues, Bali DI employs a completely different approach:

  1. Dependency resolution happens at compile-time by an annotation processor which generates Java source code to wire up your components.
  2. Dependencies are made accessible to your components by calling abstract, parameterless methods.
  3. Dependencies are created lazily and - if desired - cached in companion classes, which are auto-generated, abstract classes.

There are many features and benefits resulting from this approach:

Quick trouble-shooting
If a dependency is missing, or doesn't have a compatible type, the compiler will tell you.
Completely type-safe, including generics
A Map<User, List<Order>> never gets assigned to a Map<String, String>.
Dependencies are identified by name, not just type
A String username() is different from a String password() without the need for any qualifier annotations.
All dependency access is inherently lazy
Dependencies are created and, if desired, cached just-in-time when they are accessed without unsolicited overhead in memory size or runtime complexity. If you follow this through, it results in a more responsive behavior of your app than if everything is done in an eager fashion.
Quick application startup
No need to scan the byte code of your app to figure out how to wire your components.
No reflection
It's not just faster without reflection, but also avoids problems with byte code analysis or obfuscation tools.
No runtime dependency
No need to worry about the diamond dependency problem, large dependencies or dependency management at all.
Tailor-made code generation
You can inspect, test and debug the generated source code which exactly matches the requirements of your project without unsolicited overhead in memory size or runtime complexity.

Demo

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

package bali.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.

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 one 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:

package bali.sample.genericclock;

/*
@javax.annotation.Generated(
    comments = "round=1, version=0.9.1-SNAPSHOT",
    date = "2021-02-18T08:22:51.523+01:00",
    value = "bali.java.AnnotationProcessor"
)
*/
public interface GenericClockApp$ extends bali.sample.genericclock.GenericClockApp { // (1)

    static bali.sample.genericclock.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:

package bali.sample.genericclock;

/*
@javax.annotation.Generated(
    comments = "round=1, version=0.9.1-SNAPSHOT",
    date = "2021-02-18T08:22:51.557+01:00",
    value = "bali.java.AnnotationProcessor"
)
*/
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 = bali.sample.genericclock.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.

Versions

Version
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.1
0.2.0
0.1.0