Java ServiceLoader 在 jar 中找不到下载的模块

Java ServiceLoader not finding downloaded module in jar

我正在构建一个 client/server 应用程序。客户端 运行 是一个小型加载程序,它以模块 jar 的形式下载客户端,但前提是 client.jar 已更改。然后加载程序尝试通过 ServiceLoader 运行 客户端。

这是 运行 客户端 jar 中的服务提供商的代码。

static PokerGameInstance getPokerGame() {
    URL[] urls = null;

    try {
        urls = new URL[] { Paths.get("client.jar").toUri().toURL() };
        System.out.println(urls[0]);
    }
    catch (Exception e) {
        System.out.println("Could not create URL[] to use to create " +
                "ClassLoader for client.jar.jar.");
        return null;
    }

    URLClassLoader classLoader;
    try {
        classLoader = new URLClassLoader(urls);
    }
    catch (Exception e) {
        System.out.println("Could not create classloader for " +
                "client.jar.");
        return null;
    }

    try { // Test code
        classLoader.loadClass("com.brandli.jbpoker.client.PokerGame");
    }
    catch (ClassNotFoundException e) {
        System.out.println("Could not find PokerGame class");
    }

    ServiceLoader<PokerGameInstance> loader = ServiceLoader
            .load(PokerGameInstance.class, classLoader);
    Optional<PokerGameInstance> optional = loader.findFirst();
    if (optional.isEmpty()) {
        System.out.println("Could not load client service provider.");
        return null;
    }

    return optional.get();
}

第一次运行s,没有client.jar。其他代码下载client.jar,然后上面的代码就是运行。查看此方法的输出,URLClassLoader 能够加载服务提供者 class(恰好称为 PokerTable)。然而,ServiceLoader 什么也没找到,方法打印 "Could not load client service provider."

然而,第二次运行s,client.jar已经存在,并没有下载新的。在这种情况下,ServiceLoader returns 正确 class 并且一切正常。

我是运行一个包含整个jar目录的模块路径。 Client.jar 也加载在那里。因此,在第二个 运行 中,系统 ClassLoader 正在获取 client.jar。换句话说,第二遍工作不是因为 ServiceLoader 从 URLClassLoader 获取 client.jar。我通过将 ClassLoader 参数 ServiceLoader.load() 设置为 null.运行 来验证这一点。

我还更改了模块路径以仅包含离散的 jar,这样系统 ClassLoader 就不会拾取 client.jar 如果它在那里。在那种情况下,上面的代码总是失败。

结果是 ServiceLoader 无法识别 client.jar 中的服务,即使 URLClassLoader 将加载该对象。这与正在下载 client.jar 无关,因为即使 client.jar 从一开始就存在问题(除非被系统 ClassLoader 拾取)。

记住 client.jar 是一个模块 jar。上面的代码在一个有这个模块的模块中-info.java:

module com.brandli.jbpoker.loader {
    exports com.brandli.jbpoker.loader;

    requires transitive javafx.controls;
    requires transitive com.brandli.jbpoker.core;
    uses com.brandli.jbpoker.loader.PokerGameInstance;
}

Client.jar 有这个模块-info.java:

    module com.brandli.jbpoker.client {

    requires transitive javafx.controls;
    requires transitive com.brandli.jbpoker.core;
    requires transitive com.brandli.jbpoker.loader;
    requires transitive com.brandli.jbpoker.common;

    provides com.brandli.jbpoker.loader.PokerGameInstance with
    com.brandli.jbpoker.client.PokerGame;
}

我怀疑这与模块有关。有人有什么想法吗?

对我的问题的评论促使我调查 ModuleLayer/ModuleFinder。我注意到有一个 ServiceLoader.load(ModuleLayer, Class)。以下代码有效:

static PokerGameInstance getPokerGame() {
    ModuleFinder finder = ModuleFinder.of(Paths.get("client.jar"),
            Paths.get("common.jar"));
    ModuleLayer parent = ModuleLayer.boot();
    Configuration cf = null;
    try {
        cf = parent.configuration()
                .resolveAndBind(finder, ModuleFinder.of(),
                 Set.of("com.brandli.jbpoker.client"));
    }
    catch (Throwable e) {
        return null;
    }

    ClassLoader cl = ClassLoader.getSystemClassLoader();

    ModuleLayer layer = null;
    try {
        layer = parent.defineModulesWithOneLoader(cf, cl);
    }
    catch (Throwable e) {
        return null;
    }
    ServiceLoader<PokerGameInstance> loader = ServiceLoader
            .load(layer, PokerGameInstance.class);

    Optional<PokerGameInstance> optional = loader.findFirst();
    if (optional.isEmpty()) {
        return null;
    }

    return optional.get();
}

我不知道为什么我问题中的代码不起作用。

编辑:来自@Slaw 的解释:

To keep backwards compatibility JPMS has the concept of the unnamed module (there's one per ClassLoader). This is where code on the class-path is placed. It's also where your client.jar ends up when loaded by your URLClassLoader, despite it having a module-info file. Classes in the unnamed module function as they did in the pre-module world; in order for a ServiceLoader to find a provider you need a provider-configuration file under META-INF/services. The uses and provides directives only take effect in named modules, which is what you get when creating a ModuleLayer.