So, I've been trying to make a small PluginLoader
in my library which allows you to load JAR
files into a custom ModuleLayer
and use the layer to load services using ServiceLoader.load(ModuleLayer, Class<?>)
.
However, when calling ServiceProvider.load
, it internally uses Reflection.getCallerClass
to get the, duhh, class calling the code, so it can load the services from it's module.
PluginLoader.java
package com.wexalian.common.plugin;
import com.wexalian.common.collection.wrapper.StreamWrapper;
import com.wexalian.nullability.annotations.Nonnull;
import java.nio.file.Path;
import java.util.ServiceLoader;
import java.util.stream.Stream;
@FunctionalInterface
public interface PluginLoader<T extends IAbstractPlugin> extends StreamWrapper.Iterable<T> {
@Nonnull
@Override
Stream<T> get();
static void init(@Nonnull ServiceLoaderLayerFunction serviceLoaderFunc) {
PluginLoaderImpl.init(serviceLoaderFunc);
}
static void loadPlugins(@Nonnull Path path) {
PluginLoaderImpl.loadPlugins(path);
}
@Nonnull
static <T extends IAbstractPlugin> PluginLoader<T> load(@Nonnull Class<T> pluginClass) {
return load(pluginClass, null);
}
@Nonnull
static <T extends IAbstractPlugin> PluginLoader<T> load(@Nonnull Class<T> pluginClass, ServiceLoaderFallbackFunction fallbackServiceProvider) {
return PluginLoaderImpl.load(pluginClass, fallbackServiceProvider);
}
@FunctionalInterface
interface ServiceLoaderLayerFunction {
@Nonnull
<T> ServiceLoader<T> load(@Nonnull ModuleLayer layer, @Nonnull Class<T> clazz);
@Nonnull
default <T> Stream<T> stream(@Nonnull ModuleLayer layer, @Nonnull Class<T> clazz) {
return load(layer, clazz).stream().map(ServiceLoader.Provider::get);
}
}
@FunctionalInterface
interface ServiceLoaderFallbackFunction {
@Nonnull
<T> ServiceLoader<T> load(@Nonnull Class<T> clazz);
@Nonnull
default <T> Stream<T> stream(@Nonnull Class<T> clazz) {
return load(clazz).stream().map(ServiceLoader.Provider::get);
}
}
}
PluginLoaderImpl.java
package com.wexalian.common.plugin;
import com.wexalian.nullability.annotations.Nonnull;
import java.io.IOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
final class PluginLoaderImpl {
private static final Set<ModuleLayer> pluginLayerSet = new HashSet<>();
private static PluginLoader.ServiceLoaderLayerFunction serviceLoaderLayer;
private static ModuleLayer coreLayer;
private static ClassLoader coreLoader;
private static boolean init = false;
private PluginLoaderImpl() {}
static void init(@Nonnull PluginLoader.ServiceLoaderLayerFunction serviceLoaderFunc) {
if (!init) {
serviceLoaderLayer = serviceLoaderFunc;
Class<?> coreClass = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass();
coreLayer = coreClass.getModule().getLayer();
coreLoader = coreClass.getClassLoader();
if (coreLayer == null) {
throw new IllegalStateException("PluginLoaderImpl can only be initialized from a named module!");
}
else init = true;
}
else throw new IllegalStateException("PluginLoaderImpl can only be initialized once!");
}
static void loadPlugins(@Nonnull Path path) {
if (init) {
if (Files.exists(path)) {
try (Stream<Path> paths = Files.list(path)) {
ModuleFinder moduleFinder = ModuleFinder.of(paths.toArray(Path[]::new));
List<String> moduleNames = moduleFinder.findAll().stream().map(ref -> ref.descriptor().name()).toList();
Configuration configuration = coreLayer.configuration().resolveAndBind(moduleFinder, ModuleFinder.of(), moduleNames);
ModuleLayer pluginLayer = coreLayer.defineModulesWithOneLoader(configuration, coreLoader);
pluginLayerSet.add(pluginLayer);
}
catch (IOException e) {
throw new IllegalStateException("Error loading plugins from path " + path, e);
}
}
}
else throw new IllegalStateException("PluginLoaderImpl has to be initialized before you can load plugins!");
}
static <T extends IAbstractPlugin> PluginLoader<T> load(Class<T> clazz, PluginLoader.ServiceLoaderFallbackFunction serviceLoader) {
if (init) {
if (!pluginLayerSet.isEmpty()) {
return () -> pluginLayerSet.stream().flatMap(layer -> serviceLoaderLayer.stream(layer, clazz)).filter(IAbstractPlugin::isEnabled);
}
else {
return () -> serviceLoaderLayer.stream(coreLayer, clazz).filter(IAbstractPlugin::isEnabled);
}
}
else if (serviceLoader != null) {
return () -> serviceLoader.stream(clazz);
}
else throw new IllegalStateException("PluginLoaderImpl has to be initialized before you can load services from plugins!");
}
}
Now my problem is:
I am currently writing a program with some services, and using that library to load JAR files and load them. However, it recognizes the PluginLoader
as the caller class, which "does not declare uses", because the library doesn't actually have the service I want.
I have found a work around, which is accepting a Function<ModuleLayer, Class<?>, ServiceProvider<?>
, which redirects all the calls to the proper module, but I'd rather not do that everywhere I use my PluginLoader
.
Other than this I wouldn't know any other solution, so maybe one of you knows.
Thanks in advance, Wexalian