Dependency Injection Module (Advanced)

In the previous article, we’ve described some common principles of Dependency Injection concepts. In this part, we will proceed with more advanced and complicated DataKernel DI use cases.

You can find full example sources on GitHub.

Rebinding

Consider the module as a black box that has something to import and to export. In cases when you need to change import/export parameters, the rebindImport() and rebindExport() methods are used. Using this scheme, modules can be developed independently of each other, without naming clashes of the dependencies.

  • In this example, we will supply two async servers with the required dependencies using the Module.
public class ModuleRebindExample extends Launcher {
	@Inject
	@Named("server1")
	AsyncHttpServer server1;

	@Inject
	@Named("server2")
	AsyncHttpServer server2;

	static class ServerModule extends AbstractModule {
		@Provides
		AsyncServlet servlet(Config config) {
			String message = config.get("message");
			return request -> HttpResponse.ok200().withPlainText(message);
		}

		@Provides
		@Export
		AsyncHttpServer server(Eventloop eventloop, AsyncServlet servlet, Config config) {
			return AsyncHttpServer.create(eventloop, servlet)
					.withListenPort(config.get(ofInteger(), "port"));
		}
	}

	@Override
	protected Module getModule() {
		return Module.create()
				.install(ServiceGraphModule.create())
				.install(new ServerModule()
						.rebindImport(Config.class, to(rootConfig -> rootConfig.getChild("config1"), Config.class))
						.rebindExport(AsyncHttpServer.class, Key.of(AsyncHttpServer.class, "server1")))
				.install(new ServerModule()
						.rebindImport(Config.class, to(rootConfig -> rootConfig.getChild("config2"), Config.class))
						.rebindExport(AsyncHttpServer.class, Key.of(AsyncHttpServer.class, "server2")))
				.bind(Eventloop.class).to(Eventloop::create)
				.bind(Config.class).toInstance(
						Config.create()
								.with("config1.port", "8080")
								.with("config1.message", "Hello from Server 1")
								.with("config2.port", "8081")
								.with("config2.message", "Hello from Server 2"));
	}

	@Override
	protected void run() throws InterruptedException {
		System.out.println("http://localhost:" + server1.getListenAddresses().get(0).getPort());
		System.out.println("http://localhost:" + server2.getListenAddresses().get(0).getPort());
		awaitShutdown();
	}

	public static void main(String[] args) throws Exception {
		ModuleRebindExample example = new ModuleRebindExample();
		example.launch(args);
	}
  • install() establishes the module by adding all bindings, transformers, generators and multibinders from given modules to this one.
  • In the import of first module, Config.class is an alias that points to the rootConfig.getChild("config1") instance.
  • The name that the module exports (AsyncHttpServer.class) we bind to the Key.of (AsyncHttpServer.class, "server1") - an alias of AsyncHttpServer.class from the first module. Similarly for the second module.
  • From this example, you can learn how to make a personal config for a module, which will bind exactly at the place where this module is connected - like subconfig of some global application config.

DI Multibinder

Multibinder allows to resolve binding conflicts when there are two or more bindings for a single key. In the following example, we will create an HTTP Server which consists of 2 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 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);
}

Export Annotation

@Export is a very important annotation that helps to state explicitly which instances we want to be exported from the module. Provider marked with the @Export annotation gains public visibility outside current module; other instances in the module become private for external modules.

public class ModulesExportExample {
	public static void main(String[] args) {
		Injector injector = Injector.of(new AbstractModule() {
			@Provides
			String secretValue() { return "42"; }

			@Provides
			@Export
			Integer publicValue(String secretValue) { return Integer.parseInt(secretValue); }
		});
		Integer instance = injector.getInstance(Key.of(Integer.class));
		System.out.println(instance);
		String s = injector.getInstanceOrNull(Key.of(String.class));
		System.out.println("String is null : " + (s == null));
	}
}
  • Here we have created an Injector and gave it a module with all the recipes that are needed to be provided.

  • Then we ask it to get the Integer and String instances respectively.

  • Eventually, we will receive the expected result for the first output - because we have marked its creation with an @Export annotation, and a null string for the second output - as the confirmation that the desired value is inaccessible for us.

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(this) - injects the data into our empty instances.

Binding Generators

There are so many different ways to bake cookies with DataKernel DI! 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 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 our 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.datakernel.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().get().getButter().getName());

Instance Factory

If you need a deep copy of an object, your bindings need to depend on the instance factories themselves, and DataKernel DI provides handy tools for such cases. In this example we will create two Integer instances using InstanceFactory.

In the AbstractModule we explicitly add Integer binding using helper method bindInstanceFactory and provide Integer factory function.

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

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

After creating an Injector of the cookbook, we get instance of the Key<InstanceFactory<Integer>>. Now simply use factory.create() to create non-singleton Integer instances:

Injector injector = Injector.of(cookbook);
InstanceFactory<Integer> factory = injector.getInstance(new Key<InstanceFactory<Integer>>() {});
Integer someInt = factory.create();
Integer otherInt = factory.create();
System.out.println("First : " + someInt + ", second one : " + otherInt);

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

First : 699, second one : 130

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 also 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 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.

Inspecting created dependency graph

DataKernel DI provides efficient DSL for inspecting created instances, scopes and dependency graph visualization. In this 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.datakernel.di.core.Injector" [label="Injector"];
	"()->DiDependencyGraphExplore$Pastry" [label="DiDependencyGraphExplore$Pastry"];

	{ rank=same; "()->DiDependencyGraphExplore$Flour" "()->DiDependencyGraphExplore$Sugar" "()->DiDependencyGraphExplore$Butter" "()->io.datakernel.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:

Scope Servlet

The main feature of the ScopeServlet is that it has available Injector in the scope while DI works. In the following example we provide several scopes that Injector will enter.

  • The first one represents a root scope, in which the "root string" message will be simply created since its creation doesn’t require any other dependencies.
  • The the next one - worker scope (a child of the root) asks for the async servlet to be created. Since an AsyncServlet requires another servlet1 and servlet2, Injector will recursively create these dependencies and fall back to the injector of its parent scope.
  • In the last two recipes, async servlets receive Injector as an argument and returns the ScopeServlet. So, while DI works, in the scope of this servlet other instances can be created.
public final class MultithreadedScopeServletExample extends MultithreadedHttpServerLauncher {
	@Provides
	String string() {
		return "root string";
	}

	@Provides
	@Worker
	AsyncServlet servlet(@Named("1") AsyncServlet servlet1, @Named("2") AsyncServlet servlet2) {
		return RoutingServlet.create()
				.map("/", request -> HttpResponse.ok200().withHtml("<a href=\"/first\">first</a><br><a href=\"/second\">second</a>"))
				.map("/first", servlet1)
				.map("/second", servlet2);
	}

	@Provides
	@Worker
	@Named("1")
	AsyncServlet servlet1(Injector injector) {
		return new ScopeServlet(injector) {
			@Provides
			Function<Object[], String> template(String rootString) {
				return args -> String.format(rootString + "\nHello1 from worker server %1$s\n\n%2$s", args);
			}

			@Provides
			@RequestScope
			String content(HttpRequest request, @WorkerId int workerId, Function<Object[], String> template) {
				return template.apply(new Object[]{workerId, request});
			}

			//[START REGION_1]
			@Provides
			@RequestScope
			Promise<HttpResponse> httpResponse(String content) {
				return Promise.of(HttpResponse.ok200().withPlainText(content));
			}
			//[END REGION_1]
		};
	}

	@Provides
	@Worker
	@Named("2")
	AsyncServlet servlet2(Injector injector) {
		return new ScopeServlet(injector) {
			@Provides
			Function<Object[], String> template(String rootString) {
				return args -> String.format(rootString + "\nHello2 from worker server %1$s\n\n%2$s", args);
			}

			@Provides
			@RequestScope
			String content(HttpRequest request, @WorkerId int workerId, Function<Object[], String> template) {
				return template.apply(new Object[]{workerId, request});
			}

			//[START REGION_2]
			@Provides
			@RequestScope
			HttpResponse httpResponse(String content) {
				return HttpResponse.ok200().withPlainText(content);
			}
			//[END REGION_2]
		};
	}

	public static void main(String[] args) throws Exception {
		MultithreadedScopeServletExample example = new MultithreadedScopeServletExample();
		example.launch(args);
	}
}

Promise Generator Module

As you may note, in the previous example we used two slightly different httpResponse() implementations.

Inside the servlet1 we’ve defined it like:

@Provides
@RequestScope
Promise<HttpResponse> httpResponse(String content) {
	return Promise.of(HttpResponse.ok200().withPlainText(content));
}

And in the servlet2:

@Provides
@RequestScope
HttpResponse httpResponse(String content) {
	return HttpResponse.ok200().withPlainText(content);
}

The thing is that the ScopeServlet by default contains PromiseGeneratorModule, which takes responsibility for the Promise creation. Thus, in the second implementation, we can omit wrapping in the Promise.

So you don’t have to care about adding Promises explicitly, just keep in mind that PromiseGeneratorModule can do it for you.

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(
				Module.create()
						.install(OptionalGeneratorModule.create())
						.bind(String.class).toInstance("Hello, World")
						.bind(new Key<Optional<String>>() {}));
		Optional<String> instance = injector.getInstance(new Key<Optional<String>>() {});
		System.out.println(instance);
	}
}

Instance Consumer Module

InstanceConsumerModule allows to transform bindings of all T for which the multibinder Set <Consumer <? extends T >> is set. This Set can accept any T instances after they are created.

  • In the following example, we set up multibinding with @ProvidesIntoSet annotation that provides results as a singleton Set.
  • Mark the needed Consumer<String> and String recipes with the @Named("consumer1") annotation.
  • Then we create module1 and establish it using install() method and multibindings from module.
  • After adding InstanceConsumerModule the bindings of the String will be transformed.
  • So we just can create an injector of module1, ask it to get the instance of a String and merely receive "Hello, World".
@Test
public void consumerTransformerHookupWithNameTest() {
	int[] calls = {0};
	AbstractModule module = new AbstractModule() {
		@Named("consumer1")
		@ProvidesIntoSet
		Consumer<String> consumer() {
			return str -> {
				System.out.println(str);
				System.out.println(++calls[0]);
			};
		}

		@ProvidesIntoSet
		Consumer<String> consumer2() {
			return str -> {
				System.err.println(str);
				System.out.println(++calls[0]);
			};
		}

		@Named("consumer1")
		@Provides
		String string() { return "Hello, World"; }
	};

	Module module1 = Module.create()
			.install(module)
			.install(InstanceConsumerModule.create()
					.withPriority(99));

	Injector injector = Injector.of(module1);
	String instance = injector.getInstance(Key.of(String.class, "consumer1"));
	assertEquals(instance, "Hello, World");
	assertEquals(1, calls[0]);
}