0

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

Wexalian
  • 1
  • 1

1 Answers1

0

When using the ModuleLayer system, you also have to define the uses and provides in your and the various plugin module definitions.

Your module:

uses com.wexalian.common.plugin.IAbstractPlugin;

And in your plugin modules:

provides com.wexalian.common.plugin.IAbstractPlugin with some.plugin.PluginFactory;

See ServiceLoader and ServiceLoader.Provider, this is how the service loader in one module knows about loaders in other modules.

Bill Mair
  • 1,073
  • 6
  • 15
  • That's not my problem. I'm writing a program which uses a library which calls ServiceLoader. ServiceLoader is activaly checking the caller class, e.g. one in the library, to see if it uses the interface. Which it can't because it's from a different module, my program. – Wexalian Feb 19 '23 at 17:12
  • 1
    @Wexalian then it would be the duty of that library to call [`addUses`](https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/lang/Module.html#addUses(java.lang.Class)) before calling into `ServiceLoader`. – Holger May 30 '23 at 12:53