A slim, lightweight, and thread-safe dependency injection (DI) framework originally designed for Fabric mods. It enables clean code organization and decoupled components by automatically managing object instances and their dependencies.
- Features
- Compatibility
- Core Concepts
- Getting Started
- Accessing Beans Manually
- Advanced Dependency Injection
- How It Works
- Error Handling
- License
- Mod-Specific Containers: Each mod gets its own isolated DI container to prevent conflicts.
- Annotation-Driven: Configure your dependencies declaratively using simple annotations.
- Constructor & Field Injection: Supports the two most common types of dependency injection.
- Full Integration of Main Class: Your main mod class (annotated with
@ModMain) is fully integrated, allowing direct field injection with@ModInject. - Singleton Scope: All classes annotated with
@ModScopedare managed as singletons within their mod's container. - List Injection: Inject all implementations of a specific interface into a single list.
- Qualifiers (
@ModIdentifier): Distinguish between multiple implementations of the same interface. - Lifecycle Management (
@PostConstruct): Execute initialization logic after all dependencies have been injected. - Automatic Circular Dependency Detection: Prevents stack overflow errors at runtime by robustly detecting cycles.
- Eager Instantiation: All managed classes are initialized at mod startup, catching configuration errors early.
- Thread-Safe: Designed for safe use in multi-threaded environments.
However, since the framework has no direct dependencies on the Fabric API, it is theoretically platform-agnostic and should work with other mod loaders (like Forge, NeoForge, etc.). Use on platforms other than Fabric is at your own risk.
The framework is built around three central annotations and the concept of a mod-specific container.
This annotation marks a class as a managed component (also known as a "bean" or "service"). Any class annotated with @ModScoped will be instantiated as a singleton by the ModInjector and stored in the mod's DI container.
This annotation tells the ModInjector where to inject dependencies. It can be used in two ways:
- Constructor Injection: Place it on a constructor. The injector will use this constructor to create the class instance, automatically providing instances for all parameters.
- Field Injection: Place it on a field. The injector will inject a suitable instance into this field after the object is created.
This annotation marks your mod's main class. It serves as the starting point for the classpath scan and allows the class itself to be treated as a managed bean.
This tutorial demonstrates how to create a service and inject it into a manager using best practices (with interfaces).
Add the dependency to your build.gradle file.
build.gradle
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
// Replace 'Tag' with the latest version from the JitPack badge above
implementation 'com.github.dotnomi:fabric-dependency-injection:Tag'
}Instead of working with concrete classes, we define a contract (an interface). This makes your code more flexible and easier to test.
MessageService.java
package com.mymod.service;
public interface MessageService {
String getWelcomeMessage(String playerName);
}Now, create a class that implements this interface and mark it with @ModScoped so the injector can manage it.
ConfigMessageService.java
package com.mymod.service.impl;
import com.dotnomi.fabricdependencyinjection.annotation.ModScoped;
import com.mymod.service.MessageService;
@ModScoped
public class ConfigMessageService implements MessageService {
@Override
public String getWelcomeMessage(String playerName) {
// In a real application, this would come from a config file
return "Welcome, " + playerName + "!";
}
}Create a PlayerManager class that depends on MessageService. Inject the interface (not the concrete class) via the constructor.
PlayerManager.java
package com.mymod.manager;
import com.dotnomi.fabricdependencyinjection.annotation.ModInject;
import com.dotnomi.fabricdependencyinjection.annotation.ModScoped;
import com.mymod.service.MessageService;
@ModScoped
public class PlayerManager {
private final MessageService messageService;
// The injector will automatically find the ConfigMessageService implementation
// and provide it here.
@ModInject
public PlayerManager(MessageService messageService) {
this.messageService = messageService;
}
public void onPlayerJoin(String playerName) {
String welcomeMessage = messageService.getWelcomeMessage(playerName);
System.out.println(welcomeMessage);
}
}In your main mod class, you can now directly inject dependencies into fields. Call ModInjector.initialize() in your onInitialize method, passing this to integrate the main class instance into the container.
MyMod.java
package com.mymod;
import com.dotnomi.fabricdependencyinjection.ModInjector;
import com.dotnomi.fabricdependencyinjection.annotation.ModInject;
import com.dotnomi.fabricdependencyinjection.annotation.ModMain;
import com.mymod.manager.PlayerManager;
import net.fabricmc.api.ModInitializer;
@ModMain
public class MyMod implements ModInitializer {
public static final String MOD_ID = "mymod";
// This field will be automatically populated by the injector!
@ModInject
private PlayerManager playerManager;
@Override
public void onInitialize() {
// Pass 'this' (the instance) to initialize the container.
// After this line returns, the 'playerManager' field will be injected.
ModInjector.initialize(MOD_ID, this);
// The playerManager is now ready to use directly. No need for manual lookups.
playerManager.onPlayerJoin("Steve");
}
}While automatic injection is preferred, you sometimes need to access a managed bean from a location where injection is not possible (e.g., in static methods, vanilla classes, or integration points with other mods). The ModInjector provides static methods for this purpose.
Use ModInjector.getInstanceOf() to retrieve a single bean instance.
// Somewhere in your code, e.g., a static helper method
public class SomeUtil {
public static void doSomethingWithPlayerManager() {
// Retrieve the PlayerManager instance from the container
PlayerManager manager = ModInjector.getInstanceOf(MyMod.MOD_ID, PlayerManager.class);
manager.onPlayerJoin("Alex");
}
}If you have multiple implementations of an interface, you can specify which one you need using an identifier.
// Assuming a StorageService with a @ModIdentifier("database")
StorageService dbService = ModInjector.getInstanceOf(MyMod.MOD_ID, StorageService.class, "database");Use ModInjector.getInstancesOf() to get a BeanList containing all beans that implement a specific interface. This is useful for plugin- or listener-style systems.
public class EventBroadcaster {
public void broadcastPlayerJoinEvent(PlayerEntity player) {
// Get all registered listeners
BeanList<PlayerJoinListener> listeners = ModInjector.getInstancesOf(MyMod.MOD_ID, PlayerJoinListener.class);
// Notify each listener
for (PlayerJoinListener listener : listeners) {
listener.onPlayerJoin(player);
}
}
}Sometimes you want to get all implementations of a specific interface, for example, for a command or plugin system. For this, you can use BeanList<T>.
Example: Registering multiple chat commands.
-
Define a
ChatCommandinterface:public interface ChatCommand { void execute(); }
-
Create multiple implementations:
@ModScoped public class HelpCommand implements ChatCommand { /* ... */ } @ModScoped public class StatusCommand implements ChatCommand { /* ... */ }
-
Inject the list of all commands:
@ModScoped public class CommandManager { private final BeanList<ChatCommand> commands; @ModInject public CommandManager(BeanList<ChatCommand> commands) { this.commands = commands; // Contains instances of HelpCommand and StatusCommand } public void registerAll() { // ... } }
If there are multiple implementations for the same interface, you need to tell the injector which one to use. Use @ModIdentifier to give each implementation a unique name.
Example: Selecting a specific storage service.
-
Define a
StorageServiceinterface and two implementations:public interface StorageService { void saveData(String data); } @ModScoped @ModIdentifier("file") public class FileStorage implements StorageService { /* ... */ } @ModScoped @ModIdentifier("database") public class DatabaseStorage implements StorageService { /* ... */ }
-
Inject a specific implementation:
@ModScoped public class PlayerDataHandler { private final StorageService storage; @ModInject public PlayerDataHandler(@ModIdentifier("file") StorageService storage) { // The FileStorage instance is guaranteed to be injected here. this.storage = storage; } }
If a class needs to run initialization logic after its dependencies have been injected, you can annotate a method with @PostConstruct. This also works in your @ModMain class.
Example: Loading a configuration file.
@ModScoped
public class ConfigService {
@ModInject
private ModPaths modPaths;
private Configuration config;
// This method is called AFTER modPaths has been injected.
@PostConstruct
public void load() {
// Load the configuration from the disk...
System.out.println("Configuration loaded!");
}
}ModInjector.initialize(modId, mainInstance)is called.- The provided
@ModMaininstance is immediately registered as a bean in the container. - A new, dedicated
ModContaineris created for themodId. - The
@ModMainannotation is read to determine the base package for scanning. - Using the
Reflectionslibrary, the classpath is scanned for all classes annotated with@ModScoped. - The container creates an instance for each found
@ModScopedclass, recursively resolving its dependencies. - Finally, dependencies are injected into the fields of the registered
@ModMaininstance, and its@PostConstructmethod is invoked. - The container is now fully initialized and running.
The framework throws specific exceptions to clearly identify configuration and runtime problems:
ContainerAlreadyInitializedException: Thrown ifinitialize()is called more than once for the samemodId.ContainerNotInitializedException: Thrown ifgetInstanceOf()is called before the container has been initialized.CircularDependencyException: Thrown when a circular dependency is detected.MultipleInjectableConstructorsException: Thrown if a class has more than one constructor annotated with@ModInject.NoInjectableConstructorException: Thrown if no suitable constructor is found.TooManyInstancesFoundException: Thrown when requesting a single instance of an interface that has multiple implementations without specifying an@ModIdentifier.UnmanagedClassException: Thrown when an instance of a class not managed by the container is requested.NoMainClassException: Thrown if the class passed toinitialize()is not annotated with@ModMain.InstanceCreationException: A general-purpose error for when instantiation fails for any other reason.
This project is licensed under the GPL-3.0 License. See the LICENSE file for details.