From 66af0a3e3cfc04a02c33ebbfb0fc390d35d42a96 Mon Sep 17 00:00:00 2001 From: Bukkit/Spigot Date: Sat, 15 May 2021 09:04:53 +1000 Subject: [PATCH] SPIGOT-5345: Add automatic library support Example plugin.yml usage: ``` libraries: - com.squareup.okhttp3:okhttp:4.9.0 ``` Libraries will only be accessible to plugins and their transitive depends, allowing for multiple versions of the same library to be used by different plugins. This feature is modeled on the parallel BungeeCord feature and intends to offer the same behaviour. Although this is a preview feature, major changes to behaviour are not expected at this point. With the exception of the issues described in SPIGOT-6419 it is not expected that this feature alters classloading behaviour, although some changes may be unavoidable. By: md_5 --- paper-api/pom.xml | 19 +++ .../bukkit/plugin/PluginDescriptionFile.java | 41 +++++ .../bukkit/plugin/java/JavaPluginLoader.java | 71 ++++----- .../org/bukkit/plugin/java/LibraryLoader.java | 128 ++++++++++++++++ .../bukkit/plugin/java/PluginClassLoader.java | 145 ++++++++++-------- 5 files changed, 303 insertions(+), 101 deletions(-) create mode 100644 paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java diff --git a/paper-api/pom.xml b/paper-api/pom.xml index 34223ab2c5..32174818dd 100644 --- a/paper-api/pom.xml +++ b/paper-api/pom.xml @@ -57,6 +57,25 @@ 1.27 compile + + + org.apache.maven + maven-resolver-provider + 3.8.1 + provided + + + org.apache.maven.resolver + maven-resolver-connector-basic + 1.6.2 + provided + + + org.apache.maven.resolver + maven-resolver-transport-http + 1.6.2 + provided + org.jetbrains diff --git a/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java b/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java index f78a7df6e2..801a323d9c 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java +++ b/paper-api/src/main/java/org/bukkit/plugin/PluginDescriptionFile.java @@ -133,6 +133,10 @@ import org.yaml.snakeyaml.nodes.Tag; * api-version * {@link #getAPIVersion()} * The API version which this plugin was programmed against + * + * libraries + * {@link #getLibraries() ()} + * The libraries to be linked with this plugin * * *

@@ -152,6 +156,8 @@ import org.yaml.snakeyaml.nodes.Tag; *main: com.captaininflamo.bukkit.inferno.Inferno *depend: [NewFire, FlameWire] *api-version: 1.13 + *libraries: + - com.squareup.okhttp3:okhttp:4.9.0 * *commands: * flagrate: @@ -247,6 +253,7 @@ public final class PluginDescriptionFile { private PermissionDefault defaultPerm = PermissionDefault.OP; private Set awareness = ImmutableSet.of(); private String apiVersion = null; + private List libraries = ImmutableList.of(); public PluginDescriptionFile(@NotNull final InputStream stream) throws InvalidDescriptionException { loadMap(asMap(YAML.get().load(stream))); @@ -957,6 +964,22 @@ public final class PluginDescriptionFile { return apiVersion; } + /** + * Gets the libraries this plugin requires. This is a preview feature. + *

    + *
  • Libraries must be GAV specifiers and are loaded from Maven Central. + *
+ *

+ * Example:

libraries:
+     *     - com.squareup.okhttp3:okhttp:4.9.0
+ * + * @return required libraries + */ + @NotNull + public List getLibraries() { + return libraries; + } + /** * @return unused * @deprecated unused @@ -1127,6 +1150,20 @@ public final class PluginDescriptionFile { apiVersion = map.get("api-version").toString(); } + if (map.get("libraries") != null) { + ImmutableList.Builder contributorsBuilder = ImmutableList.builder(); + try { + for (Object o : (Iterable) map.get("libraries")) { + contributorsBuilder.add(o.toString()); + } + } catch (ClassCastException ex) { + throw new InvalidDescriptionException(ex, "libraries are of wrong type"); + } + libraries = contributorsBuilder.build(); + } else { + libraries = ImmutableList.of(); + } + try { lazyPermissions = (Map) map.get("permissions"); } catch (ClassCastException ex) { @@ -1201,6 +1238,10 @@ public final class PluginDescriptionFile { map.put("api-version", apiVersion); } + if (libraries != null) { + map.put("libraries", libraries); + } + if (classLoaderOf != null) { map.put("class-loader-of", classLoaderOf); } diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java index 765e880596..390abd5115 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/paper-api/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -7,12 +7,12 @@ import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -38,6 +38,7 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginLoader; import org.bukkit.plugin.RegisteredListener; +import org.bukkit.plugin.SimplePluginManager; import org.bukkit.plugin.TimedRegisteredListener; import org.bukkit.plugin.UnknownDependencyException; import org.jetbrains.annotations.NotNull; @@ -50,8 +51,8 @@ import org.yaml.snakeyaml.error.YAMLException; public final class JavaPluginLoader implements PluginLoader { final Server server; private final Pattern[] fileFilters = new Pattern[]{Pattern.compile("\\.jar$")}; - private final Map> classes = new ConcurrentHashMap>(); private final List loaders = new CopyOnWriteArrayList(); + private final LibraryLoader libraryLoader; /** * This class was not meant to be constructed explicitly @@ -62,6 +63,15 @@ public final class JavaPluginLoader implements PluginLoader { public JavaPluginLoader(@NotNull Server instance) { Validate.notNull(instance, "Server cannot be null"); server = instance; + + LibraryLoader libraryLoader = null; + try { + libraryLoader = new LibraryLoader(server.getLogger()); + } catch (NoClassDefFoundError ex) { + // Provided depends were not added back + server.getLogger().warning("Could not initialize LibraryLoader (missing dependencies?)"); + } + this.libraryLoader = libraryLoader; } @Override @@ -130,7 +140,7 @@ public final class JavaPluginLoader implements PluginLoader { final PluginClassLoader loader; try { - loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file); + loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file, (libraryLoader != null) ? libraryLoader.createLoader(description) : null); } catch (InvalidPluginException ex) { throw ex; } catch (Throwable ex) { @@ -189,46 +199,27 @@ public final class JavaPluginLoader implements PluginLoader { } @Nullable - Class getClassByName(final String name) { - Class cachedClass = classes.get(name); - - if (cachedClass != null) { - return cachedClass; - } else { - for (PluginClassLoader loader : loaders) { - try { - cachedClass = loader.findClass(name, false); - } catch (ClassNotFoundException cnfe) {} - if (cachedClass != null) { - return cachedClass; - } + Class getClassByName(final String name, boolean resolve, PluginDescriptionFile description) { + for (PluginClassLoader loader : loaders) { + try { + return loader.loadClass0(name, resolve, false, ((SimplePluginManager) server.getPluginManager()).isTransitiveDepend(description, loader.plugin.getDescription())); + } catch (ClassNotFoundException cnfe) { } } return null; } void setClass(@NotNull final String name, @NotNull final Class clazz) { - if (!classes.containsKey(name)) { - classes.put(name, clazz); - - if (ConfigurationSerializable.class.isAssignableFrom(clazz)) { - Class serializable = clazz.asSubclass(ConfigurationSerializable.class); - ConfigurationSerialization.registerClass(serializable); - } + if (ConfigurationSerializable.class.isAssignableFrom(clazz)) { + Class serializable = clazz.asSubclass(ConfigurationSerializable.class); + ConfigurationSerialization.registerClass(serializable); } } - private void removeClass(@NotNull String name) { - Class clazz = classes.remove(name); - - try { - if ((clazz != null) && (ConfigurationSerializable.class.isAssignableFrom(clazz))) { - Class serializable = clazz.asSubclass(ConfigurationSerializable.class); - ConfigurationSerialization.unregisterClass(serializable); - } - } catch (NullPointerException ex) { - // Boggle! - // (Native methods throwing NPEs is not fun when you can't stop it before-hand) + private void removeClass(@NotNull Class clazz) { + if (ConfigurationSerializable.class.isAssignableFrom(clazz)) { + Class serializable = clazz.asSubclass(ConfigurationSerializable.class); + ConfigurationSerialization.unregisterClass(serializable); } } @@ -374,10 +365,16 @@ public final class JavaPluginLoader implements PluginLoader { PluginClassLoader loader = (PluginClassLoader) cloader; loaders.remove(loader); - Set names = loader.getClasses(); + Collection> classes = loader.getClasses(); - for (String name : names) { - removeClass(name); + for (Class clazz : classes) { + removeClass(clazz); + } + + try { + loader.close(); + } catch (IOException ex) { + // } } } diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java b/paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java new file mode 100644 index 0000000000..939afec1f2 --- /dev/null +++ b/paper-api/src/main/java/org/bukkit/plugin/java/LibraryLoader.java @@ -0,0 +1,128 @@ +// CHECKSTYLE:OFF +package org.bukkit.plugin.java; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.bukkit.plugin.PluginDescriptionFile; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.DependencyRequest; +import org.eclipse.aether.resolution.DependencyResolutionException; +import org.eclipse.aether.resolution.DependencyResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transfer.AbstractTransferListener; +import org.eclipse.aether.transfer.TransferCancelledException; +import org.eclipse.aether.transfer.TransferEvent; +import org.eclipse.aether.transport.http.HttpTransporterFactory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +class LibraryLoader +{ + + private final Logger logger; + private final RepositorySystem repository; + private final DefaultRepositorySystemSession session; + private final List repositories; + + public LibraryLoader(@NotNull Logger logger) + { + this.logger = logger; + + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService( RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class ); + locator.addService( TransporterFactory.class, HttpTransporterFactory.class ); + + this.repository = locator.getService( RepositorySystem.class ); + this.session = MavenRepositorySystemUtils.newSession(); + + session.setChecksumPolicy( RepositoryPolicy.CHECKSUM_POLICY_FAIL ); + session.setLocalRepositoryManager( repository.newLocalRepositoryManager( session, new LocalRepository( "libraries" ) ) ); + session.setTransferListener( new AbstractTransferListener() + { + @Override + public void transferStarted(@NotNull TransferEvent event) throws TransferCancelledException + { + logger.log( Level.INFO, "Downloading {0}", event.getResource().getRepositoryUrl() + event.getResource().getResourceName() ); + } + } ); + session.setReadOnly(); + + this.repositories = repository.newResolutionRepositories( session, Arrays.asList( new RemoteRepository.Builder( "central", "default", "https://repo.maven.apache.org/maven2" ).build() ) ); + } + + @Nullable + public ClassLoader createLoader(@NotNull PluginDescriptionFile desc) + { + if ( desc.getLibraries().isEmpty() ) + { + return null; + } + logger.log( Level.INFO, "[{0}] Loading {1} libraries... please wait", new Object[] + { + desc.getName(), desc.getLibraries().size() + } ); + + List dependencies = new ArrayList<>(); + for ( String library : desc.getLibraries() ) + { + Artifact artifact = new DefaultArtifact( library ); + Dependency dependency = new Dependency( artifact, null ); + + dependencies.add( dependency ); + } + + DependencyResult result; + try + { + result = repository.resolveDependencies( session, new DependencyRequest( new CollectRequest( (Dependency) null, dependencies, repositories ), null ) ); + } catch ( DependencyResolutionException ex ) + { + throw new RuntimeException( "Error resolving libraries", ex ); + } + + List jarFiles = new ArrayList<>(); + for ( ArtifactResult artifact : result.getArtifactResults() ) + { + File file = artifact.getArtifact().getFile(); + + URL url; + try + { + url = file.toURI().toURL(); + } catch ( MalformedURLException ex ) + { + throw new AssertionError( ex ); + } + + jarFiles.add( url ); + logger.log( Level.INFO, "[{0}] Loaded library {1}", new Object[] + { + desc.getName(), file + } ); + } + + URLClassLoader loader = new URLClassLoader( jarFiles.toArray( new URL[ jarFiles.size() ] ) ); + + return loader; + } +} diff --git a/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java index 5830e8b9b7..6843e32438 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +++ b/paper-api/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java @@ -9,6 +9,7 @@ import java.net.URL; import java.net.URLClassLoader; import java.security.CodeSigner; import java.security.CodeSource; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.Map; @@ -37,6 +38,7 @@ final class PluginClassLoader extends URLClassLoader { private final JarFile jar; private final Manifest manifest; private final URL url; + private final ClassLoader libraryLoader; final JavaPlugin plugin; private JavaPlugin pluginInit; private IllegalStateException pluginState; @@ -46,7 +48,7 @@ final class PluginClassLoader extends URLClassLoader { ClassLoader.registerAsParallelCapable(); } - PluginClassLoader(@NotNull final JavaPluginLoader loader, @Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file) throws IOException, InvalidPluginException, MalformedURLException { + PluginClassLoader(@NotNull final JavaPluginLoader loader, @Nullable final ClassLoader parent, @NotNull final PluginDescriptionFile description, @NotNull final File dataFolder, @NotNull final File file, @Nullable ClassLoader libraryLoader) throws IOException, InvalidPluginException, MalformedURLException { super(new URL[] {file.toURI().toURL()}, parent); Validate.notNull(loader, "Loader cannot be null"); @@ -57,6 +59,7 @@ final class PluginClassLoader extends URLClassLoader { this.jar = new JarFile(file); this.manifest = jar.getManifest(); this.url = file.toURI().toURL(); + this.libraryLoader = libraryLoader; try { Class jarClass; @@ -92,87 +95,101 @@ final class PluginClassLoader extends URLClassLoader { } @Override - protected Class findClass(String name) throws ClassNotFoundException { - return findClass(name, true); + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + return loadClass0(name, resolve, true, true); } - Class findClass(@NotNull String name, boolean checkGlobal) throws ClassNotFoundException { + Class loadClass0(@NotNull String name, boolean resolve, boolean checkGlobal, boolean checkLibraries) throws ClassNotFoundException { + try { + return super.loadClass(name, resolve); + } catch (ClassNotFoundException ex) { + } + + if (checkLibraries && libraryLoader != null) { + try { + return libraryLoader.loadClass(name); + } catch (ClassNotFoundException ex) { + } + } + + if (checkGlobal) { + Class result = loader.getClassByName(name, resolve, description); + + if (result != null) { + PluginDescriptionFile provider = ((PluginClassLoader) result.getClassLoader()).description; + + if (provider != description + && !seenIllegalAccess.contains(provider.getName()) + && !((SimplePluginManager) loader.server.getPluginManager()).isTransitiveDepend(description, provider)) { + + seenIllegalAccess.add(provider.getName()); + if (plugin != null) { + plugin.getLogger().log(Level.WARNING, "Loaded class {0} from {1} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{name, provider.getFullName()}); + } else { + // In case the bad access occurs on construction + loader.server.getLogger().log(Level.WARNING, "[{0}] Loaded class {1} from {2} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{description.getName(), name, provider.getFullName()}); + } + } + + return result; + } + } + + throw new ClassNotFoundException(name); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { if (name.startsWith("org.bukkit.") || name.startsWith("net.minecraft.")) { throw new ClassNotFoundException(name); } Class result = classes.get(name); if (result == null) { - if (checkGlobal) { - result = loader.getClassByName(name); + String path = name.replace('.', '/').concat(".class"); + JarEntry entry = jar.getJarEntry(path); - if (result != null) { - PluginDescriptionFile provider = ((PluginClassLoader) result.getClassLoader()).description; + if (entry != null) { + byte[] classBytes; - if (provider != description - && !seenIllegalAccess.contains(provider.getName()) - && !((SimplePluginManager) loader.server.getPluginManager()).isTransitiveDepend(description, provider)) { - - seenIllegalAccess.add(provider.getName()); - if (plugin != null) { - plugin.getLogger().log(Level.WARNING, "Loaded class {0} from {1} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{name, provider.getFullName()}); - } else { - // In case the bad access occurs on construction - loader.server.getLogger().log(Level.WARNING, "[{0}] Loaded class {1} from {2} which is not a depend, softdepend or loadbefore of this plugin.", new Object[]{description.getName(), name, provider.getFullName()}); - } - } + try (InputStream is = jar.getInputStream(entry)) { + classBytes = ByteStreams.toByteArray(is); + } catch (IOException ex) { + throw new ClassNotFoundException(name, ex); } - } - if (result == null) { - String path = name.replace('.', '/').concat(".class"); - JarEntry entry = jar.getJarEntry(path); + classBytes = loader.server.getUnsafe().processClass(description, path, classBytes); - if (entry != null) { - byte[] classBytes; - - try (InputStream is = jar.getInputStream(entry)) { - classBytes = ByteStreams.toByteArray(is); - } catch (IOException ex) { - throw new ClassNotFoundException(name, ex); - } - - classBytes = loader.server.getUnsafe().processClass(description, path, classBytes); - - int dot = name.lastIndexOf('.'); - if (dot != -1) { - String pkgName = name.substring(0, dot); - if (getPackage(pkgName) == null) { - try { - if (manifest != null) { - definePackage(pkgName, manifest, url); - } else { - definePackage(pkgName, null, null, null, null, null, null, null); - } - } catch (IllegalArgumentException ex) { - if (getPackage(pkgName) == null) { - throw new IllegalStateException("Cannot find package " + pkgName); - } + int dot = name.lastIndexOf('.'); + if (dot != -1) { + String pkgName = name.substring(0, dot); + if (getPackage(pkgName) == null) { + try { + if (manifest != null) { + definePackage(pkgName, manifest, url); + } else { + definePackage(pkgName, null, null, null, null, null, null, null); + } + } catch (IllegalArgumentException ex) { + if (getPackage(pkgName) == null) { + throw new IllegalStateException("Cannot find package " + pkgName); } } } - - CodeSigner[] signers = entry.getCodeSigners(); - CodeSource source = new CodeSource(url, signers); - - result = defineClass(name, classBytes, 0, classBytes.length, source); } - if (result == null) { - result = super.findClass(name); - } + CodeSigner[] signers = entry.getCodeSigners(); + CodeSource source = new CodeSource(url, signers); - if (result != null) { - loader.setClass(name, result); - } - - classes.put(name, result); + result = defineClass(name, classBytes, 0, classBytes.length, source); } + + if (result == null) { + result = super.findClass(name); + } + + loader.setClass(name, result); + classes.put(name, result); } return result; @@ -188,8 +205,8 @@ final class PluginClassLoader extends URLClassLoader { } @NotNull - Set getClasses() { - return classes.keySet(); + Collection> getClasses() { + return classes.values(); } synchronized void initialize(@NotNull JavaPlugin javaPlugin) {