1. Bootique DI framework
Bootique DI is a lightweight DI engine used to power Bootique application framework. It is a JSR330 (Dependency Injection for Java) compatible implementation.
2. Core concepts
The central source of all configuration in Bootique DI is an instance of Injector. It represents a registry that provides access to configured services. Each injected service is called binding in terms of Bootique DI. To create an Injector
you need to provide a set of configuration units that define services available for injection.
Here is a simple example of Bootique DI usage:
import javax.inject.Inject;
import io.bootique.di.DIBootstrap;
import io.bootique.di.Injector;
public class Application {
public static void main(String[] args) {
Injector injector = DIBootstrap
.injectorBuilder(
binder -> binder.bind(Service.class)
.to(MyService.class) (1)
.inSingletonScope(),
binder -> binder.bind(Worker.class).to(MyWorker.class)
)
.build();
Worker worker = injector.getInstance(Worker.class);
worker.doWork();
}
}
interface Worker {
void doWork();
}
interface Service {
String getInfo();
}
class MyService implements Service {
@Override
public String getInfo() {
return "Hello world!";
}
}
class MyWorker implements Worker {
private Service service;
@Inject
MyWorker(Service service) {
this.service = service;
}
public void doWork() {
System.out.println(service.getInfo());
}
}
1 | here we bind a Service interface to concrete implementation. |
Each service known to Injector
can be injected into field or constructor marked with @javax.inject.Inject
annotation, or into factory methods of modules. Optionally you can inject not only service itself but also a lazy provider of that service. This can be very useful to break circular dependencies between services. Here is an example of Provider
injection:
class MyService implements Service {
private Provider<Worker> workerProvider;
@Inject
MyService(Provider<Worker> workerProvider) {
this.workerProvider = workerProvider;
}
// ...
}
class MyWorker implements Worker {
private Service service;
@Inject
MyWorker(Service service) {
this.service = service;
}
// ...
}
3. BQModule
BQModule
is a single unit of configuration. It provides two ways of configuring services.
3.1. Configure method
BQModule
interface contains single void configure(Binder binder)
method. It can be used to configure simple bindings as mentioned above. Also, it’s useful for extending other Bootique modules. More options discussed in a separate Binder section.
3.2. Custom factory methods
For greater control, you can use custom factory methods to create and configure services. Any module method annotated with @io.bootique.di.Provides
annotation and returning single object will be treated as a factory method. All arguments of such method will be automatically injected, no @Inject
annotation is required.
Here is an example of such methods.
class MyModule implements BQModule {
public void configure(Binder binder) {
}
@Provides
Service createService() {
return new MyService("service");
}
@Provides
Worker createWorker(Service service) {
return new MyWorker(service);
}
}
If you don’t need to provide anything in a configure() method, you can use io.bootique.di.BaseBQModule as a base class and omit it completely. |
4. Binder
Binder provides the API for the module to bind its services to the DI container. Every binding is defined by its key. In a simple case, binding key based on just a java class.
4.1. Service binding
binder.bind(Service.class).to(MyService.class)
using interface is optional, you can directly bind implementation: binder.bind(MyService.class); |
In this case, MyService
will be created by the container. As an alternative, you can provide a manually initialized instance of the MyService
.
binder.bind(Service.class).toInstance(new MyService())
Or you can use a custom factory for that:
binder.bind(Service.class).toProvider(MyServiceProvider.class)
Note that MyServiceProvider
can use injection like any other service managed by the DI container:
class MyServiceProvider implements Provider<Service> {
private SomeOtherService otherService;
@Inject
public MyServiceProvider(SomeOtherService otherService) {
this.otherService = otherService;
}
@Override
public Service get() {
return new MyService(otherService.getSomething());
}
}
Moreover, MyServiceProvider
can be configured as a binding itself:
binder.bind(Service.class).toProvider(MyServiceProvider.class)
binder.bind(MyServiceProvider.class).to(MyServiceProviderImpl.class)
The final option of how you can bind service is via a concrete Provider
implementation:
binder.bind(Service.class).toProviderInstance(new MyServiceProvider())
4.2. Qualifiers
Sometimes, it’s required to provide several variants of the same service. In that case, you need to distinguish them at the injection point. For that you can use qualifiers. A simple qualifier is a service name:
binder.bind(Service.class, "internal").to(MyInternalService.class)
binder.bind(Service.class, "public").to(MyPublicService.class)
To inject this service you need to use javax.inject.Named
annotation at the injection point:
class MyWorker {
@Inject
public MyWorker(@Named("public") Service service) {
}
}
Additionally, you can use custom annotation to achieve this:
@Target({ ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Marker {
}
//...
binder.bind(String.class, Marker.class).toInstance("My string")
class MyWorker {
@Inject
public MyWorker(@Marker String arg) {
}
}
4.3. Generics
Due to generics implementation in Java, you can’t directly use something like Service<String>.class
to bind generic classes. For these cases, Bootique DI provides helper class TypeLiteral
.
binder.bind(Key.get(new TypeLiteral<Service<String>>(){})).to(MyStringService.class);
binder.bind(Key.get(new TypeLiteral<Service<Integer>>(){})).to(MyIntegerService.class);
you don’t need additional qualifiers to inject different versions of the generic class. |
4.4. Collections
Collections can be injected like any other generic class mentioned above. However, there is advanced support for Set
and Map
injection offered by Binder API.
class MyModule implements BQModule {
@Override
public void configure(Binder binder) {
binder.bindSet(Service.class)
.add(DefaultService.class)
.add(Key.get(Service.class, "internal"))
.addProvider(ServiceProvider.class);
}
@Provides
@Singleton
@Named("internal")
Service createInternalService() {
return new InternalService();
}
@Provides
@Singleton
Worker createWorker(Set<Service> services) {
return new MyWorker(services);
}
}
4.5. Optional binding
In normal cases, Bootique DI will throw an exception if injected service is missing. If you want to make some service optional you can create an optional binding for it.
class MyModule implements BQModule {
@Override
public void configure(Binder binder) {
binder.bindOptional(Service.class);
}
@Provides
Worker createWorker(Optional<Service> service) {
return new MyWorker(service.orElse(new DefaultService()));
}
}
5. Injector
Injector
is an entry point for all DI-managed objects. Injector allows getting configured services directly or via a lazy provider. You can get services by their binding keys.
Injector injector = ...;
// get directly by the class
Worker worker = injector.getInstance(Worker.class);
// get by the key
Key<Service<Integer>> serviceKey = Key.get(new TypeLiteral<Service<Integer>>(){});
Provider<Service<Integer>> serviceProvider = injector.getProvider(serviceKey);
Also, the Injector
API contains some useful methods for container retrospection.
-
injector.hasProvider()
method allows checking presence of the binding. -
injector.getKeysByType()
method returns collection of key bound to given type, regardless of additional qualifiers.