ActiveInject Advanced

In the previous part we explored some common principles of Dependency Injection concepts and ActiveInject features. In this part, we will proceed with more advanced and complex ActiveInject use cases.

You can find full example sources on GitHub.

DI Multibinder

Multibinder resolves binding conflicts if there are two or more bindings for a single key. In the following example, we will create an HTTP Server which consists of two AbstractModules. Both modules include 2 conflicting keys. In the example we’ll use different ways to provide multibinding.

In the first servlet AbstractModule, we provide multibind for the map of String and AsyncServlet by overriding configure() method. We use the multibindToMap method which returns a binding of the map for the provided conflicting binding maps:

static class ServletMapsModule extends AbstractModule {
	@Override
	protected void configure() {
		multibindToMap(String.class, AsyncServlet.class);
	}

	@Provides
	public Map<String, AsyncServlet> firstPage() {
		return singletonMap("/first",
				request -> HttpResponse.ok200().withPlainText("Hello from first page!"));
	}

	@Provides
	public Map<String, AsyncServlet> lastPage() {
		return singletonMap("/last",
				request -> HttpResponse.ok200().withPlainText("Hello from last page!"));
	}

	@ProvidesIntoSet
	AsyncServlet primary(Map<String, AsyncServlet> initializers) {
		RoutingServlet routingServlet = RoutingServlet.create();
		initializers.forEach(routingServlet::map);
		return routingServlet;
	}
}

Note, that primary servlet is marked with @ProvidesIntoSet annotation. We will use this later.

In the second servlet module we’ll automatically set up multibinding with a built-in @ProvidesIntoSet annotation. This annotation provides results as a singleton set, which is then provided to our primary AsyncServlet:

static class ServletInitializersModule extends AbstractModule {
	@ProvidesIntoSet
	public Consumer<RoutingServlet> firstPage() {
		return routingServlet ->
				routingServlet.map("/first",
						request -> HttpResponse.ok200().withPlainText("Hello from first page!"));
	}

	@ProvidesIntoSet
	public Consumer<RoutingServlet> lastPage() {
		return routingServlet ->
				routingServlet.map("/last",
						request -> HttpResponse.ok200().withPlainText("Hello from last page!"));
	}

	@ProvidesIntoSet
	AsyncServlet primary(Set<Consumer<RoutingServlet>> initializers) {
		RoutingServlet routingServlet = RoutingServlet.create();
		initializers.forEach(initializer -> initializer.accept(routingServlet));
		return routingServlet;
	}
}

Finally, we can pull all the modules together. Remember we marked the primary servlets with @ProvidesIntoSet annotation? Now we can simply combine and then compile them using Injector.of():

public static void main(String[] args) {
	Injector injector = Injector.of(new ServletMapsModule(), new ServletInitializersModule());

	String s = injector.getInstance(new Key<Set<AsyncServlet>>() {}).toString();
	System.out.println(s);
}

You can find example sources on GitHub

Instance Injector

InstanceInjector can inject instances into @Inject fields and methods of some already existing objects. Consider 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);
}

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

Here during the internal work of DI, the InstanceInjector in fact gives launcher a hand:

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

Binding Generators

Let’s consider Cookies example form the previous part. This time we have the same POJO ingredients, but now our cookie is a generic Cookie<T> and has a field Optional<T> pastry:

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. BindingGenerator tries to generate a missing dependency binding when Injector compiles the final binding graph trie. You can substitute 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(AssertionError::new).getButter().getName());

You can find example sources on GitHub

Transient Bindings

In case you need a non-singleton object so that each binding receives its own instance, you should use Transient Bindings. Simply add a @Transient annotation:

AbstractModule cookbook = new AbstractModule() {
	@Provides
	@Transient
	Integer giveMe() {
		return random.nextInt(1000);
	}
};

After creating an Injector of the cookbook, each time you use injector.getInstance, you’ll get a new non-singleton Integer instance:

Injector injector = Injector.of(cookbook);
Integer someInt = injector.getInstance(Integer.class);
Integer otherInt = injector.getInstance(Integer.class);
System.out.println("First : " + someInt + ", second  : " + otherInt);

The output will illustrate that the created instances are different and will look something like this:

First : 699, second one : 130

You can find example sources on GitHub

ActiveSpecializer optimization

ActiveSpecializer is a library that optimizes code for JVM. You can simply combine it with ActiveInject and speed up your code by up to 30%. In order to set up ActiveSpecializer, simply use Injector.useSpecializer() before Injector instantiation. This will make JVM perceive the whole tree of bindings equivalent to a manually written fabric method which would be extremely hard to implement and support in real-life projects.

Instance Provider

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

In the AbstractModule we explicitly add InstanceProvider binding for Integer using bindInstanceProvider helper method and provide 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 simply use provider.get() to get a lazy Integer instance.

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

Unlike the previous example, If you call provide.get() several times, you’ll receive the same value.

You can find example sources on GitHub

Inspecting created dependency graph

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

First, let’s overview three Injector methods: peekInstance, hasInstance and getInstance. They allow to inspect 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 getInstance call before
  • hasInstance - checks if an instance of the provided key was created by getInstance 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 provided binding
  • getBindings - returns dependencies of the provided scope (including Injector)

Finally, you can visualize your dependency graph with Graphviz:

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:

You can find example sources on GitHub

Optional Generator Module

OptionalGeneratorModule works similarly to the previous generator module with the difference that OptionalGeneratorModule is responsible for creating Optional objects.

  • In the next example we will need an instance of Optional<String>.
  • The recipe for creation is placed inside the module.
  • install() establishes OptionalGeneratorModule for the further automatic creation of Optional object.
  • Then we just bind the String recipe and in the next line specify the binding to construct an instance for key Optional<String>.
  • Eventually, we just create an injector of module, ask it to get the instance of Optional<String> and receive "Hello, World".
public class OptionalGeneratorModuleExample {
	public static void main(String[] args) {
		Injector injector = Injector.of(ModuleBuilder.create()
				.install(OptionalGeneratorModule.create())
				.bind(String.class).toInstance("Hello, World")
				.bind(new Key<Optional<String>>() {})
				.build());
		Optional<String> instance = injector.getInstance(new Key<Optional<String>>() {});
		System.out.println(instance);
	}
}

You can find example sources on GitHub