Dependency Injection
Imperat provides a powerful dependency injection system that allows you to inject services, managers, and other dependencies directly into your command classes. This feature makes your commands cleaner and more maintainable by separating business logic from command handling.
Overview
The @Dependency
annotation allows you to inject dependencies into your command classes. These dependencies are resolved at runtime and can be any object that you register with the Imperat instance.
Basic Usage
1. Register Dependencies
First, register your dependencies with the Imperat instance using the dependencyResolver
method:
public class MyPlugin extends JavaPlugin {
private BukkitImperat imperat;
private PlayerManager playerManager;
private EconomyService economyService;
@Override
public void onEnable() {
// Initialize your services
playerManager = new PlayerManager();
economyService = new EconomyService();
// Register dependencies with Imperat
imperat = BukkitImperat.builder(this)
.dependencyResolver(PlayerManager.class, () -> playerManager)
.dependencyResolver(EconomyService.class, () -> economyService)
.build();
}
}
2. Inject Dependencies in Commands
Use the @Dependency
annotation on fields in your command classes:
@Command("balance")
@Permission("economy.balance")
public final class BalanceCommand {
@Dependency
private PlayerManager playerManager;
@Dependency
private EconomyService economyService;
@Usage
public void checkBalance(BukkitSource source) {
if (source.isConsole()) {
source.error("This command can only be used by players!");
return;
}
Player player = source.as(Player.class);
double balance = economyService.getBalance(player.getUniqueId());
source.reply("Your balance: $" + balance);
}
@Usage
public void checkOtherBalance(BukkitSource source, @Named("player") Player target) {
double balance = economyService.getBalance(target.getUniqueId());
source.reply(target.getName() + "'s balance: $" + balance);
}
}
Advanced Usage
Constructor Injection
You can also inject dependencies through constructor parameters.
Injecting dependency through a constructor may not work when you have subcommand classes, whether inner-classes or external classes.
It's always preferred to use the @Dependency
.
@Command("admin")
@Permission("admin.command")
public final class AdminCommand {
private final PlayerManager playerManager;
private final EconomyService economyService;
public AdminCommand(PlayerManager playerManager, EconomyService economyService) {
this.playerManager = playerManager;
this.economyService = economyService;
}
@Usage
public void giveMoney(BukkitSource source, @Named("player") Player target, @Named("amount") double amount) {
economyService.addBalance(target.getUniqueId(), amount);
source.reply("Added $" + amount + " to " + target.getName());
}
}
Dependency Registration Methods
Simple Registration
Register a dependency with a simple supplier:
.dependencyResolver(MyService.class, () -> myServiceInstance)
Lazy Initialization
Register dependencies that are initialized on first use:
.dependencyResolver(ExpensiveService.class, () -> {
if (expensiveService == null) {
expensiveService = new ExpensiveService();
}
return expensiveService;
})
Real-World Example
Here's a comprehensive example showing dependency injection in a rank management system:
// Service classes
public class RankManager {
private final Map<String, Rank> ranks = new HashMap<>();
public void createRank(String name) {
ranks.put(name, new Rank(name));
}
public Rank getRank(String name) {
return ranks.get(name);
}
public boolean hasRank(String name) {
return ranks.containsKey(name);
}
}
public class PermissionManager {
private final Map<UUID, Set<String>> permissions = new HashMap<>();
public void addPermission(UUID playerId, String permission) {
permissions.computeIfAbsent(playerId, k -> new HashSet<>()).add(permission);
}
public boolean hasPermission(UUID playerId, String permission) {
return permissions.getOrDefault(playerId, Set.of()).contains(permission);
}
}
// Command using dependency injection
@Command("rank")
@Permission("rank.admin")
@Description("Rank management system")
public final class RankCommand {
@Dependency
private RankManager rankManager;
@Dependency
private PermissionManager permissionManager;
@Usage
@Description("Create a new rank")
public void createRank(BukkitSource source, @Named("name") String rankName) {
if (rankManager.hasRank(rankName)) {
source.error("Rank '" + rankName + "' already exists!");
return;
}
rankManager.createRank(rankName);
source.reply("Created rank: " + rankName);
}
@Usage
@Description("Give a rank to a player")
public void giveRank(BukkitSource source, @Named("player") Player player, @Named("rank") String rankName) {
Rank rank = rankManager.getRank(rankName);
if (rank == null) {
source.error("Rank '" + rankName + "' does not exist!");
return;
}
// Add rank permissions to player
for (String permission : rank.getPermissions()) {
permissionManager.addPermission(player.getUniqueId(), permission);
}
source.reply("Gave rank '" + rankName + "' to " + player.getName());
}
@Usage
@Description("Check if player has permission")
public void checkPermission(BukkitSource source, @Named("player") Player player, @Named("permission") String permission) {
boolean hasPermission = permissionManager.hasPermission(player.getUniqueId(), permission);
source.reply(player.getName() + " has permission '" + permission + "': " + hasPermission);
}
}
// Plugin main class
public class RankPlugin extends JavaPlugin {
private BukkitImperat imperat;
private RankManager rankManager;
private PermissionManager permissionManager;
@Override
public void onEnable() {
// Initialize services
rankManager = new RankManager();
permissionManager = new PermissionManager();
// Register dependencies
imperat = BukkitImperat.builder(this)
.dependencyResolver(RankManager.class, () -> rankManager)
.dependencyResolver(PermissionManager.class, () -> permissionManager)
.build();
// Register command
imperat.registerCommand(new RankCommand());
}
}
Best Practices
1. Service Design
- Single Responsibility: Each service should have a single, well-defined purpose
- Interface Segregation: Use interfaces to define service contracts
- Dependency Inversion: Depend on abstractions, not concrete implementations
- Using Inner non-static subcommand classes: This will allow your inner subcommands to share the dependencies from your root/parent class.
2. Registration
- Register Early: Register dependencies during plugin initialization
- Use Meaningful Names: Use descriptive class names for better readability
- Handle Null Cases: Ensure your services handle null or missing dependencies gracefully
3. Usage
- Minimize Dependencies: Only inject what you actually need
- Avoid Circular Dependencies: Be careful not to create dependency cycles
- Use Constructor Injection: Prefer constructor injection for required dependencies
- Use Field Injection: Use field injection for optional or frequently changing dependencies
4. Performance
- Lazy Loading: Use lazy initialization for expensive services
- Caching: Cache frequently accessed data within your services
- Thread Safety: Ensure your services are thread-safe if used in async contexts
Error Handling
When a dependency cannot be resolved, Imperat will throw a UnknownDependencyException
, A dependency fails to be resolved when the
returned value from its dependency resolver is null
(by-default even without a dependency-resolver).
Make sure to register A dependency resolver for all of your dependency fields in your command class.
Advanced Patterns
Factory Pattern
.dependencyResolver(PlayerService.class, () -> {
return new PlayerServiceFactory().createService(config.getDatabaseType());
})
Singleton Pattern
.dependencyResolver(DatabaseService.class, () -> DatabaseService.getInstance())
Configuration-Based Registration
.dependencyResolver(FeatureService.class, () -> {
if (config.isFeatureEnabled("advanced")) {
return new AdvancedFeatureService();
} else {
return new BasicFeatureService();
}
})
Dependency injection in Imperat makes your commands more modular, testable, and maintainable. By separating concerns and using proper dependency management, you can create complex command systems that are easy to understand and extend.