Paper Plugin Guide
File mode0

Paper essentials

The main class and plugin lifecycle

onLoad, onEnable and onDisable, and how the rest of your code gets hold of your plugin.

Beginner38 min read

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:

MethodWhen Paper calls itWhat 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.
When the server starts When the server stops Paper reads your plugin.yml and finds the main class it names Paper creates one object of it you never write new MyPlugin() yourself onLoad() very early: no worlds yet, rarely needed The worlds load world, world_nether, world_the_end onEnable() load data, register listeners and commands "Done!" in the console players can join, your code reacts Someone types stop players are still online at this point onDisable() save data, close files and connections Paper cleans up after you cancels your tasks, removes your listeners Players are saved and disconnected your quit listener no longer runs Worlds are saved then the Java program ends
The order of events when the server starts and stops. The purple boxes are the methods you write; Paper does everything else.

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:

HelloLifecyclePlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The main class. extends JavaPlugin gives it everything a plugin needs: a logger, a data folder, access to the server and the three lifecycle methods. final just means no other class may extend this one.
  2. Paper calls this first, before any world is loaded. This plugin only prints a line, which is all most plugins would ever do here.
  3. The numbers in the messages make the order easy to see in the console.
  4. Paper calls this when the plugin starts. The worlds exist now, players can join soon, and this is where real plugins set everything up.
  5. getPluginMeta() gives you the information from your plugin.yml: name, version, authors, description and more. Reading the version from here means you never have to type it twice.
  6. Your plugin's own folder for files, such as config.yml. Paper does not create it until something saves a file there.
  7. getServer() is the server itself. From it you reach players, worlds, the scheduler, the plugin manager and almost everything else.
  8. A warning: something is odd, but the plugin keeps working.
  9. An error: something is broken. Paper prints it in red.
  10. 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:

Server console: starting
[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):

Server console: stopping
 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 worlds

onDisable 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:

MethodWhat 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?"

Logger levels
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:

Two ways to reach the server
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:

  1. Load settings and data: saveDefaultConfig(), read files.
  2. Create your helper objects: managers, trackers, caches.
  3. Register listeners and commands, so Paper starts calling your code.
  4. 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:

LifecycleDemoPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Five minutes in ticks: 5 minutes times 60 seconds times 20 ticks per second. The L makes it a long, the number type the scheduler wants.
  2. A field that will hold the tracker. It is a field, not a variable inside onEnable, because onDisable needs the same object later to save it.
  3. Only a log line. Nothing else here needs to run before the worlds exist.
  4. Job 2: create the helper object. this is your plugin object, handed to the tracker so it can find the data folder and the logger.
  5. Job 1: load the saved counts from visits.yml before anyone can join.
  6. Job 3: register the listener. It gets the tracker, because counting visits is its whole job.
  7. Also job 3: registers /visits. registerCommand is a shortcut JavaPlugin gives you for simple commands, and it only works inside onEnable.
  8. 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.
  9. A short "I am ready" line with a useful number. Server owners love these when something goes wrong.
  10. If onEnable crashed before it created the tracker, the field is still null. Paper still calls onDisable after a crash, so this check stops a second crash.
  11. 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:

Steve joined the game
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.

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:

VisitTracker.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The tracker keeps the plugin it was given. final means this field is set once, in the constructor, and never changes.
  2. 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.
  3. The constructor asks for the plugin. Nobody can create a VisitTracker without handing one over, so the field can never be missing.
  4. Here the plugin is used: the file lives at plugins/LifecycleDemo/visits.yml.
  5. Reads the file. If it does not exist yet (the very first start), loadConfiguration simply returns an empty configuration, so the loop runs zero times.
  6. Writes every count into a fresh YAML file. yaml.save also creates the plugin's folder if it is missing.
  7. 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.
  8. 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.
  9. 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:

VisitListener.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The listener's only dependency. It cannot do its job without one.
  2. The main class passes the tracker in when it writes new VisitListener(tracker).
  3. this.tracker is the field; plain tracker is the constructor's parameter. This line copies the parameter into the field so the handler can use it later.
  4. Every join adds one visit and returns the new total.
  5. Fills the <count> tag in the message with the number, as plain text.
VisitsCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The simplest kind of command. Commands, part 1 explains it fully.
  2. Same pattern as the listener: the command asks for the tracker in its constructor.
  3. The console can run commands too, but the console has no visits. This checks that the sender is a player and names it player in one step.
  4. 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:

LifecycleDemoPlugin the main class Paper made builds everything in onEnable VisitTracker holds the counts, saves the file VisitListener adds a visit on every join VisitsCommand shows the count on /visits new VisitTracker(this) new VisitListener(tracker) new VisitsCommand(tracker) arrows: "creates and hands over" dashed: "uses the same tracker"
The main class is the wiring hub. It creates the tracker, then gives that same tracker to the listener and the command.

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 /visits right 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:

Asking Paper for your plugin
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():

The static instance pattern
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:

Server console
org.bukkit.plugin.IllegalPluginAccessException: Plugin attempted to register task while disabled

Do 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:

LobbyCrashPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Looks up a world by name. If no world has that name, getWorld returns null: nothing.
  2. The bug. Calling a method on null throws a NullPointerException, and onEnable stops right here.

Here is what Paper prints (the stack trace is shortened):

Server console
[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.0

Read 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:

Fail politely instead
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:

Server console
org.bukkit.plugin.IllegalPluginAccessException: Plugin attempted to register com.example.MyListener@6d1e7682 while not enabled

Use 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
        • resources/
          • plugin.ymlName, version, main class and api-version

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

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. Pass this around 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 onDisable too.
  • Using /reload to test. Restart the server instead.
  • Printing with System.out.println. Use getLogger(), so every line shows your plugin's name.
Try it

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.

UptimePlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. A field, so both lifecycle methods can see it.
  2. Remembers the moment the plugin started.
  3. Milliseconds to minutes. The underscore is just a separator that makes big numbers easier to read.
Try it

Replace the static instance

A friend's listener reads the plugin version through a static instance:

VersionGreeter.java (your friend's version)
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.

VersionGreeter.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The dependency is now visible in the constructor.
  2. 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 in plugin.yml, and is created once by Paper.
  • onLoad runs very early and is rarely needed; onEnable sets everything up; onDisable saves 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 getPlugin and static instances, but prefer constructors.
  • Paper cancels your tasks and unregisters your listeners; you save data and close connections.
  • If onEnable throws, Paper logs the error and disables your plugin. Check for problems and fail with a clear message.

Quick quiz

  1. Where should you register your listeners?

  2. Your plugin saves each player's data in a PlayerQuitEvent handler. What happens to players who are online when the server stops?

  3. Your plugin starts a repeating task with runTaskTimer. Do you have to cancel it in onDisable?

  4. A listener needs your plugin's logger. Which way does this guide recommend?

  5. The console shows "Error occurred while enabling MyPlugin v1.0.0 (Is it up to date?)". What happened?

Next steps