Basics
Key
- Applications consist of components and each component has an inner id called Key
- A
Key
consists of aType
and an optional qualifierObject
(useful when you need to differentiate keys of the sameType
):
public class Key<T> { @NotNull private final Type type; @Nullable private final Object qualifier;}
- A
Key
type can be a simple JavaClass
or a more complexParameterizedType
, 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)
- You can use
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 (Key
s) required for creation of some object. AdditionallyBinding
has acompile()
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
Binding
s are configured inModule
s (modules will be explained later).- You can either instantiate
Binding
s directly using static factory methods fromBinding
class or you can use aModuleBuilder
DSL to construct modules that provide required bindings - Alternatively, you can use annotation-based approach to define bindings (methods annotated with
@Provides
annotation)
- You can either instantiate
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
itselfMultibinders 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 newInjector
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.