Skip to content

Commit

Permalink
✨ Add custom class loader for module dependency management
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ItsTheSky committed Jan 18, 2025
1 parent 9edb2d8 commit 7b8c768
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 10 deletions.
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Class<?>> loadedClasses = new ConcurrentHashMap<>();
private final Set<String> 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<URL> 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
);
}
}
13 changes: 3 additions & 10 deletions src/main/java/info/itsthesky/disky/api/modules/ModuleManager.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down

0 comments on commit 7b8c768

Please sign in to comment.