From 7b8c768076def5dd2520c508ef4a8b13f1baba8b Mon Sep 17 00:00:00 2001 From: Sky Date: Sat, 18 Jan 2025 21:26:49 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20class=20loader=20for?= =?UTF-8?q?=20module=20dependency=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `DiSkyModuleClassLoader` to handle module loading with relocation and dependency resolution. Updated build script to include bStats library and package relocation logic for specific dependencies. This prepares the architecture for better plugin modularization. --- build.gradle | 11 ++ .../api/modules/DiSkyModuleClassLoader.java | 130 ++++++++++++++++++ .../disky/api/modules/ModuleManager.java | 13 +- 3 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 src/main/java/info/itsthesky/disky/api/modules/DiSkyModuleClassLoader.java diff --git a/build.gradle b/build.gradle index 2242618a..d5a92df5 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,9 @@ dependencies { // Stable: net.dv8tion:JDA implementation 'net.dv8tion:JDA:5.2.2' + // bStats + implementation 'org.bstats:bstats-bukkit:3.0.2' + // Class manipulation implementation 'net.bytebuddy:byte-buddy:1.11.22' } @@ -50,6 +53,14 @@ shadowJar { minimize() setArchiveFileName('DiSky ' + version + '.jar') + + // We currently cannot relocate JDA, as its default package is + // used over modules and more. However, as DiSky should be the only + // plugin using JDA, this should not be a problem. + + // relocate('net.dv8tion.jda', 'net.itsthesky.disky.jda') + + relocate('org.bstats', 'net.itsthesky.disky.bstats') } task removeClassPath { diff --git a/src/main/java/info/itsthesky/disky/api/modules/DiSkyModuleClassLoader.java b/src/main/java/info/itsthesky/disky/api/modules/DiSkyModuleClassLoader.java new file mode 100644 index 00000000..797b1703 --- /dev/null +++ b/src/main/java/info/itsthesky/disky/api/modules/DiSkyModuleClassLoader.java @@ -0,0 +1,130 @@ +package info.itsthesky.disky.api.modules; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public class DiSkyModuleClassLoader extends URLClassLoader { + private final String originalPackage; + private final String relocatedPackage; + private final Map> loadedClasses = new ConcurrentHashMap<>(); + private final Set processedJars = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public DiSkyModuleClassLoader(URL[] urls, ClassLoader parent, String originalPackage, String relocatedPackage) { + super(urls, parent); + this.originalPackage = originalPackage; + this.relocatedPackage = relocatedPackage; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // Vérifie si la classe a déjà été chargée + Class loadedClass = loadedClasses.get(name); + if (loadedClass != null) { + return loadedClass; + } + + // Si la classe demandée est dans le package original JDA + if (name.startsWith(originalPackage)) { + String relocatedName = name.replace(originalPackage, relocatedPackage); + try { + // Essaie de charger la classe relocalisée depuis le parent + Class relocatedClass = getParent().loadClass(relocatedName); + loadedClasses.put(name, relocatedClass); + return relocatedClass; + } catch (ClassNotFoundException e) { + // Continue avec le comportement normal si la classe relocalisée n'existe pas + } + } + + try { + // Essaie de charger normalement + Class c = super.loadClass(name, resolve); + loadedClasses.put(name, c); + return c; + } catch (ClassNotFoundException e) { + // Si la classe n'est pas trouvée, vérifie les dépendances + Class dependencyClass = loadFromDependencies(name); + if (dependencyClass != null) { + loadedClasses.put(name, dependencyClass); + return dependencyClass; + } + throw e; + } + } + + private Class loadFromDependencies(String name) throws ClassNotFoundException { + try { + // Parcourt les URLs du classloader pour trouver les JARs + for (URL url : getURLs()) { + if (url.getProtocol().equals("file") && url.getPath().endsWith(".jar")) { + String jarPath = url.getPath(); + if (processedJars.add(jarPath)) { // Évite de traiter plusieurs fois le même JAR + try (JarFile jarFile = new JarFile(new File(jarPath))) { + // Analyse les dépendances du JAR + analyzeJarDependencies(jarFile); + } + } + } + } + } catch (IOException e) { + throw new ClassNotFoundException("Error analyzing dependencies", e); + } + return null; + } + + private void analyzeJarDependencies(JarFile jarFile) throws IOException { + // Lit le fichier MANIFEST.MF pour trouver les dépendances + Manifest manifest = jarFile.getManifest(); + if (manifest != null) { + Attributes attributes = manifest.getMainAttributes(); + String classpath = attributes.getValue(Attributes.Name.CLASS_PATH); + if (classpath != null) { + // Ajoute les dépendances au classloader + for (String path : classpath.split(" ")) { + try { + File dependency = new File(new File(jarFile.getName()).getParent(), path); + if (dependency.exists()) { + addURL(dependency.toURI().toURL()); + } + } catch (MalformedURLException e) { + // Ignore les URLs malformées + } + } + } + } + } + + // Méthode utilitaire pour charger un module avec ses dépendances + public static DiSkyModuleClassLoader createWithDependencies(File moduleJar, ClassLoader parent, + String originalPackage, String relocatedPackage) throws IOException { + // Crée une liste des URLs incluant le module et ses dépendances + List urls = new ArrayList<>(); + urls.add(moduleJar.toURI().toURL()); + + // Ajoute le dossier lib/ s'il existe + File libDir = new File(moduleJar.getParentFile(), "lib"); + if (libDir.exists() && libDir.isDirectory()) { + File[] libs = libDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (libs != null) { + for (File lib : libs) { + urls.add(lib.toURI().toURL()); + } + } + } + + return new DiSkyModuleClassLoader( + urls.toArray(new URL[0]), + parent, + originalPackage, + relocatedPackage + ); + } +} \ No newline at end of file diff --git a/src/main/java/info/itsthesky/disky/api/modules/ModuleManager.java b/src/main/java/info/itsthesky/disky/api/modules/ModuleManager.java index f60eb5bb..acfe00fe 100644 --- a/src/main/java/info/itsthesky/disky/api/modules/ModuleManager.java +++ b/src/main/java/info/itsthesky/disky/api/modules/ModuleManager.java @@ -1,28 +1,21 @@ package info.itsthesky.disky.api.modules; -import ch.njol.skript.Skript; import ch.njol.skript.SkriptAddon; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.lang.ExpressionInfo; -import ch.njol.skript.lang.SyntaxElementInfo; -import ch.njol.skript.registrations.Classes; import info.itsthesky.disky.DiSky; -import info.itsthesky.disky.api.ReflectionUtils; -import info.itsthesky.disky.api.generator.DocBuilder; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; -import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.net.URL; import java.net.URLClassLoader; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Logger;