首页 > 解决方案 > Java ServiceLoader 在 jar 中找不到下载的模块

问题描述

我正在构建一个客户端/服务器应用程序。客户端运行一个小型加载程序,它以模块 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();
}

第一次运行,没有client.jar。其他代码下载client.jar,然后运行上面的代码。查看此方法的输出,URLClassLoader 能够加载服务提供者类(恰好称为 PokerTable)。但是,ServiceLoader 什么也没找到,并且该方法打印“无法加载客户端服务提供者”。

但是,第二次运行时,client.jar 已经存在,并且没有下载新的。在这种情况下,ServiceLoader 返回正确的类并且一切正常。

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

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

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

请记住,client.jar 是一个模块 jar。上面的代码位于具有此 module-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 有这个 module-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;
}

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

标签: javajarserviceloader

解决方案


对我的问题的评论使我研究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 的解释:

为了保持向后兼容性,JPMS 有未命名模块的概念(每个 ClassLoader 有一个)。这是放置类路径上的代码的地方。尽管它有一个模块信息文件,但它也是 URLClassLoader 加载时您的 client.jar 结束的地方。未命名模块中的类的功能与前模块世界中的一样;为了让 ServiceLoader 找到提供程序,您需要 META-INF/services 下的提供程序配置文件。使用和提供指令仅在命名模块中生效,这是您在创建 ModuleLayer 时得到的。


推荐阅读