Skip to content

Basic Usage

The_Fireplace edited this page Jun 21, 2022 · 6 revisions

What is @Implementation?

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.

Simple Example

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
    }
}

Injecting dependencies

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
}

Creating singletons

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
}

Named implementations (Binding Annotations)

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.

Environment specific files

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

Soft Dependency specific files

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

Binding with multiple interfaces

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
    }
}

How do the Injectors work?

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.