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.
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.
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:
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 primaryAsyncServlet:
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():
@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.
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.
InstanceInjector can inject instances into @Inject fields and methods of some already existing objects.
Consider this simple example:
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:
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.
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:
Next, we create AbstractModulecookbook and override its configure() method:
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
You can substitute generate() with the following code:
Now you can create cookbookinjector and get an instance of Cookie<Pastry>:
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.
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:
The output will illustrate that the created instances are different and will look something like this:
InstanceProvider is a version of Injector.getInstance() with a baked-in key. It can be fluently requested by
In the AbstractModule we explicitly add InstanceProvider binding for Integer using bindInstanceProvider helper
method and also provide Integer factory function:
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.
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, cookbookAbstractModule 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
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:
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:
You’ll receive the following output:
Which can be transformed into the following graph:
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.
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:
And in the servlet2:
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".
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