When starting the server:
- PaperClassloaderBytecodeModifier (server) is initialized, this is set in the ClassloaderBytecodeModifier (api) singleton class.
- Providers are created and loaded by a ProviderSource.
Providers provider = Providers.INSTANCE;
provider.addProviders(DirectoryProviderSource.INSTANCE, pluginfile.toPath());
DirectoryProviderSource (context: a directory path), FileProviderSource (context: a path), PluginFlagProviderSource (context, a list of files) Provider sources are given a context parameter, where it is responsible for loading providers depending on the context.
public class Providers {
private static final Logger LOGGER = Logger.getLogger("PluginLoading"); // Name for easier understanding
public static final Providers INSTANCE = new Providers();
public <C> void addProviders(ProviderSource<C> source, C context) {
try {
source.registerProviders(ServerEntryPointHandler.INSTANCE, context);
} catch (Throwable e) {
LOGGER.log(Level.SEVERE, e.getMessage(), e);
}
}
}
So in this case, the DirectoryProviderSource is given the plugin directory as context to load all providers in that directory.
These provider sources then load providers into EntrypointHandlers.
public interface EntrypointHandler {
void enter(Entrypoint<?> entrypoint);
<T extends PluginProvider<?>> void register(Entrypoint<T> entrypoint, T provider);
}
The current entrypoints are BOOTSTRAPPER and PLUGIN.
The reason for needing this bootstrap is because of dynamic plugin loading because of stuff like the api.
RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler(this);
try {
FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, file.toPath());
runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
} catch (SerializationException |
InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception
throw new InvalidPluginException(ex);
} catch (Throwable e) {
throw new InvalidPluginException(e);
}
This allows us to capture stored providers via the entry handler. The special thing about RuntimePluginEntrypointHandler is it does not allow BOOTSTRAP entrypoint, this is to prevent the loading of paper plugins with initilizers... since we can't load those during runtime.
For the implementation of ServerEntryPointHandler registering these entry points will be put into ProviderStorage.
public class ServerEntryPointHandler implements EntrypointHandler {
public static final ServerEntryPointHandler INSTANCE = new ServerEntryPointHandler();
...
public ServerEntryPointHandler() {
this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage());
}
@Override
public void enter(Entrypoint<?> entrypoint) {
ProviderStorage<?> storage = this.storage.get(entrypoint);
...
storage.enter();
}
@Override
public <T extends PluginProvider<?>> void register(Entrypoint<T> entrypoint, T provider) {
ProviderStorage<T> providerStorage = this.get(entrypoint);
...
providerStorage.register(provider);
}
public interface ProviderStorage<T extends PluginProvider<?>> {
void register(T provider);
void enter();
Iterable<T> getRegisteredProviders();
}
Provider storage is meant to store multiple providers of the same type together. This is important when for example providers depend on eachother and must load in a certain order. Provider storage is typically responsible for creating the JavaPlugins/PluginBootstrappers and properly registering/adding them.
The ServerPluginProviderStorage has two options for loading plugins, ModernPluginLoadingStrategy/LegacyPluginLoadingStrategy. These are how they handle cyclic dependencies and stuff. But generally, load plugin providers depending on their dependencies.
But, going back to ServerEntryPointHandler... in many places the singleton will be called and the entrypoint will be executed. This will cause all providers in the desired plugin provider storage to be loaded.
io.papermc.paper.plugin.provider.service.entrypoint.ServerEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.provider.service.entrypoint.Entrypoint.BOOTSTRAPPER);
Providers are only currently created by the FileProviderSource, where it tries to find a PluginFileType. These plugin file types are currently: PAPER, SPIGOT. These are identified but what file is first found (paper-plugin.yml, then plugin.yml)
public static final PluginFileType<SpigotPluginProvider> SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) {
@Override
protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) {
entrypointHandler.register(Entrypoint.PLUGIN, provider);
}
};
For example, this is the spigot file type that is loaded into the PLUGIN entrypoint.
More complicated providers, like the paper provider, registers a bit more...
public static final PluginFileType<PaperParentProvider> PAPER = new PluginFileType<>("paper-plugin.yml", PaperParentProvider.FACTORY) {
@Override
protected void register(EntrypointHandler entrypointHandler, PaperParentProvider provider) {
PaperPluginProvider parent = provider.createInstance();
if (parent.shouldCreateBootstrap()) {
PaperPluginProvider.PaperBootstrapProvider bootstrapPluginProvider = parent.createBootstrapProvider();
entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider);
entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider));
} else {
entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider());
}
}
};
So, there are currently two types of main providers: PaperParentProvider and SpigotPluginProvider.
These are very simple, and simply provide a JavaPlugin.
This represents a complicated sharing process between multiple providers. A PaperParentProvider provides a PaperPluginProvider (new name? appreciated).
@Override
public PaperPluginProvider createInstance() {
PaperClasspathBuilder builder = new PaperClasspathBuilder();
if (this.description.getLoader() != null) {
try (
PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(this.path, this.jarFile, this.description, this.getClass().getClassLoader())
) {
PluginLoader loader = ProviderUtil.loadClass(this.description.getLoader(), PluginLoader.class, simplePluginClassLoader);
loader.classloader(builder);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return new PaperPluginProvider(this.path, this.jarFile, this.description, builder.buildClassLoader(this), this.logger);
}
A PaperParentProvider is responsible for creating the inital classpath that the plugin will use. After, a PaperPluginProvider is created. Now, this by itself is not a provider but holds to inner classes: PaperBootstrapProvider & PaperServerPluginProvider. So although they are two separate providers, they both share a parent PaperPluginProvider which allows them to share a classloader, configuration, file, etc.
Now, going back to how they are stored.
After this, plugins are loaded! Yay! Note I am still working on making the implementation of SimplePluginManager cleaner... I have to hack it on and ugh, it's a mess.
This is now a middle class between the paper plugin configuration and the bukkit plugin description. Paper plugin configs are created using configurate.
Paper config exclusives contain:
- loader
- bootstrapper
- plugin command permission messages support minimessage
Bukkit plugin config exclusives contain:
- classLoaderOf: unused, not implemented in paper config
- awareness: unused, not implemented in paper config
- libraries: it is encouraged to use the runtime plugin loader instead for paper
Currently spigot stores the main classloader storage in the JavaPluginLoader. Paper eliminates that and is moving all that logic away from that class. There is now a PaperPluginClassLoaderStorage singleton, which holds ConfiguredPluginClassLoaders (both bukkit & paper classloader classes implement this)
@ApiStatus.Internal
public interface ConfiguredPluginClassLoader {
PluginConfiguration getConfiguration();
Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException;
// Called in the constructor of javaplugin, at the very top
void init(JavaPlugin plugin);
}
The implementation of PaperPluginClassLoaderStorage is pretty much identical to what the logic was in JavaPluginLoader, it's just moved to a nicer place and allows for multiple different classloaders (Paper Classloader is not in api).
Current paper classloader implementations are PaperSimplePluginClassLoader -> PaperPluginClassLoader (ConfiguredPluginClassLoader). The PaperSimplePluginClassLoader is the simplest paper classloader, and is currently used in the PaperLoader (this has an isolated classloader). This is basically a URLClassLoader that supports the paper ClassloaderBytecodeModifier. PaperPluginClassLoader then also has a libraryLoader and PaperPluginClassLoaderStorage. This allows it to refer to other classloaders (plugins) and resolve their own libraries resolved in their PluginLoader.
public class TestPluginLoader implements PluginLoader {
@Override
public void classloader(PluginClasspathBuilder classpathBuilder) {
classpathBuilder.addLibrary(new JarLibrary(Path.of("bob.jar")));
MavenLibraryResolver resolver = new MavenLibraryResolver();
resolver.addDependency(new Dependency(new DefaultArtifact("com.owen1212055:particlehelper:1.0.0-SNAPSHOT"), null));
resolver.addRepository(new RemoteRepository.Builder("bytecode", "default", "https://repo.bytecode.space/repository/maven-public/").build());
resolver.setFilter(new DependencyFilter() {
@Override
public boolean accept(DependencyNode node, List<DependencyNode> parents) {
return true;
}
});
classpathBuilder.addLibrary(resolver);
}
}
Plugin loaders are mostly all implemented on the api side, where ClassPathLibrarys, for example the MavenLibraryResolver and JarLibrary are meant to register jars into a LibraryStore.``
public interface LibraryStore {
void addLibrary(@NotNull Path library);
}
The PluginClasspathBuilder only supports adding ClassPathLibraries into themselves.
public PaperPluginClassLoader buildClassLoader(PluginProvider<?> provider) {
PaperLibraryStore paperLibraryStore = new PaperLibraryStore();
for (ClassPathLibrary library : this.libraries) {
library.addToLibraryStore(paperLibraryStore);
}
List<Path> paths = paperLibraryStore.getPaths();
URL[] urls = new URL[paths.size()];
for (int i = 0; i < paths.size(); i++) {
Path path = paperLibraryStore.getPaths().get(i);
try {
urls[i] = path.toUri().toURL();
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
try {
return new PaperPluginClassLoader(provider.getLogger(), provider.getSource(), provider.file(), provider.getConfiguration(), this.getClass().getClassLoader(), new URLClassLoader(urls));
} catch (IOException exception) {
throw new RuntimeException(exception);
}
}
We then build a paper plugin classloader using these libraries by creating our own paper library store (server) and grabbing all the URLS of the jars.
The plugin bootstrap currently supports supplying your own custom java plugin (must be the same as defined in paper-plugin.yml:main... not sure if I will keep this restriction)
public interface PluginBootstrap {
void boostrap(@NotNull PluginBootstrapContext context);
@NotNull
default JavaPlugin createPlugin(@NotNull PluginBootstrapContext context) {
return ProviderUtil.loadClass(context.getConfiguration().getMain(), JavaPlugin.class, this.getClass().getClassLoader());
}
}
This allows you to pass context into your javaplugin constructor from your plugin bootstrap class.
Each method provides a context object.
@ApiStatus.NonExtendable
public interface PluginBootstrapContext {
@NotNull
PluginConfiguration getConfiguration();
@NotNull
Path getDataDirectory();
@NotNull
Path getConfigurationFile();
@NotNull
Logger getLogger();
}
This allows you to resolve things like config/data info early, which is important inorder to register custom things via the bootstrap method.
Api exposure is currently very low, I try to keep it so that there is very little of this internal implementation shown... because that is what makes the current plugin system so hard to change. The classes in yellow are marked as internal. As you can see, most of this is internal...