java - 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;
}
我怀疑这与模块有关。有人有什么想法吗?
解决方案
对我的问题的评论使我研究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 时得到的。
推荐阅读
- remix - 如何使混音连接到仲裁网络
- javascript - Bot Framework瀑布中的命名函数?
- vue.js - 如何在vuejs中格式化电话(即xxx-xxx-xxxx)
标签 - c# - SQLite 的相对路径不适用于 WIX 工具集
- python - 使用 cx_Freeze for windows 构建的 python tkinter exe 不会显示 GUI
- javascript - Vue Mixin 用于子组件
- python - Django - 十进制类型的对象不是 JSON 可序列化的,并且在视图中转换为模型数据
- amazon-s3 - 在 S3 中保存文件和在 Cloudfront 上使其无效之间的时间。我应该等多久?
- java - Spring Web 应用程序和使用线程池的异步执行
- javascript - 在 JavaScript 中获取数组中的数组对象