The main class and plugin lifecycle
onLoad, onEnable and onDisable, and how the rest of your code gets hold of your plugin.
Every plugin has one main class, and Paper calls three of its methods at fixed moments: when the plugin loads, when it is enabled, and when it is disabled. On this page you will learn what belongs in each one, how your other classes get hold of your plugin, and how to shut down without losing data.
Your main class is your plugin
Open any plugin project and you will find one class that extends JavaPlugin. That is the main class. Your plugin.yml names it on the main: line, and that line is the only way Paper finds it.
When the server starts, Paper reads plugin.yml, finds the main class, and creates exactly one object of it. You never write new MyPlugin() yourself. Paper does it, and Paper decides when the important moments happen. Your job is to fill in the methods Paper calls at those moments.
Those moments are called the plugin lifecycle: the plugin is loaded, then enabled, and finally disabled. Each moment has a method with a matching name:
| Method | When Paper calls it | What you usually do there |
|---|---|---|
onLoad() | Very early, right after creating your main class. No worlds exist yet. | Almost nothing. Most plugins leave it out. |
onEnable() | When the plugin starts. By default the worlds are already loaded. | Load settings and data, register listeners and commands, start repeating tasks. |
onDisable() | When the plugin stops, usually because the server is shutting down. | Save data, close files and database connections. |
You do not have to write all three. JavaPlugin already has empty versions of them, so you only override the ones you need. That is what @Override means: "I am replacing a method my parent class already has". Inheritance, interfaces and annotations explains extends and @Override in detail.
Watching the lifecycle happen
The best way to understand the lifecycle is to watch it. This small plugin writes a line to the server console from each of the three methods:
Where this file livesplugin-lifecycle-hellosrcmainjavacomexamplepluginlifecyclehelloHelloLifecyclePlugin.java
The package com.example.pluginlifecyclehello is the folder path com/example/pluginlifecyclehello inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-hello/
- src/main/
- java/com/example/pluginlifecyclehello/Package com.example.pluginlifecyclehello
- HelloLifecyclePlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecyclehello/Package com.example.pluginlifecyclehello
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecyclehello;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class HelloLifecyclePlugin extends JavaPlugin {6 7 @Override8 public void onLoad() {9 getLogger().info("1. onLoad: Paper created my main class. Worlds are not loaded yet.");10 }11 12 @Override13 public void onEnable() {14 getLogger().info("2. onEnable: worlds are loaded, time to set everything up.");15 getLogger().info("My name is " + getPluginMeta().getName() + ", version " + getPluginMeta().getVersion());16 getLogger().info("My data folder is " + getDataFolder().getPath());17 getLogger().info("This server runs Minecraft " + getServer().getMinecraftVersion()18 + " and allows " + getServer().getMaxPlayers() + " players.");19 getLogger().warning("This is what a warning looks like.");20 getLogger().severe("And this is what an error looks like.");21 }22 23 @Override24 public void onDisable() {25 getLogger().info("3. onDisable: the server is stopping. Save your data here.");26 }27}- The main class.
extends JavaPlugingives it everything a plugin needs: a logger, a data folder, access to the server and the three lifecycle methods.finaljust means no other class may extend this one. - Paper calls this first, before any world is loaded. This plugin only prints a line, which is all most plugins would ever do here.
- The numbers in the messages make the order easy to see in the console.
- Paper calls this when the plugin starts. The worlds exist now, players can join soon, and this is where real plugins set everything up.
getPluginMeta()gives you the information from yourplugin.yml: name, version, authors, description and more. Reading the version from here means you never have to type it twice.- Your plugin's own folder for files, such as
config.yml. Paper does not create it until something saves a file there. getServer()is the server itself. From it you reach players, worlds, the scheduler, the plugin manager and almost everything else.- A warning: something is odd, but the plugin keeps working.
- An error: something is broken. Paper prints it in red.
- Paper calls this when the plugin stops. It is your last chance to save anything.
Start your test server with Run Paper Server (see Build, install and test). Here is what the console shows. Some of Paper's own lines are left out so yours are easier to spot:
[14:02:03 INFO]: [HelloLifecycle] Loading server plugin HelloLifecycle v1.0.0[14:02:03 INFO]: [HelloLifecycle] 1. onLoad: Paper created my main class. Worlds are not loaded yet.[14:02:04 INFO]: Preparing level "world"[14:02:07 INFO]: [HelloLifecycle] Enabling HelloLifecycle v1.0.0[14:02:07 INFO]: [HelloLifecycle] 2. onEnable: worlds are loaded, time to set everything up.[14:02:07 INFO]: [HelloLifecycle] My name is HelloLifecycle, version 1.0.0[14:02:07 INFO]: [HelloLifecycle] My data folder is plugins\HelloLifecycle[14:02:07 INFO]: [HelloLifecycle] This server runs Minecraft 26.3 and allows 20 players.[14:02:07 WARN]: [HelloLifecycle] This is what a warning looks like.[14:02:07 ERROR]: [HelloLifecycle] And this is what an error looks like.[14:02:07 INFO]: Done (4.218s)! For help, type "help"Notice three things. Paper prints "Loading server plugin" and "Enabling" for you, so you can always see when each step starts. Every line from your logger starts with [HelloLifecycle], so you can tell your lines apart from other plugins'. And onLoad ran before the world was prepared, while onEnable ran after.
Now type stop in the console (no slash in the console):
stop[14:10:21 INFO]: Stopping the server[14:10:21 INFO]: Stopping server[14:10:21 INFO]: [HelloLifecycle] Disabling HelloLifecycle v1.0.0[14:10:21 INFO]: [HelloLifecycle] 3. onDisable: the server is stopping. Save your data here.[14:10:21 INFO]: Saving players[14:10:21 INFO]: Saving worldsonDisable runs before Paper saves the players and worlds. That detail matters later on this page.
The tools your main class gives you
Because your main class extends JavaPlugin, it inherits a set of useful methods. You will use these on almost every page of this guide:
| Method | What it gives you |
|---|---|
getLogger() | The logger: writes lines to the server console with your plugin's name in front. |
getServer() | The running server: players, worlds, the plugin manager, the scheduler. |
getPluginMeta() | The values from plugin.yml: getName(), getVersion(), getAuthors(), getDescription(). |
getDataFolder() | Your data folder, plugins/YourPlugin/, where your files live. |
getConfig(), saveDefaultConfig() | Your config.yml. Configuration files covers these. |
getLifecycleManager(), registerCommand(...) | Register commands. Commands, part 1 covers these. |
Logging at the right level
The logger has a method for each level of importance. Pick the level by asking "does the server owner need to act on this?"
1getLogger().info("Loaded 12 homes.");2getLogger().warning("The setting 'max-homes' is negative, using 1 instead.");3getLogger().severe("Could not save homes.yml. Data may be lost!");4getLogger().fine("Debug detail, hidden unless the server owner turns it on.");info is for normal news, warning (printed as WARN) for problems the plugin can work around, and severe (printed as ERROR) for real failures. fine lines are hidden by default, which makes them handy for debug output you want to leave in your code. Never use System.out.println in a plugin: the line has no plugin name in front, so nobody can tell where it came from.
getServer() or Bukkit?
You will see two ways to reach the server in code online:
1int a = getServer().getOnlinePlayers().size();2int b = Bukkit.getServer().getOnlinePlayers().size();3int c = Bukkit.getOnlinePlayers().size();All three give the same answer. Bukkit is a class with static shortcut methods: you can call them from any class without having your plugin object at hand. Inside your main class, getServer() is the natural choice. In other classes, Bukkit.something() is fine and very common. Neither is wrong.
What goes in onEnable
onEnable is where your plugin comes to life. Almost every plugin does the same four jobs there, in this order:
- Load settings and data:
saveDefaultConfig(), read files. - Create your helper objects: managers, trackers, caches.
- Register listeners and commands, so Paper starts calling your code.
- Start repeating tasks, such as an autosave every few minutes.
The order matters. A listener that counts visits needs the visit data to exist before the first player joins, so the data is loaded first and the listener is registered after.
The rest of this page uses a small but complete plugin called LifecycleDemo. It counts how many times each player has joined, shows the count when they join and on /visits, and saves the counts to a file so they survive restarts. Here is its main class:
Where this file livesplugin-lifecycle-demosrcmainjavacomexamplepluginlifecycledemoLifecycleDemoPlugin.java
The package com.example.pluginlifecycledemo is the folder path com/example/pluginlifecycledemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-demo/
- src/main/
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- LifecycleDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- VisitListener.javaListener: reacts to events
- VisitTracker.javaHelper class
- VisitsCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecycledemo;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class LifecycleDemoPlugin extends JavaPlugin {6 7 private static final long AUTOSAVE_TICKS = 5 * 60 * 20L;8 9 private VisitTracker tracker;10 11 @Override12 public void onLoad() {13 getLogger().info("Loaded. Waiting for the worlds before doing anything.");14 }15 16 @Override17 public void onEnable() {18 tracker = new VisitTracker(this);19 tracker.load();20 21 getServer().getPluginManager().registerEvents(new VisitListener(tracker), this);22 registerCommand("visits", "Shows how many times you have joined", new VisitsCommand(tracker));23 getServer().getScheduler().runTaskTimer(this, () -> tracker.save(), AUTOSAVE_TICKS, AUTOSAVE_TICKS);24 25 getLogger().info("Ready. Loaded visit counts for " + tracker.size() + " players.");26 }27 28 @Override29 public void onDisable() {30 if (tracker != null) {31 tracker.save();32 }33 getLogger().info("Visit counts saved. Goodbye!");34 }35}- Five minutes in
ticks : 5 minutes times 60 seconds times 20 ticks per second. TheLmakes it along, the number type the scheduler wants. - A
field that will hold the tracker. It is a field, not a variable insideonEnable, becauseonDisableneeds the same object later to save it. - Only a log line. Nothing else here needs to run before the worlds exist.
- Job 2: create the helper object.
thisis your plugin object, handed to the tracker so it can find the data folder and the logger. - Job 1: load the saved counts from
visits.ymlbefore anyone can join. - Job 3: register the listener. It gets the tracker, because counting visits is its whole job.
- Also job 3: registers
/visits.registerCommandis a shortcutJavaPlugingives you for simple commands, and it only works insideonEnable. - Job 4: save every five minutes, so a crash loses at most five minutes of counts. The first number is the delay before the first run, the second is the time between runs.
- A short "I am ready" line with a useful number. Server owners love these when something goes wrong.
- If
onEnablecrashed before it created the tracker, the field is stillnull. Paper still callsonDisableafter a crash, so this check stops a second crash. - The final save. Players' newest visits since the last autosave are written to disk.
When Steve joins for the third time, he sees this, and /visits tells him the same number later:
Welcome! This is visit number 3.
You have joined this server 3 times.
Passing your plugin to other classes
Real plugins are split into several classes: a listener here, a command there, a class that stores data. Sooner or later, one of those classes needs something only the plugin has: the logger, the data folder, the config or the scheduler. How does it get hold of the plugin object?
There are three ways you will see in code online. All three work. One is clearly the best.
The recommended way: pass it in the constructor
A constructor is the special method that runs when an object is created with new. If a class needs something, it asks for it in its constructor, and whoever creates the object has to hand it over. This is called constructor injection. Here is the tracker from LifecycleDemo:
Where this file livesplugin-lifecycle-demosrcmainjavacomexamplepluginlifecycledemoVisitTracker.java
The package com.example.pluginlifecycledemo is the folder path com/example/pluginlifecycledemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-demo/
- src/main/
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- LifecycleDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- VisitListener.javaListener: reacts to events
- VisitTracker.javayou are hereHelper class
- VisitsCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecycledemo;2 3import java.io.File;4import java.io.IOException;5import java.util.HashMap;6import java.util.Map;7import java.util.UUID;8import java.util.logging.Level;9import org.bukkit.configuration.file.YamlConfiguration;10 11public final class VisitTracker {12 13 private final LifecycleDemoPlugin plugin;14 private final File file;15 private final Map<UUID, Integer> visits = new HashMap<>();16 17 public VisitTracker(LifecycleDemoPlugin plugin) {18 this.plugin = plugin;19 this.file = new File(plugin.getDataFolder(), "visits.yml");20 }21 22 public void load() {23 YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);24 for (String key : yaml.getKeys(false)) {25 visits.put(UUID.fromString(key), yaml.getInt(key));26 }27 }28 29 public void save() {30 YamlConfiguration yaml = new YamlConfiguration();31 for (Map.Entry<UUID, Integer> entry : visits.entrySet()) {32 yaml.set(entry.getKey().toString(), entry.getValue());33 }34 try {35 yaml.save(file);36 } catch (IOException exception) {37 plugin.getLogger().log(Level.SEVERE, "Could not save " + file.getName(), exception);38 }39 }40 41 public int addVisit(UUID playerId) {42 return visits.merge(playerId, 1, Integer::sum);43 }44 45 public int visitsOf(UUID playerId) {46 return visits.getOrDefault(playerId, 0);47 }48 49 public int size() {50 return visits.size();51 }52}- The tracker keeps the plugin it was given.
finalmeans this field is set once, in the constructor, and never changes. - The counts: a map from each player's
UUID to a number. UUIDs never change, even when a player changes their name. Lists, sets and maps explains maps. - The constructor asks for the plugin. Nobody can create a
VisitTrackerwithout handing one over, so the field can never be missing. - Here the plugin is used: the file lives at
plugins/LifecycleDemo/visits.yml. - Reads the file. If it does not exist yet (the very first start),
loadConfigurationsimply returns an empty configuration, so the loop runs zero times. - Writes every count into a fresh YAML file.
yaml.savealso creates the plugin's folder if it is missing. - Saving a file can fail, for example when the disk is full. Java makes you handle that case. This code logs a clear error with the details instead of crashing.
- The plugin is used again, this time for its logger. Passing the exception as the last argument prints its full
stack trace , which you need to find the cause. - Adds 1 to the player's count, or starts at 1 if they have none, and returns the new count.
The listener and the command do not need the plugin at all. They only need the tracker, so that is what they ask for:
Where this file livesplugin-lifecycle-demosrcmainjavacomexamplepluginlifecycledemoVisitListener.java
The package com.example.pluginlifecycledemo is the folder path com/example/pluginlifecycledemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-demo/
- src/main/
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- LifecycleDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- VisitListener.javayou are hereListener: reacts to events
- VisitTracker.javaHelper class
- VisitsCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecycledemo;2 3import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;4import org.bukkit.entity.Player;5import org.bukkit.event.EventHandler;6import org.bukkit.event.Listener;7import org.bukkit.event.player.PlayerJoinEvent;8 9public final class VisitListener implements Listener {10 11 private final VisitTracker tracker;12 13 public VisitListener(VisitTracker tracker) {14 this.tracker = tracker;15 }16 17 @EventHandler18 public void onJoin(PlayerJoinEvent event) {19 Player player = event.getPlayer();20 int count = tracker.addVisit(player.getUniqueId());21 player.sendRichMessage("<gray>Welcome! This is visit number <gold><count></gold>.",22 Placeholder.unparsed("count", String.valueOf(count)));23 }24}- The listener's only dependency. It cannot do its job without one.
- The main class passes the tracker in when it writes
new VisitListener(tracker). this.trackeris the field; plaintrackeris the constructor's parameter. This line copies the parameter into the field so the handler can use it later.- Every join adds one visit and returns the new total.
- Fills the
<count>tag in the message with the number, as plain text.
Where this file livesplugin-lifecycle-demosrcmainjavacomexamplepluginlifecycledemoVisitsCommand.java
The package com.example.pluginlifecycledemo is the folder path com/example/pluginlifecycledemo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-demo/
- src/main/
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- LifecycleDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- VisitListener.javaListener: reacts to events
- VisitTracker.javaHelper class
- VisitsCommand.javayou are hereCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecycledemo/Package com.example.pluginlifecycledemo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecycledemo;2 3import io.papermc.paper.command.brigadier.BasicCommand;4import io.papermc.paper.command.brigadier.CommandSourceStack;5import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;6import org.bukkit.entity.Player;7 8public final class VisitsCommand implements BasicCommand {9 10 private final VisitTracker tracker;11 12 public VisitsCommand(VisitTracker tracker) {13 this.tracker = tracker;14 }15 16 @Override17 public void execute(CommandSourceStack source, String[] args) {18 if (!(source.getSender() instanceof Player player)) {19 source.getSender().sendRichMessage("<red>Only players can have visits.");20 return;21 }22 int count = tracker.visitsOf(player.getUniqueId());23 player.sendRichMessage("<gray>You have joined this server <gold><count></gold> times.",24 Placeholder.unparsed("count", String.valueOf(count)));25 }26}- The simplest kind of command. Commands, part 1 explains it fully.
- Same pattern as the listener: the command asks for the tracker in its constructor.
- The console can run commands too, but the console has no visits. This checks that the sender is a player and names it
playerin one step. - Reads the count without changing it.
Look at how the pieces fit together. The main class creates everything in onEnable and hands each object exactly what it needs:
Why is this the best way?
- You can see every dependency. Read a class's constructor and you know everything it needs. Nothing is hidden.
- The order is guaranteed. The listener cannot exist before the tracker, because it needs the tracker to be built.
- The listener and the command share one tracker. A visit added by the listener is visible to
/visitsright away, because there is only one map. - It grows well. When the plugin gets bigger, you keep doing the same thing. Organizing a bigger plugin builds on this.
The other two ways you will meet
Asking Paper for the plugin. JavaPlugin.getPlugin finds the running object of your main class from anywhere:
1LifecycleDemoPlugin plugin = JavaPlugin.getPlugin(LifecycleDemoPlugin.class);2plugin.getLogger().info("Found my plugin without a constructor.");It works, and it is handy in a small helper class that has no other way in. The downside is that the dependency is hidden inside a method, so you cannot see it from outside the class.
A static instance. Many tutorials store the plugin in a static field so any class can write MyPlugin.getInstance():
1private static LifecycleDemoPlugin instance;2 3public static LifecycleDemoPlugin getInstance() {4 return instance;5}with instance = this; as the first line of onEnable. It is quick to write, and you will see it in many popular plugins. Its problems show up later: every class can reach everything, so the code turns into a tangle; if any code calls getInstance() before onEnable runs, it gets null; and testing such classes is harder. Recognize it when you read it, and prefer constructors in your own code.
Disabling cleanly
When the plugin is disabled, Paper cleans up a lot for you, right after your onDisable returns:
- All your scheduled tasks are canceled. That is why LifecycleDemo never stops its autosave task itself.
- All your listeners are unregistered.
- Services and plugin messaging channels you registered are removed.
Paper cannot know what your data means, so these jobs are yours:
- Save data that only lives in memory, like the visit counts. Anything in a field disappears when the server stops.
- Close things you opened: database connections, open files, network connections.
- Clean up things you put in the world that should not stay there, such as temporary display entities, unless they should survive the restart.
One more rule: do not start new tasks in onDisable. Your plugin already counts as disabled at that point, and the scheduler refuses with this error:
org.bukkit.plugin.IllegalPluginAccessException: Plugin attempted to register task while disabledDo the final save right there, directly, like LifecycleDemo does.
When onEnable crashes
Sometimes onEnable throws an exception, which means something went wrong badly enough that Java stopped running the method. This plugin expects a world called lobby, but on a fresh server there is no such world:
Where this file livesplugin-lifecycle-crashsrcmainjavacomexamplepluginlifecyclecrashLobbyCrashPlugin.java
The package com.example.pluginlifecyclecrash is the folder path com/example/pluginlifecyclecrash inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-crash/
- src/main/
- java/com/example/pluginlifecyclecrash/Package com.example.pluginlifecyclecrash
- LobbyCrashPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecyclecrash/Package com.example.pluginlifecyclecrash
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecyclecrash;2 3import org.bukkit.Location;4import org.bukkit.World;5import org.bukkit.plugin.java.JavaPlugin;6 7public final class LobbyCrashPlugin extends JavaPlugin {8 9 private Location lobbySpawn;10 11 @Override12 public void onEnable() {13 World lobby = getServer().getWorld("lobby");14 lobbySpawn = lobby.getSpawnLocation();15 getLogger().info("Lobby spawn found at " + lobbySpawn.getBlockX() + ", " + lobbySpawn.getBlockZ());16 }17}- Looks up a world by name. If no world has that name,
getWorldreturnsnull: nothing. - The bug. Calling a method on
nullthrows aNullPointerException, andonEnablestops right here.
Here is what Paper prints (the stack trace is shortened):
[14:02:07 INFO]: [LobbyCrash] Enabling LobbyCrash v1.0.0[14:02:07 ERROR]: Error occurred while enabling LobbyCrash v1.0.0 (Is it up to date?)java.lang.NullPointerException: Cannot invoke "org.bukkit.World.getSpawnLocation()" because "lobby" is null at com.example.pluginlifecyclecrash.LobbyCrashPlugin.onEnable(LobbyCrashPlugin.java:14) ~[plugin-lifecycle-crash-1.0.0.jar:?] at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:280) ~[paper-api-26.3.build.142-beta.jar:?] ...[14:02:07 INFO]: [LobbyCrash] Disabling LobbyCrash v1.0.0Read it from the top. "Error occurred while enabling" says the crash happened in onEnable. The next line names the exception and, in plain words, the reason: lobby is null. The first at line points at your own file and line: LobbyCrashPlugin.java:14. (The "Is it up to date?" part is a generic hint Paper always adds; it does not mean anything is out of date.)
Then Paper disables the plugin. Everything onEnable registered before the crash is undone, onDisable runs, and the server keeps running without your plugin. /plugins lists it in red. The fix is to check for the problem and fail politely, with a message the server owner can act on:
1World lobby = getServer().getWorld("lobby");2if (lobby == null) {3 getLogger().severe("There is no world called lobby. Create it, then restart the server.");4 getServer().getPluginManager().disablePlugin(this);5 return;6}disablePlugin(this) switches your own plugin off on purpose, and return leaves onEnable so the rest of the setup does not run. Null, exceptions and stack traces teaches stack traces in depth, and the Error Doctor explains any error you paste into it.
What about onLoad?
Most plugins never need onLoad. It runs before the worlds load and before any plugin is enabled, which makes many things impossible there. For example, registering a listener in onLoad fails, because the plugin is not enabled yet:
org.bukkit.plugin.IllegalPluginAccessException: Plugin attempted to register com.example.MyListener@6d1e7682 while not enabledUse it only when something must happen before other plugins enable. The classic case is another plugin's documentation telling you to: WorldGuard, for example, asks plugins to register custom region flags in onLoad. If you want to run code before the worlds load for your own reasons, read about load: STARTUP in plugin.yml explained and about the bootstrapper in Paper plugins.
The whole demo
- plugin-lifecycle-demo/
- src/
- main/
- java/
- com/example/pluginlifecycledemo/
- LifecycleDemoPlugin.javaThe main class: creates the tracker, registers the listener and command, saves on disable
- VisitTracker.javaHolds the visit counts and loads and saves visits.yml
- VisitListener.javaAdds a visit and greets the player on every join
- VisitsCommand.java/visits shows the player's count
- com/example/pluginlifecycledemo/
- resources/
- plugin.ymlName, version, main class and api-version
- java/
- main/
- src/
After the test server has run once and someone has joined, the plugin's data folder holds the saved file:
- run/The test server's folder inside your project
- plugins/
- LifecycleDemo/The data folder, named after the plugin
- visits.ymlOne line per player: their UUID and their visit count
- LifecycleDemo/The data folder, named after the plugin
- plugins/
Download the LifecycleDemo project to run it, or open it in the Compile Lab to change it in your browser.
Mistakes to avoid
- Doing setup in the main class's constructor or in field initializers. Paper creates your main class long before the server is ready: no worlds exist and your plugin is not enabled, so registering things fails. Put setup in
onEnable. - Creating your main class with
new.new LifecycleDemoPlugin()anywhere in your code throws an exception, because Paper only allows the one object it made itself. Passthisaround instead. - Forgetting to save in
onDisable. Everything in fields is gone after a restart. - Saving only on quit. Players online during shutdown never trigger your quit handler. Save them in
onDisabletoo. - Using
/reloadto test. Restart the server instead. - Printing with
System.out.println. UsegetLogger(), so every line shows your plugin's name.
Log how long the plugin ran
Write a plugin called UptimeLogger whose onDisable prints how many minutes the plugin was enabled, for example [UptimeLogger] I was enabled for 42 minutes.
Hint 1
System.currentTimeMillis() returns the current time as a number of milliseconds. Save it somewhere in onEnable, read it again in onDisable, and subtract.
Hint 2
The saved time must survive from one method to the other, so it has to be a field, not a variable inside onEnable. There are 60,000 milliseconds in a minute.
Show the solution
The start time goes in a long field. onDisable subtracts it from the current time and divides by 60,000. Integer division drops the remainder, so 2 minutes and 50 seconds shows as 2 minutes.
Where this file livesplugin-lifecycle-exercisesrcmainjavacomexamplepluginlifecycleexerciseUptimePlugin.java
The package com.example.pluginlifecycleexercise is the folder path com/example/pluginlifecycleexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-exercise/
- src/main/
- java/com/example/pluginlifecycleexercise/Package com.example.pluginlifecycleexercise
- UptimePlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- VersionGreeter.javaListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecycleexercise/Package com.example.pluginlifecycleexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecycleexercise;2 3import org.bukkit.plugin.java.JavaPlugin;4 5public final class UptimePlugin extends JavaPlugin {6 7 private long enabledAtMillis;8 9 @Override10 public void onEnable() {11 enabledAtMillis = System.currentTimeMillis();12 getServer().getPluginManager().registerEvents(new VersionGreeter(this), this);13 }14 15 @Override16 public void onDisable() {17 long minutes = (System.currentTimeMillis() - enabledAtMillis) / 60_000;18 getLogger().info("I was enabled for " + minutes + " minutes.");19 }20}- A field, so both lifecycle methods can see it.
- Remembers the moment the plugin started.
- Milliseconds to minutes. The underscore is just a separator that makes big numbers easier to read.
Replace the static instance
A friend's listener reads the plugin version through a static instance:
1@EventHandler2public void onJoin(PlayerJoinEvent event) {3 String version = UptimePlugin.getInstance().getPluginMeta().getVersion();4 event.getPlayer().sendRichMessage("<gray>This server runs UptimeLogger <green>" + version);5}Rewrite VersionGreeter so it gets the plugin through its constructor instead, and register it in UptimePlugin's onEnable.
Hint 1
Give the listener a private final UptimePlugin plugin; field and a constructor that takes an UptimePlugin.
Hint 2
In onEnable, create the listener with new VersionGreeter(this): this is the plugin.
Show the solution
The listener asks for the plugin in its constructor and keeps it in a field. Now getInstance() and the static field can be deleted from the main class. The solution also uses a placeholder instead of joining strings, so the version is inserted as plain text.
Where this file livesplugin-lifecycle-exercisesrcmainjavacomexamplepluginlifecycleexerciseVersionGreeter.java
The package com.example.pluginlifecycleexercise is the folder path com/example/pluginlifecycleexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- plugin-lifecycle-exercise/
- src/main/
- java/com/example/pluginlifecycleexercise/Package com.example.pluginlifecycleexercise
- UptimePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- VersionGreeter.javayou are hereListener: reacts to events
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/pluginlifecycleexercise/Package com.example.pluginlifecycleexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.pluginlifecycleexercise;2 3import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;4import org.bukkit.event.EventHandler;5import org.bukkit.event.Listener;6import org.bukkit.event.player.PlayerJoinEvent;7 8public final class VersionGreeter implements Listener {9 10 private final UptimePlugin plugin;11 12 public VersionGreeter(UptimePlugin plugin) {13 this.plugin = plugin;14 }15 16 @EventHandler17 public void onJoin(PlayerJoinEvent event) {18 event.getPlayer().sendRichMessage("<gray>This server runs UptimeLogger <green><version></green>.",19 Placeholder.unparsed("version", plugin.getPluginMeta().getVersion()));20 }21}- The dependency is now visible in the constructor.
- Uses the field instead of a static lookup.
The registration line in UptimePlugin, shown in the first exercise's solution, is registerEvents(new VersionGreeter(this), this).
Recap
- Your main class
extends JavaPlugin, is named inplugin.yml, and is created once by Paper. onLoadruns very early and is rarely needed;onEnablesets everything up;onDisablesaves and closes.- In
onEnable: load data, create helpers, register listeners and commands, start tasks. - Pass the plugin, or just the object a class needs, through constructors. Recognize
getPluginand static instances, but prefer constructors. - Paper cancels your tasks and unregisters your listeners; you save data and close connections.
- If
onEnablethrows, Paper logs the error and disables your plugin. Check for problems and fail with a clear message.
Quick quiz
Where should you register your listeners?
Listeners are registered inonEnable. Paper refuses to register them inonLoadbecause the plugin is not enabled yet, and the constructor runs even earlier.Your plugin saves each player's data in a
PlayerQuitEventhandler. What happens to players who are online when the server stops?Plugins are disabled before players are disconnected, so your quit handler is already unregistered when they leave. Save every online player's data inonDisabletoo.Your plugin starts a repeating task with
runTaskTimer. Do you have to cancel it inonDisable?AfteronDisable, Paper cancels every task your plugin scheduled and unregisters its listeners. You only cancel a task yourself when you want it to stop earlier.A listener needs your plugin's logger. Which way does this guide recommend?
Constructor injection makes the dependency visible and guarantees it exists. Creating your main class withnewfails, because Paper makes the only one. The static instance works but hides dependencies and can returnnulltoo early.The console shows "Error occurred while enabling MyPlugin v1.0.0 (Is it up to date?)". What happened?
The message meansonEnablecrashed. "Is it up to date?" is a generic hint Paper always adds. The lines below it name the exception and the line of your code that caused it.
Next steps
- plugin.yml explained: every field Paper reads before it creates your main class.
- Events and listeners: what to register in
onEnable. - Timing and tasks: more about the repeating task that LifecycleDemo uses for autosaves.
- Saving player data: bigger and safer ways to store data like visit counts.