Skip to main content

Basics

Key

  • Applications are made up of components, and each component has an internal id called a Key
  • A Key consists of a Type and an optional Object qualifier (useful when you want to distinguish between keys of the same Type):
public class Key<T> {    @NotNull    private final Type type;    @Nullable    private final Object qualifier;}
  • A Key type can be a simple Java Class or a more complex ParameterizedType, for example
  • There are multiple ways to create a Key
    • You can use Key.of(...), Key.ofType(...) static factories
    • You can use an abstract Key class directly by passing type as a generic parameter (useful for parameterized types)
Key<Integer> integerKey = Key.of(Integer.class);Key<List<String>> listOfStringsKey = new Key<List<String>>(){};
note

A Key automatically simplifies any covariant (? extends T) or contravariant (? super T) types to T.

That is why the following is true:

Key<List<? extends String>> covariantKey = new Key<List<? extends String>>(){};Key<List<? super String>> contravariantKey = new Key<List<? super String>>(){};
Key<List<String>> simpleKey = new Key<List<String>>(){};
covariantKey.equals(simpleKey); // truecontravariantKey.equals(simpleKey); // true

Binding

  • Some dependencies may be required to create application components (an individual dependency can be defined by a Key).
  • Dependency Injection takes care of supplying application components with these required dependencies.
  • To do this, we need to specify what it should provide and how to use the provided objects.
  • Therefore, a Binding contains a set of dependencies (Keys) required to create some object. Additionally, a Binding has a compile() method which describes how a binding should be compiled (how exactly the dependencies should be used to create the required object).
public final class Binding<T> {     final Set<Key<?> dependencies;
     public abstract CompiledBinding<T> compile(...);}
  • Binding is like a "recipe" for how to create an instance of a component:
    • dependencies show which ingredients should be used
    • the compile() method describes how to cook them together
  • Bindings are configured in Modules (modules will be explained later).
    • You can either instantiate Bindings directly using static factory methods from the Binding class, or you can use ModuleBuilder DSL to create modules that provide the required bindings
    • Alternatively, you can use the annotation-based approach to define bindings (methods annotated with @Provides annotation)
Module module1 = ModuleBuilder.create()        .bind(String.class).to(integer -> integer.toString(), Integer.class)        .build();
Module module2 = new AbstractModule() {    @Provides    String string(Integer integer) {        return integer.toString();    }};

module1 is equivalent to module2. Both define one single instance of String Binding, which has a single dependency on some Integer instance. A String is created by calling the Integer#toString method on an Integer instance.

Module

A dependency graph is dificult to create directly, so we provide mechanisms for automatic transformation, generation and validation of a graph using a simple but powerful DSL.

All these preprocessing steps are performed at start-up by compiling Modules

Each module exports several user-defined entities that help create a dependency graph:

  • A trie of Bindings itself

  • Multibinders that help resolve duplicate bindings (see example)

  • BindingGenerators, which are used to automatically generate missing dependencies (see example)

  • BindingTransformers that transform certain bindings (see example):

    • To intercept/modify/wrap provided instances
    • To intercept/modify/wrap the dependencies of provided instances
public interface Module {    Trie<Scope, Map<Key<?>, Set<Binding<?>>>> getBindings();    Map<Key<?>, Multibinder<?>> getMultibinders();    Map<KeyPattern<?>, Set<BindingGenerator<?>>> getBindingGenerators();    Map<KeyPattern<?>, Set<BindingTransformer<?>>> getBindingTransformers();}
  • Multibinders, BindingGenerators and BindingTransformers can be created using a clean and extremely simple Java8+ functional DSL
  • The resulting dependency graph is validated - checked for cyclic and missing dependencies, then compiled into a final scope tree and passed to the Injector

It is trivial to manually implement the Module interface, but it is even easier to extend AbstractModule, which supports @Provides method scanning and the DSL for creating/transforming/generating bindings.

Injector

Injector combines multiple modules together, resolves dependencies and allows you to obtain required instances
  • Provides all the required dependencies (injects) for the component by recursively traversing the dependencies graph in a postorder way, and creates them first.
  • Bindings are singletons by default - if an instance was created once, it will not be recreated from scratch again. If you need it for other bindings, Injector will take it from the cache. You don not need to apply any additional annotations for it.
  • To provide the requested key, Injector recursively creates all its dependencies and falls back to injector of its parent scope if no binding is found in its scope.

Scopes

In short, a Scope gives us “local singletons” which live as long as the scope itself. ActiveJ Inject scopes are a bit different from other DI libraries:

  • The internal structure of the Injector is a prefix tree and the prefix is a scope.
  • The identifiers (or prefixes) of the tree are simple annotations.
  • An Injector can enter the scope. This means you create a new Injector and its scope will be set to the one that it enters.
  • This can be done multiple times, so you can have N injectors in certain scope.
public class Injector { ...    final Trie<Scope, ScopeLocalData> scopeDataTree; ...
    public Injector enterScope(Scope scope) {        return new Injector(this, scopeDataTree.get(scope));    } ...}

This article can show you how scopes work.