Modularize is a framework for Minecraft plugin development that enables you to split your plugin into flexible, manageable modules. It provides automatic registration of listeners and commands, module lifecycle management, health monitoring, and more.
- Automatic Registration: Automatically register listeners and commands using annotations—no manual registration needed
- Module System: Organize your plugin into independent, configurable modules
- Hot Reloading: Reload individual modules or all modules at runtime without restarting the server
- Health Monitoring: Built-in server health monitoring with customizable TPS thresholds
- Class Blocking: Prevent usage of problematic classes (e.g.,
PlayerInteractEvent) in modularized code - Lifecycle Hooks:
Initializable,Reloadable, andHealthyinterfaces for fine-grained control - Zero Configuration: No need to manually edit
plugin.yml—everything is handled automatically - Dependency Injection Ready: Works seamlessly with dependency injection frameworks like Guice
- Installation
- Quick Start
- Core Concepts
- Annotations
- Lifecycle Interfaces
- Module Management
- Health Monitoring
- Class Blocking
- Configuration
- Examples
- Requirements
Add the JitPack repository and dependency to your pom.xml:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.jbwm</groupId>
<artifactId>Modularize</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>- Scan your package for modularized classes:
Set<Class<?>> modularizedClasses = Modularize.scanPackage("com.yourplugin.modules");- Build the module manager:
ModuleManager moduleManager = Modularize.buildManager(this, modularizedClasses);- Register all modules:
moduleManager.registerAll();That's it! Your listeners and commands are now automatically registered.
@Listen
@Module(name = "MyModule")
public class MyModule implements Listener, Initializable {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
event.getPlayer().sendMessage("Welcome!");
}
@Override
public void init() {
// Initialize your module here
getLogger().info("MyModule initialized!");
}
}A module is a class annotated with @Module that can optionally be annotated with @Listen or @Command. Modules are:
- Automatically registered when you call
registerAll() - Configurable via
config.yml(enabled/disabled) - Reloadable individually or all at once
- Isolated units of functionality
The ModuleManager is the central component that:
- Registers listeners and commands
- Manages module lifecycle
- Handles reloading
- Monitors server health
- Enforces class blocking rules
Marks a class as a module. Required for module functionality.
@Module(name = "ModuleName")
public class MyModule {
// Module code
}Parameters:
name(required): Unique identifier for the module (used in config.yml)
Automatically registers the class as a Bukkit Listener. The class must implement Listener.
@Listen
@Module(name = "MyListener")
public class MyListener implements Listener {
@EventHandler
public void onEvent(SomeEvent event) {
// Handle event
}
}Note: You still need to implement the Listener interface and use @EventHandler annotations.
Automatically registers a command. The class must implement TabExecutor.
@Command(
name = "mycommand",
usage = "/mycommand [args]",
permission = "myplugin.use",
permissionMessage = "You don't have permission!",
aliases = {"mc", "mycmd"}
)
@Module(name = "CommandModule")
public class MyCommand implements TabExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
// Command logic
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
// Tab completion logic
return Collections.emptyList();
}
}Parameters:
name(required): Command nameusage(required): Command usage stringpermission(optional): Required permission nodepermissionMessage(optional): Message shown when permission is deniedaliases(optional): Command aliases
Important:
CommandExecutoris not supported. UseTabExecutorinstead.- You don't need to register commands in
plugin.yml- Modularize handles it automatically.
Implement this interface to perform initialization tasks when the module is loaded (e.g., loading data from config, setting up inventories).
@Module(name = "MyModule")
public class MyModule implements Initializable {
@Override
public void init() {
// Called during module initialization
loadConfig();
setupInventories();
}
}Implement this interface to handle module reloading. The reload() method is called when the module is reloaded.
@Module(name = "MyModule")
public class MyModule implements Reloadable {
@Override
public void reload() {
// Called when module is reloaded
reloadConfig();
refreshData();
}
}Implement this interface to react to server health changes. Requires health monitoring to be initialized first.
@Module(name = "MyModule")
public class MyModule implements Healthy {
@Override
public void ifUnhealthy() {
// Called when server becomes unhealthy (TPS drops)
disableExpensiveOperations();
}
@Override
public void ifBackToHealth() {
// Called when server health is restored
enableExpensiveOperations();
}
}// Register all modules
moduleManager.registerAll();
// Register modules with specific qualifiers (if using @Qualifier)
moduleManager.registerAll("production", "enabled");// Reload a specific module by name
moduleManager.reloadModule("MyModule");
// Reload all modules
moduleManager.reloadModules();Note: Reloading reads from config.yml, so modules disabled in the config will be disabled after reload.
// Get all active listeners
Map<String, Listener> listeners = moduleManager.getListeners();
// Get all active commands
Map<String, Pair<TabExecutor, PluginCommand>> commands = moduleManager.getCommands();
// Get all initialized class instances
Set<Object> initializedClasses = moduleManager.getInitializedClasses();Modularize includes a built-in server health monitor that tracks TPS (Ticks Per Second) and notifies modules when the server becomes unhealthy.
moduleManager.registerAll();
moduleManager.initializeHealthMonitor();moduleManager.initializeHealthMonitor(
30 * 20L, // checkInterval: Check every 30 seconds (in ticks)
10, // historySize: Require 10 consecutive low TPS readings
19.0, // TPStreshold: Consider unhealthy if TPS < 19.0
60 * 20L // startDelay: Wait 60 seconds after server start (recommended: ≥200 ticks)
);Parameters:
checkInterval: How often (in ticks) to check server healthhistorySize: How many consecutive low TPS readings are required to trigger unhealthy stateTPStreshold: TPS threshold below which readings are considered lowstartDelay: Delay from server start before monitoring begins (recommended: at least 200 ticks)
For large projects with multiple developers, you can block the use of specific classes in modularized code. This is useful for preventing usage of problematic classes like PlayerInteractEvent (which fires for both hands, pressure plates, etc.).
Set<Class<?>> modularizedClasses = Modularize.scanPackage("com.yourplugin.modules");
ModuleManager moduleManager = Modularize.buildManager(
this,
modularizedClasses,
CheckType.EQUALS, // Match type: EQUALS or CONTAINS
ErrorType.RUN_TIME_EXCEPTION, // Error type: INFO or RUN_TIME_EXCEPTION
"org.bukkit.event.player.PlayerInteractEvent" // Blocked classes
);
moduleManager.registerAll();CheckType.EQUALS: Exact class name match (e.g.,org.bukkit.event.player.PlayerInteractEvent)CheckType.CONTAINS: Partial class name match (e.g.,PlayerInteractEvent)
ErrorType.INFO: Print a warning message to consoleErrorType.RUN_TIME_EXCEPTION: Throw aRuntimeExceptionand prevent plugin startup
Create your own events outside the scanned package (before or at the same level as your modules package):
com.yourplugin
├── modules/ ← Scanned package
│ └── MyModule.java
└── events/ ← Custom events (outside scanned package)
└── PlayerClickedBlockEvent.java
This way, you can use your custom events in modules while blocking the problematic Bukkit events.
When a class is annotated with @Module and @Listen or @Command, it becomes a module and is automatically added to config.yml:
modules:
MyModule: true
AnotherModule: trueYou don't need to manually add modules to the config—Modularize handles it automatically.
Simply change the value in config.yml:
modules:
MyModule: true # Module is enabled
AnotherModule: false # Module is disabledDisabled modules won't be registered, and reloading will respect these settings.
- Module: Class annotated with
@ModuleAND (@ListenOR@Command) - Non-Module: Class annotated with only
@Listenor@Command(without@Module)
Non-module classes are still registered but:
- Won't appear in
config.yml - Can't be reloaded individually
- Can't use lifecycle interfaces
Complete code examples are available in the example/ directory of this repository.
@Listen
@Module(name = "WelcomeModule")
public class WelcomeModule implements Listener, Initializable {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
player.sendMessage("Welcome to the server!");
}
@Override
public void init() {
getLogger().info("WelcomeModule initialized!");
}
}@Command(
name = "hello",
usage = "/hello [player]",
permission = "myplugin.hello"
)
@Module(name = "HelloCommand")
public class HelloCommand implements TabExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length > 0) {
Player target = Bukkit.getPlayer(args[0]);
if (target != null) {
sender.sendMessage("Hello, " + target.getName() + "!");
} else {
sender.sendMessage("Player not found!");
}
} else {
sender.sendMessage("Hello!");
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) {
return Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.filter(name -> name.toLowerCase().startsWith(args[0].toLowerCase()))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
}@Listen
@Module(name = "ConfigModule")
public class ConfigModule implements Listener, Reloadable {
private String message;
@Override
public void reload() {
// Reload configuration
reloadConfig();
message = getConfig().getString("message", "Default message");
getLogger().info("ConfigModule reloaded!");
}
@EventHandler
public void onPlayerChat(AsyncPlayerChatEvent event) {
event.getPlayer().sendMessage(message);
}
}@Listen
@Module(name = "PerformanceModule")
public class PerformanceModule implements Listener, Healthy {
private boolean expensiveOperationsEnabled = true;
@Override
public void ifUnhealthy() {
expensiveOperationsEnabled = false;
getLogger().warning("Server unhealthy - disabling expensive operations");
}
@Override
public void ifBackToHealth() {
expensiveOperationsEnabled = true;
getLogger().info("Server healthy - re-enabling expensive operations");
}
@EventHandler
public void onSomeEvent(SomeEvent event) {
if (expensiveOperationsEnabled) {
// Perform expensive operation
}
}
}- Java: 17 or higher
- Minecraft Server: Paper/Spigot 1.19.3+ (or compatible)
- Maven: 3.8+ (for building)
Contributions are welcome! Please feel free to submit a Pull Request.
See the LICENSE file for details.
- Built for the Minecraft plugin development community
- Uses Reflections for class scanning
- Compatible with dependency injection frameworks like Guice
Made for Minecraft plugin developers