Skip to main content

ActiveJ Inject under the hood

Implicit bindings

ActiveJ Inject provides several bindings implicitly. Some of these bindings are required for internal use while still can be used by application code.

These bindings are:

  • InstanceInjector binding used for injecting fields
  • InstanceProvider binding used for providing instances similar to Injector#getInstance method
  • OptionalDependency binding, which can be used to determine the presence or absence of a binding
  • A Key<T> binding that you can use to receive an ActiveJ Inject Key for a given type

Instance Injector

The InstanceInjector can inject instances into the @Inject fields and methods of some already existing objects. Let's take a look at this simple example:

@Inject
String message;

@Provides
String message() {
return "Hello, world!";
}

@Override
protected void run() {
System.out.println(message);
}

public static void main(String[] args) throws Exception {
Launcher launcher = new InstanceInjectorExample();
launcher.launch(args);
}

A question that might bother you is how does the Launcher actually know that the message variable contains "Hello, world!" string in order to display it in the run() method?

Here, during the internal work of the DI, the InstanceInjector actually helps the launcher:

private void postInjectInstances(String[] args) {
Injector injector = this.createInjector(args);
InstanceInjector<Launcher> instanceInjector = injector.getInstanceInjector(Launcher.class);
instanceInjector.injectInto(this);
}
  • createInjector produces injector with the given arguments.
  • instanceInjector gets all the required data from the injector.
  • injectInto injects the data into our empty instances.

You can find example sources on GitHub

Instance Provider

InstanceProvider is a version of Injector.getInstance() with a key baked in. It can be freely requested by provider methods.

In AbstractModule, we explicitly add an InstanceProvider binding for Integer using the bindInstanceProvider helper method and provide an Integer factory function:

AbstractModule cookbook = new AbstractModule() {
@Override
protected void configure() {
bindInstanceProvider(Integer.class);
}

@Provides
Integer giveMe() {
return random.nextInt(1000);
}
};

After creating an Injector of the cookbook, we get instance of the Key<InstanceProvider<Integer>>. Now just use the provider.get() method to get a lazy Integer instance.

Injector injector = Injector.of(cookbook);
InstanceProvider<Integer> provider = injector.getInstance(new Key<>() {});
// lazy value get.
Integer someInt = provider.get();
System.out.println(someInt);

Unlike the previous example, if you call the provide.get() method several times, you will receive the same value.

You can find example sources on GitHub

Binding Generators

Let's take a look at the Cookbook example. This time we have the same POJO ingredients, but now our cookie is a generic Cookie<T> and has an Optional<T> pastry field:

static class Cookie<T> {
private final Optional<T> pastry;

@Inject
Cookie(Optional<T> pastry) {
this.pastry = pastry;
}

public Optional<T> getPastry() {
return pastry;
}
}

Next, we create an AbstractModule cookbook and override its configure() method:

AbstractModule cookbook = new AbstractModule() {
@Override
protected void configure() {
// note (1)
generate(Optional.class, (bindings, scope, key) -> {
Binding<Object> binding = bindings.get(key.getTypeParameter(0));
return binding != null ?
binding.mapInstance(Optional::of) :
Binding.toInstance(Optional.empty());
});

bind(new Key<Cookie<Pastry>>() {});
}

generate() adds a BindingGenerator for a given class to this module, in this case it is an Optional. A BindingGenerator tries to generate the missing dependency binding when the Injector compiles the final binding graph trie. You can replace generate() with the following code:

@Provides
<T> Optional<T> pastry(@io.activej.di.annotation.Optional T instance) {
return Optional.ofNullable(instance);
}

Now you can create cookbook injector and get an instance of Cookie<Pastry>:

Injector injector = Injector.of(cookbook);
System.out.println(injector.getInstance(new Key<Cookie<Pastry>>() {}).getPastry().orElseThrow().getButter().getName());

You can find example sources on GitHub

More information on generators can be found here.

Inspecting created dependency graph

ActiveJ Inject provides an efficient DSL for inspecting created instances, scopes, and dependency graph visualization. In this Cookie example, we create, as usual, Sugar, Butter, Flour, Pastry and Cookie POJOs, a cookbook AbstractModule with two scopes (parent scope for Cookie and @OrderScope for ingredients) and a cookbook injector.

Let's first look at three Injector methods: peekInstance, hasInstance and getInstance. They allow you to inspect the created instances:

Cookie cookie1 = injector.peekInstance(Cookie.class);
System.out.println("Instance is present in injector before 'get' : " + injector.hasInstance(Cookie.class));
System.out.println("Instance before get : " + cookie1);

Cookie cookie = injector.getInstance(Cookie.class);

Cookie cookie2 = injector.peekInstance(Cookie.class);
System.out.println("Instance is present in injector after 'get' : " + injector.hasInstance(Cookie.class));
System.out.println("Instance after get : " + cookie2);
System.out.println(); /// created instance check.
System.out.println("Instances are same : " + cookie.equals(cookie2));
  • peekInstance - returns an instance only if it was already created by the getInstance method call before
  • hasInstance - checks if an instance of the provided key was created by the getInstance method call before
  • getInstance - returns an instance of the provided key

Next, we'll explore tools for scopes inspecting:

final Scope ORDER_SCOPE = Scope.of(OrderScope.class);

System.out.println("Parent injector, before entering scope : " + injector);

Injector subInjector = injector.enterScope(ORDER_SCOPE);
System.out.println("Parent injector, after entering scope : " + subInjector.getParent());
System.out.println("Parent injector is 'injector' : " + injector.equals(subInjector.getParent()));
System.out.println("Pastry binding check : " + subInjector.getBinding(Pastry.class));
  • getParent - returns parent scope of the current scope
  • getBinding - returns dependencies of the provided binding
  • getBindings - returns dependencies of the provided scope (including Injector)
Utils.printGraphVizGraph(subInjector.getBindingsTrie());

You'll receive the following output:

digraph {
rankdir=BT;
"()->DiDependencyGraphExplore$Flour" [label="DiDependencyGraphExplore$Flour"];
"()->DiDependencyGraphExplore$Sugar" [label="DiDependencyGraphExplore$Sugar"];
"()->DiDependencyGraphExplore$Butter" [label="DiDependencyGraphExplore$Butter"];
"()->DiDependencyGraphExplore$Cookie" [label="DiDependencyGraphExplore$Cookie"];
"()->io.activej.di.core.Injector" [label="Injector"];
"()->DiDependencyGraphExplore$Pastry" [label="DiDependencyGraphExplore$Pastry"];

{ rank=same; "()->DiDependencyGraphExplore$Flour" "()->DiDependencyGraphExplore$Sugar" "()->DiDependencyGraphExplore$Butter" "()->io.activej.di.core.Injector" }

"()->DiDependencyGraphExplore$Cookie" -> "()->DiDependencyGraphExplore$Pastry";
"()->DiDependencyGraphExplore$Pastry" -> "()->DiDependencyGraphExplore$Butter";
"()->DiDependencyGraphExplore$Pastry" -> "()->DiDependencyGraphExplore$Flour";
"()->DiDependencyGraphExplore$Pastry" -> "()->DiDependencyGraphExplore$Sugar";
}

Which can be transformed into the following graph:

graph BT DiDependencyGraphExplore$Cookie --> id1(DiDependencyGraphExplore$Pastry) id1 --> DiDependencyGraphExplore$Flour id1 --> DiDependencyGraphExplore$Sugar id1 --> DiDependencyGraphExplore$Butter Injector

You can find example sources on GitHub