Bootique DI - v3.0


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.