[SPIGOT-6621] ClassLoader Is Closing Jar Files Too Early (Caused by Auto-Library Change) Created: 03/Jul/21  Updated: 11/Jul/21

Status: Open
Project: Spigot
Component/s: None
Affects Version/s: None
Fix Version/s: None

Type: Bug Priority: Minor
Reporter: Mick Monkey Assignee: Unassigned
Resolution: Unresolved Votes: 0
Labels: None

Version: CraftBukkit version 3168-Spigot-a483d2c-bc00005 (MC: 1.17) (Implementing API version 1.17-R0.1-SNAPSHOT)
Guidelines Read: Yes

 Description   

tl;dr: After the "automatic library support" commit to Bukkit, the plugin class loader now completely closes and uncaches all plugins immediately on shutdown, making them inaccessible in onDisable() in places they previously were accessible.
This is a breaking bug for Citizens.

The loose summary of how things are laid out is:

  • Citizens is the base plugin, that provides NPCs
  • Other plugins (such as Sentinel, which will be the example) function as addons, that add additional features to NPCs
  • Sentinel has a Depend on Citizens listed in its plugin.yml, so that it loads immediately after Citizens and can register its addon data into Citizens
  • The secondary result of depends is that shutdown order is reversed - Sentinel shuts down before Citizens does.
  • Citizens auto-saves all data in its onDisable()
  • Because Sentinel has registered addon data into Citizens, Citizens must save Sentinel's data during its save cycle.
  • This means that in the Citizens onDisable(), Citizens must reference the Sentinel classes.

On earlier versions (Spigot 1.16.5 builds dated prior to May 14th, or 1.15.2/etc older versions, throughout basically all history back to 2012 Bukkit versions when Citizens was started), this worked as expected.
However, starting approximately May 14th, when the "automatic library support" commits were published - ref
https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/commits/146a7e4bd764990c56bb326643e92eb69f24d27e#src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java - this stopped functioning. References from Citizens to Sentinel classes now fail with a classloader error:

[06:22:01] [Server thread/ERROR]: Error occurred while disabling Citizens v2.0.28-SNAPSHOT (build 2333) (Is it up to date?)
java.lang.IllegalStateException: zip file closed
at java.util.zip.ZipFile.ensureOpen(ZipFile.java:828) ~[?:?]
at java.util.zip.ZipFile.getEntry(ZipFile.java:327) ~[?:?]
at java.util.jar.JarFile.getEntry(JarFile.java:513) ~[?:?]
at java.util.jar.JarFile.getJarEntry(JarFile.java:468) ~[?:?]
at org.bukkit.plugin.java.PluginClassLoader.findClass(PluginClassLoader.java:154) ~[spigot-1.17.jar:3168-Spigot-a483d2c-bc00005]
at java.lang.ClassLoader.loadClass(ClassLoader.java:586) ~[?:?]
at org.bukkit.plugin.java.PluginClassLoader.loadClass0(PluginClassLoader.java:104) ~[spigot-1.17.jar:3168-Spigot-a483d2c-bc00005]
at org.bukkit.plugin.java.PluginClassLoader.loadClass(PluginClassLoader.java:99) ~[spigot-1.17.jar:3168-Spigot-a483d2c-bc00005]
at java.lang.ClassLoader.loadClass(ClassLoader.java:519) ~[?:?]
at java.lang.Class.forName0(Native Method) ~[?:?]
at java.lang.Class.forName(Class.java:466) ~[?:?]
at sun.reflect.generics.factory.CoreReflectionFactory.makeNamedType(CoreReflectionFactory.java:114) ~[?:?]
at sun.reflect.generics.visitor.Reifier.visitClassTypeSignature(Reifier.java:125) ~[?:?]
at sun.reflect.generics.tree.ClassTypeSignature.accept(ClassTypeSignature.java:49) ~[?:?]
at sun.reflect.annotation.AnnotationParser.parseSig(AnnotationParser.java:440) ~[?:?]
at sun.reflect.annotation.AnnotationParser.parseAnnotation2(AnnotationParser.java:242) ~[?:?]
at sun.reflect.annotation.AnnotationParser.parseAnnotations2(AnnotationParser.java:121) ~[?:?]
at sun.reflect.annotation.AnnotationParser.parseAnnotations(AnnotationParser.java:73) ~[?:?]
at java.lang.reflect.Field.declaredAnnotations(Field.java:1205) ~[?:?]
at java.lang.reflect.Field.declaredAnnotations(Field.java:1203) ~[?:?]
at java.lang.reflect.Field.getAnnotation(Field.java:1170) ~[?:?]
at net.citizensnpcs.api.persistence.PersistenceLoader.getFieldsFromClass(PersistenceLoader.java:361) ~[?:?]
at net.citizensnpcs.api.persistence.PersistenceLoader.getFields(PersistenceLoader.java:345) ~[?:?]

General hoped-for solution would be correcting it to not unload plugin jars until after ALL plugins are shutdown, rather than individual unloading each jar as the plugin unloads.



 Comments   
Comment by Jan Boerman [ 11/Jul/21 ]

Perhaps a good solution here would be introduce an onUnload() method to Plugin.java as a dual of onLoad().

The plugin's lifecycle would then look as follows:

  1. The plugin is constructed
  2. The plugin's onLoad() method is called (dependencies are already loaded at this point)
  3. The plugin's onEnable() method is called (every other plugin is already loaded at this point, but not necessarily enabled)
  4. Server is running
  5. The plugin's onDisable() method is called (other plugins that depend on this plugin are already disabled, but still classloaded)
  6. The plugin's onUnload() method is called (other plugins that depend on this plugin are not classloaded anymore)

This way Citizens will still be able to access classes from the addons in the onDisable method.

Obviously JavaPlugin can implement the onUnload() method using an empty body, or a default method on the Plugin interface could be used.

Comment by Mick Monkey [ 05/Jul/21 ]

If Sentinel is disabled outside of a full server shutdown, that is erroneous. Any users that intentionally caused that to happen would be told "yeah obviously that broke it what did you expect". Same for Citizens, same for any&every other Citizens addon (I note, a lot of plugins function in whole or in part as Citizens addons, and they're all broken by this change)... same for most plugins. There's a reason for the giant red warning labels on "/reload", and a reason that plugins like Essentials even actively add extra warning messages when a reload is done.

I would be shocked if Citizens was the only case of this change breaking. I know for a fact a lot of plugins have two-way interoperation akin to Citizens. It's a core part of how plugin development works.

The PluginDisableEvent wouldn't really help - when Citizens shuts down, it saves all data for each NPC... some of that data is supplied by Sentinel (or whichever addon). It needs to be able to save that data. The only way to use PluginDisableEvent here would be for Citizens to save and shutdown the moment any plugin that depends on Citizens is disabled... which would be an awkward hackaround, not a solution - and likely to create its own new issues. (Individual unhooking doesn't work - everything saves at once into one file, it can't be separated... separating saves per plugin would be a massive scale of rewrite for literally hundreds of developers (Citizens is primarily an API for other plugins to add onto, rather than a full-featured system on its own, and a lot of plugin developers have used it - including a lot of small/individual developers, running their own servers and using Citizens as an API to power things like NPC-driven quests unique to their server... every last one of them would need to build their own systems for separated saving), that would also introduce significant bugs on top of invalidating all the convenience/cleanliness/stability/bug-prevention/etc. benefits of the unified save system).

"Plugin jars are still loaded in memory before the shutdown finishes" doesn't seem like it should be undefined behavior - especially for plugins that are explicitly interoperating with each other, and have even marked that via the Depends system.
Note that I'm not saying they have to remain loaded forever after disabling, just like... if all plugins are being disabled (as happens during shutdown, or even the "/reload" command), disable them all first, THEN unload, rather than actively unloading individually after disabling. I don't think individual unloading solves any issues, but it absolutely does create issues.

Comment by md_5 [ 03/Jul/21 ]

I disagree this is a bug.

There is a reason disables are done in reverse-depend order and we have PluginDisableEvent — so plugins can unhook each other.

Right now you are accessing a disabled plugin when you should not be. You can easily see how this would be an issue if you consider the scenario where the server is not reloading or stopping, but merely the Sentinel plugin is disabled — it would be disabled yet you would still be trying to use it during normal server operations.

Just because there was some old undefined behaviour for a number of years, doesn't mean it was correct.

Comment by Black Hole [ 03/Jul/21 ]

Please consider using https://hub.spigotmc.org/javadocs/spigot/org/bukkit/event/server/PluginDisableEvent.html as a workaround.

Generated at Tue Apr 22 05:09:39 UTC 2025 using Jira 10.3.5#10030005-sha1:190c783f2bd6c69cd5accdb70f97e48812a78d14.