-
Notifications
You must be signed in to change notification settings - Fork 4
Basic Usage
One of the key differences between Annotated DI and plain Guice is the @Implementation
annotation. This automatically sets up a binding from the interface to its implementation class. The advantage to this is that we can create bindings 1. without the API having a compile-time dependency on the implementation and 2. without a large configuration file somewhere pointing all the API interfaces to their implementations.
So we can have an interface, let's call it MonsterFactory
, which we want injected somewhere. Then we have a class StandardMonsterFactory
, which implements this interface.
MonsterFactory.java
(part of the API)
interface MonsterFactory {
void createMonster();
}
StandardMonsterFactory.java
(implementation detail, part of your mod)
@Implementation
class StandardMonsterFactory implements MonsterFactory {
void createMonster() {
//do something
}
}
Any class that is being injected can have a public constructor annotated with @Inject
to have its parameters injected using dependency injection. For example, we have a class MyMonsterSpawner
which needs a MonsterFactory
to create monsters. We can make the constructor as follows.
class MyMonsterSpawner {
private final MonsterFactory monsterFactory;
@Inject
public MyMonsterSpawner(MonsterFactory monsterFactory) {
this.monsterFactory = monsterFactory;
}
}
Ideally since this is going to be dependency injected, we'll also have this file behind an interface, so MyMonsterSpawner would realistically end up more like:
@Implementation
class MyMonsterSpawner implements MonsterSpawner {
private final MonsterFactory monsterFactory;
@Inject
public MyMonsterSpawner(MonsterFactory monsterFactory) {
this.monsterFactory = monsterFactory;
}
// Some implemented method here
}
By default, a new instance is created each time your implementation is injected. There are many times where you may want a class to be a singleton. To do this, add @Singleton
to your implementation class.
For example, with the MyMonsterSpawner
discussed above, let's say we only ever want a single MyMonsterSpawner
to exist, and re-use it wherever MonsterSpawner
is injected. The class would become the following:
@Singleton
@Implementation
class MyMonsterSpawner implements MonsterSpawner {
private final MonsterFactory monsterFactory;
@Inject
public MyMonsterSpawner(MonsterFactory monsterFactory) {
this.monsterFactory = monsterFactory;
}
// Some implemented method here
}
There are times when you need an implementation that is different from the one that is injected by default. This can be achieved with the same interface by using a Named implementation.
For example, say we have a special implementation of MonsterFactory
that creates monsters faster, but is only safe to use in specific cases. Let's call it UnsafeMonsterFactory
. Our specific case that needs it is the SkyMonsterSpawner
. The code would look something like:
UnsafeMonsterFactory.java
@Implementation(name="unsafe")
class UnsafeMonsterFactory implements MonsterFactory {
void createMonster() {
//do something
}
}
SkyMonsterSpawner.java
class SkyMonsterSpawner implements MonsterSpawner {
private final MonsterFactory unsafeMonsterFactory;
@Inject
public SkyMonsterSpawner(@Named("unsafe") MonsterFactory unsafeMonsterFactory) {
this.unsafeMonsterFactory = unsafeMonsterFactory;
}
}
Note that Guice recommends using purpose-built annotations instead of Named since they are more easily type checked, however these are not supported with @Implementation
based bindings. Please create manual bindings with the di-module
entrypoint if you need custom Binding Annotations.
@Implementation
has the environment
property which can be set to CLIENT
or SERVER
to note if it should only load on a client or dedicated server. On Fabric, it recognizes when a class has been annotated with @EnvType
and use that if the environment
property hasn't been set. Please make sure client side classes annotated with @Implementation
are marked as client-side so they do not get class loaded on a dedicated server.
@Implementation
has the dependencyModIds
property which can be set to any number of dependencies to note if it should only load when those mods are present. This is useful when dealing with Implementations you only want loaded if a certain soft-dependency is present.
By default,@Implementation
is designed to detect and bind to a single interface. If your implementation implements multiple interfaces, or needs to bind to multiple, you should set the value
parameter to the fully qualified class name(s), or set the allInterfaces
property to true. Let's re-use the example of a SimpleMonsterFactory
with multiple interfaces.
First one, we only want to inject for MonsterFactory:
@Implementation("com.mypackage.MonsterFactory")
class StandardMonsterFactory implements MonsterFactory, SomeOtherInterface {
void createMonster() {
//do something
}
}
Second example, we also want this injected for SomeOtherInterface
:
@Implementation({"com.mypackage.MonsterFactory", "com.mypackage.SomeOtherInterface"})
class StandardMonsterFactory implements MonsterFactory, SomeOtherInterface {
void createMonster() {
//do something
}
}
Second example, simplified:
@Implementation(allInterfaces=true)
class StandardMonsterFactory implements MonsterFactory, SomeOtherInterface {
void createMonster() {
//do something
}
}
As of Annotated DI 3.0.0, Injectors have been rewritten to handle mod-related scopes and errors within mods a little better. Originally, there was one global injector which pooled all mods' bindings. The problem with this was, if any mod had an error in their setup, it would break the whole thing and the loader would then blame Annotated DI. Which brings us to the current system: Each mod gets its own injector, which is a child of its dependencies' injectors, all the way up the tree to Annotated DI which is a parent to all the other Injectors. Mods initialize their own Injectors, which will only add their own bindings, and initialize dependencies' injectors if needed. Your mod's injector gains access to the dependencies' bindings. For more information, see Guice's documentation on the scope of child injectors.