Skip to main content

Basics

Key

  • Applications consist of components and each component has an inner id called Key
  • A Key consists of a Type and an optional qualifier Object (useful when you need to differentiate 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

  • Application components can require some dependencies in order to be created (an individual dependency can be defined by a Key).
  • Dependency Injection takes care of supplying application components with these required dependencies.
  • In order to do it, we need to specify what it needs to provide and how to use the provided objects.
  • Therefore, Binding contains a set of dependencies (Keys) required for creation of some object. Additionally Binding has a compile() method that describes how a binding should be compiled (how exactly dependencies should be used to create a required object).
public final class Binding<T> {     final Set<Key<?> dependencies;
     public abstract CompiledBinding<T> compile(...);}
  • Binding is like a "recipe" of how to create an instance of a component:
    • dependencies show what ingredients should be used
    • 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 Binding class or you can use a ModuleBuilder DSL to construct modules that provide required bindings
    • Alternatively, you can use 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 a single String instance Binding that has a single dependency on some Integer instance. A String is created by calling Integer#toString method on an instance of Integer.

Module

Dependency graph is hard to create directly, so we provide automatic graph transformation, generation and validation mechanisms with a simple yet powerful DSL.

All of these preprocessing steps are performed in start-up time by compiling Modules

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

  • A trie of Bindings itself

  • Multibinders that help to resolve duplicate bindings (see example)

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

  • BindingTransformers that transform defined 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 with clean and extremely simple Java8+ functional DSL
  • Resulting dependency graph is validated - checked for cyclic and missing dependencies, then compiled into a final scope tree and passed to Injector

It’s trivial to manually implement the Module interface, but it’s 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 to obtain requried instances
  • Provides all the required dependencies (injects) for the component recursively traversing the dependencies graph in a postorder way and creates them first.
  • Bindings are by default singletons - if an instance was created once, it won't be recreated from scratch again. If it is needed for other bindings, Injector will take it from cache. You don't need to apply any additional annotations for it.
  • To provide the requested key, Injector recursively creates all of its dependencies and falls back to injector of its parent scope if binding in its scope is not found.

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.
  • Injector can enter the scope. This means you create a new Injector and its scope will be set to the one that it's entering.
  • 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.