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。上面的代码在一个有这个模块的模块中。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在fo.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:的解释

为了保持向后兼容性,JPMS有未命名的概念模块(每个ClassLoader有一个(。这是放置类路径。当由URLClassLoader加载,尽管它有一个模块信息文件。未命名模块中的类的功能与预模块中的相同世界为了让ServiceLoader找到提供程序,您需要META-INF/services下的提供程序配置文件。用途和提供的指令仅在命名模块中生效创建ModuleLayer时获得。

最新更新