Skip to main content

Examples

To represent the main concepts and features of ActiveJ Inject, we've created an example that starts with low-level DI concepts and gradually covers more specific advanced features.

note

To run the examples, you need to clone ActiveJ from GitHub

git clone https://github.com/activej/activej

And import it as a Maven project. Check out tag v4.3. Before running the examples, build the project. These examples are located at activej -> core-inject -> test and named DiFollowUpTest

This example on GitHub

In this example we have a kitchen, where you can automatically create tasty cookies with wonderful ActiveJ Inject. Before we get to cooking, note that there are several POJOs with default constructors marked with @Inject annotation: Kitchen, Sugar, Butter, Flour, Pastry and Cookie.

Manual Bind#

Let's bake a Cookie using ActiveJ Inject in a hardcore way. First of all, we need to provide all the ingredients for the cookie: Sugar, Butter and Flour. Next, there is a recipe for Pastry, which includes ingredients (Sugar, Butter and Flour) we already know how to get. Finally, we can add a recipe of how to bake a Cookie.

graph BT id1(Cookie) --> id2(Pastry) id2 --> Butter id2 --> Flour id2 --> Sugar

It's baking time now! Just create the Injector with all these recipes and ask it for your Cookie instance.

public void transformBindingSnippet() {
Module cookbook = ModuleBuilder.create()
.bind(Sugar.class).to(Sugar::new)
.bind(Butter.class).to(Butter::new)
.bind(Flour.class).to(() -> new Flour("GoodFlour", 100.0f))
.bind(Pastry.class).to(Pastry::new, Sugar.class, Butter.class, Flour.class)
.bind(Cookie.class).to(Cookie::new, Pastry.class)
.transform(0, (bindings, scope, key, binding) ->
binding.onInstance(x -> System.out.println(Instant.now() + " -> " + key)))
.build();
Injector injector = Injector.of(cookbook);
assertEquals("GoodFlour", injector.getInstance(Cookie.class).getPastry().getFlour().getName());
}

Bind Using ModuleBuilder#

This time we will bake a Cookie with a simple DSL. We will bundle our recipes for Sugar, Butter and Flour in a 'cookbook' module.

Instead of creating bindings explicitly and storing them directly in a map, we just bind the recipes in our module and then give it to the injector.

public void moduleBindSnippet() {
Module module = ModuleBuilder.create()
.bind(Sugar.class).to(() -> new Sugar("WhiteSugar", 10.0f))
.bind(Butter.class).to(() -> new Butter("PerfectButter", 20.0f))
.bind(Flour.class).to(() -> new Flour("GoodFlour", 100.0f))
.bind(Pastry.class).to(Pastry::new, Sugar.class, Butter.class, Flour.class)
.bind(Cookie.class).to(Cookie::new, Pastry.class)
.build();
Injector injector = Injector.of(module);
assertEquals("PerfectButter", injector.getInstance(Cookie.class).getPastry().getButter().getName());
}

Bind Using @Provides#

It's time for real Cookie business. Instead of making bindings explicitly, we will use the declarative DSL.

Like in the previous example, we create a cookbook module, but this time all bindings for the ingredients will be created automatically from the provider methods. These methods are marked with the @Provides annotation:

public void provideAnnotationSnippet() {
Module cookbook = new AbstractModule() {
@Provides
Sugar sugar() { return new Sugar("WhiteSugar", 10.f); }
@Provides
Butter butter() { return new Butter("PerfectButter", 20.0f); }
@Provides
Flour flour() { return new Flour("GoodFlour", 100.0f); }
@Provides
Pastry pastry(Sugar sugar, Butter butter, Flour flour) {
return new Pastry(sugar, butter, flour);
}
@Provides
Cookie cookie(Pastry pastry) {
return new Cookie(pastry);
}
};
Injector injector = Injector.of(cookbook);
assertEquals("PerfectButter", injector.getInstance(Cookie.class).getPastry().getButter().getName());
}

Bind Using Instance or Class Scan#

Sometimes it happens that you prepare an injection scheme, but this scheme is not a module. But there is a scan() method which can help you to make a connection between DI entities and your scheme.

public void scanObjectSnippet() {
Module cookbook = ModuleBuilder.create()
.scan(new Object() {
@Provides
Sugar sugar() { return new Sugar("WhiteSugar", 10.f); }
@Provides
Butter butter() { return new Butter("PerfectButter", 20.0f); }
@Provides
Flour flour() { return new Flour("GoodFlour", 100.0f); }
@Provides
Pastry pastry(Sugar sugar, Butter butter, Flour flour) {
return new Pastry(sugar, butter, flour);
}
@Provides
Cookie cookie(Pastry pastry) {
return new Cookie(pastry);
}
})
.build();
Injector injector = Injector.of(cookbook);
assertEquals("PerfectButter", injector.getInstance(Cookie.class).getPastry().getButter().getName());
}

If your class provides a scheme, you can use it easily:

public void scanClassSnippet() {
Module cookbook = ModuleBuilder.create().scan(InjectsDefinition.class).build();
Injector injector = Injector.of(cookbook);
assertEquals("PerfectButter", injector.getInstance(Cookie.class).getPastry().getButter().getName());
}

Automatic Bind Using @Inject#

When we created our POJOs, we've marked their constructors with @Inject annotation:

static class Sugar {
private final String name;
private final float weight;
@Inject
public Sugar() {
this.name = "WhiteSugar";
this.weight = 10.f;
}

If a binding depends on a class that has no known binding, injector will try to automatically generate binding for it. It will search for @Inject annotation on its constructors, static factory methods or the class itself (in this case the default constructor is used) and use them as a factory in generated binding.

Since nothing depends on the Cookie binding, by default no bindings will be generated at all. Here we use a plain bind to tell the injector that we want this binding to be present. Thus the whole tree of bindings it depends on will be generated:

public void injectAnnotationSnippet() {
Module cookbook = ModuleBuilder.create().bind(Cookie.class).build();
Injector injector = Injector.of(cookbook);
assertEquals("WhiteSugar", injector.getInstance(Cookie.class).getPastry().getSugar().getName());
}

Using @Named annotation#

Let's be trendy and bake a sugar-free cookie. In order to do so, along with @Provides annotation, we will also use @Named annotation and provide two different Sugar, Pastry and Cookie factory functions. This approach allows using different instances of the same class. Now we can tell our injector, which of the cookies we need - a usual one or sugar-free.

public void namedAnnotationSnippet() {
Module cookbook = new AbstractModule() {
@Provides
@Named("zerosugar")
Sugar sugar1() { return new Sugar("SugarFree", 0.f); }
@Provides
@Named("normal")
Sugar sugar2() { return new Sugar("WhiteSugar", 10.f); }
@Provides
Butter butter() { return new Butter("PerfectButter", 20.f); }
@Provides
Flour flour() { return new Flour("GoodFlour", 100.f); }
@Provides
@Named("normal")
Pastry pastry1(@Named("normal") Sugar sugar, Butter butter, Flour flour) {
return new Pastry(sugar, butter, flour);
}
@Provides
@Named("zerosugar")
Pastry pastry2(@Named("zerosugar") Sugar sugar, Butter butter, Flour flour) {
return new Pastry(sugar, butter, flour);
}
@Provides
@Named("normal")
Cookie cookie1(@Named("normal") Pastry pastry) {
return new Cookie(pastry);
}
@Provides
@Named("zerosugar")
Cookie cookie2(@Named("zerosugar") Pastry pastry) { return new Cookie(pastry); }
};
Injector injector = Injector.of(cookbook);
float normalWeight = injector.getInstance(Key.ofName(Cookie.class, "normal"))
.getPastry().getSugar().getWeight();
float zerosugarWeight = injector.getInstance(Key.ofName(Cookie.class, "zerosugar"))
.getPastry().getSugar().getWeight();
assertEquals(10.f, normalWeight);
assertEquals(0.f, zerosugarWeight);
}

You can also use ModuleBuilder

public void moduleBuilderWithQualifiedBindsSnippet() {
Module cookbook = ModuleBuilder.create()
.bind(Key.of(Sugar.class, "zerosugar")).to(() -> new Sugar("SugarFree", 0.f))
.bind(Key.of(Sugar.class, "normal")).to(() -> new Sugar("WhiteSugar", 10.f))
.bind(Key.of(Pastry.class, "zerosugar")).to(Pastry::new, Key.of(Sugar.class).qualified("zerosugar"), Key.of(Butter.class), Key.of(Flour.class))
.bind(Key.of(Pastry.class, "normal")).to(Pastry::new, Key.of(Sugar.class).qualified("normal"), Key.of(Butter.class), Key.of(Flour.class))
.bind(Key.of(Cookie.class, "zerosugar")).to(Cookie::new, Key.of(Pastry.class).qualified("zerosugar"))
.bind(Key.of(Cookie.class, "normal")).to(Cookie::new, Key.of(Pastry.class).qualified("normal"))
.build();
Injector injector = Injector.of(cookbook);
float normalWeight = injector.getInstance(Key.of(Cookie.class, "normal"))
.getPastry().getSugar().getWeight();
float zerosugarWeight = injector.getInstance(Key.of(Cookie.class, "zerosugar"))
.getPastry().getSugar().getWeight();
assertEquals(10.f, normalWeight);
assertEquals(0.f, zerosugarWeight);
}

Non-singleton Instances Using Scopes#

Our cookies turned out to be so tasty, that a lot of people want to try them. However, there is a problem, ActiveJ Inject makes instances singleton by default. Yet, we can't sell the same one cookie to all our customers.

Luckily, there is a solution: we can use a custom @ScopeAnnotation @OrderScope to create ORDER_SCOPE scope:

@ScopeAnnotation(threadsafe = false)
@Target({ElementType.METHOD})
@Retention(RUNTIME)
public @interface OrderScope {
}
public static final Scope ORDER_SCOPE = Scope.of(OrderScope.class);

So our cookbook will look as follows:

Module cookbook = ModuleBuilder.create()
.bind(Kitchen.class).to(Kitchen::new)
.bind(Sugar.class).to(Sugar::new).in(OrderScope.class)
.bind(Butter.class).to(Butter::new).in(OrderScope.class)
.bind(Flour.class).to(Flour::new).in(OrderScope.class)
.bind(Pastry.class).to(Pastry::new, Sugar.class, Butter.class, Flour.class).in(OrderScope.class)
.bind(Cookie.class).to(Cookie::new, Pastry.class).in(OrderScope.class)
.build();

In this way, only kitchen will remain singleton:

graph BT subgraph Root Scope Kitchen subgraph N subgraph Order Scope Sugar-->id1(Pastry) Flour-->id1 Butter-->id1 id1-->Cookie end end end

We received 10 orders from our customers, so now we need 10 instances of cookies:

  • First, we inject an instance of Kitchen. Now this instance is stored in the root scope injector.
  • Next, we create 10 subinjectors which enter ORDER_SCOPE.
  • Each subinjector creates only one instance of Cookie and refers to the single Kitchen instance of their parent root scope.
Injector injector = Injector.of(cookbook);
Kitchen kitchen = injector.getInstance(Kitchen.class);
Set<Cookie> cookies = new HashSet<>();
for (int i = 0; i < 10; ++i) {
Injector subinjector = injector.enterScope(ORDER_SCOPE);
assertSame(subinjector.getInstance(Kitchen.class), kitchen);
if (i > 0) assertFalse(cookies.contains(subinjector.getInstance(Cookie.class)));
cookies.add(subinjector.getInstance(Cookie.class));
}
assertEquals(10, cookies.size());

Transforming Binds#

You can configure the process of how your injector gets instances and transform this process. For example, you can simply add some logging by overriding configure method:

public void transformBindingSnippet() {
Module cookbook = ModuleBuilder.create()
.bind(Sugar.class).to(Sugar::new)
.bind(Butter.class).to(Butter::new)
.bind(Flour.class).to(() -> new Flour("GoodFlour", 100.0f))
.bind(Pastry.class).to(Pastry::new, Sugar.class, Butter.class, Flour.class)
.bind(Cookie.class).to(Cookie::new, Pastry.class)
.transform(0, (bindings, scope, key, binding) ->
binding.onInstance(x -> System.out.println(Instant.now() + " -> " + key)))
.build();
Injector injector = Injector.of(cookbook);
assertEquals("GoodFlour", injector.getInstance(Cookie.class).getPastry().getFlour().getName());
}

Now you will receive an output which will represent the time when an instance was created and the instance itself.