I am trying to make a plugin system for an app I'm working on, what I want is to have a directory that can be filled with plugin jar files that is then loaded into the project when it starts. I have made the plugin structure dependency and have test plugins, the part that I am having problems with is the plugin loading system. So far I have been using this blog post as reference but the class loader provided in it does not work.
The structure that I am aiming for with this plugin system is like this:
C:.
├───commands
│ ├───plugin
│ │ └plugin.jar
│ └───plugin2
│ └plugin2.jar
└───src
└───Java app
├───controller
│ └───loaders
├───model
└───view
Inside the loaders package are the following two classes that were taken from the blog:
PluginLoader.class
public class PluginLoader {
private final Map<String, CommandFactory> commandFactoryMap = new HashMap<>();
private final File pluginsDir;
private final AtomicBoolean loading = new AtomicBoolean();
public PluginLoader(final File pluginsDir) {
this.pluginsDir = pluginsDir;
}
public void loadCommands() {
if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
System.err.println("Skipping Plugin Loading. Plugin dir not found: " + pluginsDir);
return;
}
if (loading.compareAndSet(false, true)) {
final File[] files = requireNonNull(pluginsDir.listFiles());
for (File pluginDir : files) {
if (pluginDir.isDirectory()) {
loadCommand(pluginDir);
}
}
}
}
private void loadCommand(final File pluginDir) {
System.out.println("Loading plugin: " + pluginDir);
final URLClassLoader pluginClassLoader = createPluginClassLoader(pluginDir);
final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(pluginClassLoader);
// broken for loop
for (OmenChatPlugin omenChatPlugin : ServiceLoader.load(OmenChatPlugin.class, pluginClassLoader)) {
installPlugin(omenChatPlugin);
}
} finally {
Thread.currentThread().setContextClassLoader(currentClassLoader);
}
}
private void installPlugin(final OmenChatPlugin plugin) {
System.out.println("Installing plugin: " + plugin.getClass().getName());
for (CommandFactory c : plugin.getCommandFactories()) {
commandFactoryMap.put(c.name(), c);
}
}
private URLClassLoader createPluginClassLoader(File dir) {
final URL[] urls = Arrays.stream(Optional.of(dir.listFiles()).orElse(new File[]{}))
.sorted()
.map(File::toURI)
.map(this::toUrl)
.toArray(URL[]::new);
System.out.println("cpcl: " + Arrays.toString(urls));
return new PluginClassLoader(urls, getClass().getClassLoader());
}
private URL toUrl(final URI uri) {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
public CommandFactory getCommandFactory(String name) {
return commandFactoryMap.get(name);
}
}
PluginClassLoader.class
public class PluginClassLoader extends URLClassLoader {
public static final List<String> SHARED_PACKAGES = Arrays.asList(
"com.github.OMEN44",
"com.github.omen"
);
private final ClassLoader parentClassLoader;
public PluginClassLoader(URL[] urls, ClassLoader parentClassLoader) {
super(urls, null);
this.parentClassLoader = parentClassLoader;
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// has the class loaded already?
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
final boolean isSharedClass = SHARED_PACKAGES.stream().anyMatch(name::startsWith);
if (isSharedClass) {
loadedClass = parentClassLoader.loadClass(name);
} else {
loadedClass = super.loadClass(name, resolve);
}
}
if (resolve) { // marked to resolve
resolveClass(loadedClass);
}
return loadedClass;
}
}
When I try to load the plugins using these classes it prints the URL generated in the URLClassLoader method but when it gets to the for loop that I've commented the ServiceLoader returns an empty list. If anyone has a solution to this issue or knows a better method to use your help is appreciated.
Here is a link to the dependency and app github.
I found the issue, when you jar the plugin you need to add a file in the META-INF
folder, the project tructure should look like this:
.
├───main
│ ├───java
│ │ └───com
│ │ └───example
│ │ └───Plugin.java
│ └───resources
│ └───META-INF
│ └───services
│ └───com.library.PluginInterface
└───test
└───java
The com.library.PluginInterface file should contain the file path to the class in your plugin that is being loaded with this example it would contain: com.example.Plugin
. com.library
is the file path to the interface or abstract class in library shared by the app and the plugin.