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