tl;dr minimal example:
class CoffeeApp {
@Component interface CoffeeComponent {
CoffeeMaker coffeeMaker();
@Component.Builder interface Builder {
Builder logLevel(String logLevel);
CoffeeComponent buildComponent();
}
}
interface Logger {
void log(String msg);
}
static class CoffeeMaker {
private final Logger logger;
@Inject CoffeeMaker(Logger logger) {
this.logger = logger;
}
void brew() {
logger.log("~ ~ ~ heating ~ ~ ~");
logger.log("=> => pumping => =>");
logger.log(" [_]P coffee! [_]P ");
}
}
@Inject static Logger createLogger(String level) {
return msg -> System.out.println(level + " " + msg);
}
}
This dependency injector uses the following annotations:
@Inject
declares an injection point. It can be a constructor or a static method in the bean class. It can also be a static method in the component class.@Qualifier
and its default implementation@Named
.- And of course,
@Component
,@Component.Factory
and@Component.Builder
.
Note this is not a complete implementation of javax.inject
or jakarta.inject
, because:
Instead there's the following rule:
If two beans of the same type and same qualifier are injected by the same component, then they are the same instance.
Intuitively this means the same bean instance is injected everywhere (unless you're using qualifiers, or inject a provider). So everything is a "singleton". In the example above, if multiple beans would request the logger, they would all get the same logger instance.
If you want to re-use a bean instance across multiple components, or multiple instances of the same component, use a @Factory
or a @Builder
to pass it around.
Components will prefer using an existing bean instance over creating a new one.
If you inject Provider<TheBean>
, rather than TheBean
directly, calling provider.get()
will create a fresh bean instance every time.
If you want create a component where some beans are swapped for mock instances, use @Component(mockBuilder = true)
.
A static mockBuilder
method will be generated, which returns a MockBuilder that can be used to register your mocks.
(If your component uses @Component.Builder
, the generated builder will have a withMocks
method that returns the MockBuilder.)
Usage example:
List<String> messages = new ArrayList<>();
CoffeeApp.Logger mockLogger = messages::add;
CoffeeApp.CoffeeComponent app = CoffeeApp_CoffeeComponent_Impl.builder()
.logLevel("")
.withMocks()
.coffeeAppLogger(mockLogger)
.build();
app.coffeeMaker().brew();
assertEquals(List.of(
"~ ~ ~ heating ~ ~ ~",
"=> => pumping => =>",
" [_]P coffee! [_]P "),
messages);
There are no "subcomponents" or "component dependencies".
There is no @Module
, but you can still have @Provides
methods, only you declare them directly in your component.
A @Provides
method must be static
.
There is no @Binds
.
It can be emulated with a @Provides
method, or, if you control the source code of the interface, a static @Inject
method.
There is no need for the @BindsInstance
annotation. Every factory parameter or builder parameter is a bound instance.
There is no @AssistedInject
, it's a can of worms.
There is no @IntoList
or @IntoSet
, you can return these collections from a @Provides
method.
There is no Lazy<T>
, please check if Provider<T>
covers your use case.
- modular-thermosiphon (dagger's "coffee machine" demo)
- jbock uses it, see for example ValidateComponent