Class ShimLoader

java.lang.Object
com.kamikazejam.kamicommon.nms.text.ShimLoader

@Internal public final class ShimLoader extends Object
Loads the relocated Adventure copy from internal-libs/adventure.jar inside this jar, in a CHILD classloader.

Why a nested jar rather than shading it flat. The relocated Adventure exists only for servers with no native Adventure, meaning everything below 1.18.2. Shading it as ordinary class entries made it importable by every consumer, and dependency scoping cannot fix that: measured 2026-08-30, a spigot-jar consumer compiled against the shaded Adventure successfully even with the dependency declared runtime-only, because scope metadata cannot hide bytes that are physically present. Java's classpath has no nested-jar support, so entries inside this jar are not classpath entries and javac cannot reach them. That survives a consumer shading this library, which is the property scoping could not provide.

Why a child loader rather than adding it to the plugin's own. Injecting into the plugin classloader needs URLClassLoader.addURL, and setAccessible on it fails on Java 16 and up. The only route that still works reaches MethodHandles.Lookup.IMPL_LOOKUP through sun.misc.Unsafe, which Java 25 already warns is scheduled for removal. A child loader uses only URLClassLoader and parent delegation, neither deprecated.

How the boundary works. Delegation is parent-first, so the child sees everything the plugin classloader has and can implement VersionedComponent, whose Class object it resolves by delegating upward. The parent cannot see into the child, so no plugin, no consumer and no downstream plugin can reach the relocated Adventure. Instances cross the boundary as VersionedComponent, which names nothing relocated.

Verified on 1.8.8, 1.16.5, 1.20.4, 1.21.11 and 26.2, across Java 8, 17, 21 and 25, including a three-plugin topology where one plugin shades this library, a second depends on that plugin, and a third shades its own copy.

  • Method Details

    • configure

      public static void configure(@NotNull @NotNull File dataFolder)
      Where to extract the nested jar. Call this before anything touches text, from the plugin's onEnable, passing the plugin's data folder.

      The default is the system temp directory, which works on a developer machine and is the wrong answer on hosted servers, where temp is routinely mounted noexec or read-only. The failure that produces does not look like it has anything to do with Adventure.

    • get

      @NotNull public static @NotNull ClassLoader get()
      The loader, extracting into whatever configure(File) was given, or temp.
    • get

      @NotNull public static @NotNull ClassLoader get(@NotNull @NotNull File cacheDir)
      The classloader holding the relocated Adventure and the implementations that use it.
      Parameters:
      cacheDir - where to place the extracted jar. Use the plugin's data folder rather than the system temp directory: hosted servers routinely mount temp noexec or read-only, and the failure that produces is not obviously about Adventure at all.
    • leakedToParent

      public static boolean leakedToParent()
      Whether the relocated Adventure is reachable from the classloader that loaded this class.

      It must be false. If it is true the nested jar has been unpacked into loose classes somewhere in the chain and every consumer can import Adventure again.