Last active
December 29, 2025 04:08
-
-
Save ozanaaslan/d0e68985d2f0766f70da66d311a6b678 to your computer and use it in GitHub Desktop.
A lightweight plugin system for Java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import lombok.Getter; | |
| import lombok.SneakyThrows; | |
| import java.io.*; | |
| import java.lang.reflect.Method; | |
| import java.net.MalformedURLException; | |
| import java.net.URL; | |
| import java.net.URLClassLoader; | |
| import java.nio.file.Files; | |
| import java.util.*; | |
| import java.util.jar.JarFile; | |
| public class ModuleManager { | |
| @Getter private List<Manifest> manifests = new ArrayList<>(); | |
| private Set<String> loading = new HashSet<>(); | |
| @Getter private File workingDirectory; | |
| @Getter private Configuration configuration; | |
| public ModuleManager(File workingDirectory) { | |
| this.workingDirectory = workingDirectory; | |
| init(); | |
| } | |
| public ModuleManager(){ | |
| this(new File(System.getProperty("user.dir"), "modules")); | |
| } | |
| @SneakyThrows | |
| private void init(){ | |
| if(!workingDirectory.exists()) workingDirectory.mkdirs(); | |
| this.configuration = new Configuration(new File(workingDirectory, "modules.properties")); | |
| } | |
| public void loadModules() { | |
| File[] files = workingDirectory.listFiles(); | |
| if (files == null) return; | |
| Arrays.stream(files) | |
| .filter(file -> file.getName().endsWith(".jar")) | |
| .forEach(file -> this.manifests.add(new Manifest(file))); | |
| this.manifests.forEach(this::linkModuleLoader); | |
| } | |
| @SneakyThrows | |
| private void linkModuleLoader(Manifest manifest) { | |
| if (manifest.getClassLoader() != null) return; | |
| String name = manifest.getAttributes().getProperty("name"); | |
| if (loading.contains(name)) return; | |
| loading.add(name); | |
| ClassLoader parent = ModuleManager.class.getClassLoader(); | |
| String depName = manifest.getAttributes().getProperty("depends"); | |
| if (depName != null) { | |
| Manifest dep = getModuleWithName(depName); | |
| if (dep != null) { | |
| linkModuleLoader(dep); | |
| if (dep.getClassLoader() != null) | |
| parent = dep.getClassLoader(); | |
| } | |
| } | |
| manifest.initLoader(parent); | |
| loading.remove(name); | |
| } | |
| public void invokePrimaries(){ manifests.forEach(m -> m.getModule().invokePrimary()); } | |
| public void invokeSecondaries(){ manifests.forEach(m -> m.getModule().invokeSecondary()); } | |
| public void invokeTertiaries(){ manifests.forEach(m -> m.getModule().invokeTertiary()); } | |
| public Manifest getModuleWithName(String name) { | |
| return manifests.stream() | |
| .filter(m -> m.getAttributes().getProperty("name").equalsIgnoreCase(name)) | |
| .findFirst() | |
| .orElse(null); | |
| } | |
| private class Configuration { | |
| @Getter private File propertiesFile; | |
| @Getter private Properties properties; | |
| @SneakyThrows | |
| public Configuration(File file){ | |
| this.propertiesFile = file; | |
| this.propertiesFile.getParentFile().mkdirs(); | |
| this.propertiesFile.createNewFile(); | |
| this.properties = new Properties(); | |
| this.properties.load(Files.newInputStream(propertiesFile.toPath())); | |
| } | |
| @SneakyThrows | |
| public String set(String path, String value){ | |
| properties.setProperty(path, value); | |
| properties.store(new FileWriter(propertiesFile), null); | |
| return value; | |
| } | |
| } | |
| public class Manifest implements Serializable { | |
| @Getter private Properties attributes; | |
| @Getter transient private File file; | |
| @Getter transient private URLClassLoader classLoader; | |
| private Module module; | |
| @SneakyThrows | |
| public Manifest(File file) { | |
| this.file = file; | |
| this.attributes = new Properties(); | |
| try (JarFile jar = new JarFile(file)) { | |
| attributes.load(jar.getInputStream(jar.getEntry("manifest.properties"))); | |
| } | |
| } | |
| public Module getModule() { | |
| if (module == null) module = new Module(this); | |
| return module; | |
| } | |
| public URLClassLoader initLoader(ClassLoader parent) throws MalformedURLException { | |
| if (this.classLoader == null) | |
| this.classLoader = new URLClassLoader(new URL[]{file.toURI().toURL()}, parent); | |
| return this.classLoader; | |
| } | |
| } | |
| public class Module { | |
| @Getter private Manifest manifest; | |
| @Getter private Object instance; | |
| @Getter private boolean excluded; | |
| @Getter private String exclusionKey; | |
| private Set<String> executedEntrypoints = new HashSet<>(); | |
| @SneakyThrows | |
| public Module(Manifest manifest){ | |
| this.manifest = manifest; | |
| this.exclusionKey = String.join(".", "module", | |
| manifest.getAttributes().getProperty("name"), | |
| manifest.getAttributes().getProperty("version"), | |
| manifest.getAttributes().getProperty("author"), | |
| "exclude" | |
| ); | |
| this.excluded = Boolean.parseBoolean( | |
| String.valueOf(configuration.getProperties().getProperty(this.exclusionKey, "false")) | |
| ); | |
| if (this.instance == null) { | |
| String mainClass = manifest.getAttributes().getProperty("main"); | |
| this.instance = Class.forName(mainClass, true, manifest.getClassLoader()) | |
| .getConstructor().newInstance(); | |
| } | |
| } | |
| @SneakyThrows | |
| public void load(String entrypoint){ | |
| if(excluded || executedEntrypoints.contains(entrypoint)) return; | |
| String depName = manifest.getAttributes().getProperty("depends"); | |
| if (depName != null) { | |
| Manifest dep = getModuleWithName(depName); | |
| if (dep != null) dep.getModule().load(entrypoint); | |
| } | |
| ModuleManager.invoke(entrypoint, instance); | |
| executedEntrypoints.add(entrypoint); | |
| } | |
| public void invokePrimary() { load("primaryEntrypoint"); } | |
| public void invokeSecondary() { load("secondaryEntrypoint"); } | |
| public void invokeTertiary() { load("tertiaryEntrypoint"); } | |
| } | |
| @SneakyThrows | |
| private static void invoke(String methodName, Object instance) { | |
| try { | |
| Method m = instance.getClass().getDeclaredMethod(methodName); | |
| m.setAccessible(true); | |
| m.invoke(instance); | |
| } catch (NoSuchMethodException e) { | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment