Skip to main content

Cookbook example

To introduce the basic concepts and features of ActiveJ Inject, we have created an example that starts with the 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 v6.0-beta2. 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 delicious cookies using the ActiveJ Inject. Before we start 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 cookies: Sugar, Butter and Flour. Next, there is th Pastry recipe, which includes the ingredients (Sugar, Butter and Flour) that we already know how to get. Finally, we can add a recipe for how to bake a Cookie.

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

It's baking time! Just create the Injector with all these recipes and ask for an instance of a Cookie.

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(Object.class, (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).pastry().flour().name());
}

Bind Using ModuleBuilder

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

Instead of explicitly creating bindings and storing them directly in a map, we will just bind the recipes in our module and then pass 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).pastry().butter().name());
}

Bind Using @Provides

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

As in the previous example, we will create a cookbook module, but this time all bindings for the ingredients will be created automatically from the provider methods. These methods are annotated 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).pastry().butter().name());
}

Bind Using Instance or Class Scan

Sometimes it happens that you have prepared an injection scheme, but that scheme is not a module. Fortunately, there is a scan() method that can help you 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).pastry().butter().name());
}

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

public void scanClassSnippet() {
Module cookbook = ModuleBuilder.create().scan(InjectsDefinition.class).build();

Injector injector = Injector.of(cookbook);
assertEquals("PerfectButter", injector.getInstance(Cookie.class).pastry().butter().name());
}

Automatic Bind Using @Inject

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

record Sugar(String name, float weight) {
@Inject
public Sugar() {
this("WhiteSugar", 10.f);
}
}

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

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

public void injectAnnotationSnippet() {
Module cookbook = ModuleBuilder.create().bind(Cookie.class).build();

Injector injector = Injector.of(cookbook);
assertEquals("WhiteSugar", injector.getInstance(Cookie.class).pastry().sugar().name());
}

Using @Named annotation

Let's be trendy and bake a sugar-free cookie. To do this, along with the @Provides annotation, we will also use the @Named annotation and provide two different Sugar, Pastry and Cookie factory functions. This approach allows us to use different instances of the same class. Now we can tell our injector, which of cookie we want, a regular 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.of(Cookie.class, "normal"))
.pastry().sugar().weight();
float zerosugarWeight = injector.getInstance(Key.of(Cookie.class, "zerosugar"))
.pastry().sugar().weight();

assertEquals(10.f, normalWeight, 0.0f);
assertEquals(0.f, zerosugarWeight, 0.0f);
}

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"))
.pastry().sugar().weight();
float zerosugarWeight = injector.getInstance(Key.of(Cookie.class, "zerosugar"))
.pastry().sugar().weight();

assertEquals(10.f, normalWeight, 0.0f);
assertEquals(0.f, zerosugarWeight, 0.0f);
}

Non-singleton Instances Using Scopes

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

Fortunately, there is a solution: we can use the custom @ScopeAnnotation @OrderScope to create an 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. This instance is now stored in the root scope injector.
  • Then we create 10 subinjectors which enter ORDER_SCOPE.
  • Each subinjector creates only one Cookie instance and refers to the single Kitchen instance of its parent root scope.
Injector injector = Injector.of(cookbook);
Kitchen kitchen = injector.getInstance(Kitchen.class);
Set<Cookie> cookies = Collections.newSetFromMap(new IdentityHashMap<>());
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 customize the process of how your injector gets instances and transform that process. For example, you can simply add logging by overriding the 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(Object.class, (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).pastry().flour().name());
}

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