带有 requires static
的可选依赖项
模块系统对依赖项有强烈的意见:默认情况下,它们需要被要求(才能被访问),然后它们需要在编译时和运行时都存在。然而,这在可选依赖项中行不通,因为代码是在不一定在运行时存在的工件上编写的。requires static
指令通过要求在编译时存在但在运行时容忍不存在来解决这个问题。
注意:您需要了解 模块系统基础知识 才能充分利用本文。
带有 requires static
的可选依赖项
当一个模块需要针对另一个模块中的类型进行编译,但不想在运行时依赖它时,它可以使用 requires static
指令。如果模块 A requires static
模块 B,则模块系统在编译时和运行时的行为不同。
- 在编译时,B 必须存在(否则会发生错误),并且 B 可以被 A 读取。(这是依赖项的常见行为。)
- 在运行时,B 可能不存在,这不会导致错误或警告。如果它存在,则 A 可以读取它。
存在如何处理并不简单,但在讨论这个问题之前,让我们看一个例子。在 JDK 中,没有依赖项是可选的,因此我们必须自己想出一个例子。
让我们想象一个应用程序,它能够很好地解决其业务案例,但在存在一个额外的专有库的情况下可以做得更好。在这个例子中,我们将应用程序的模块称为 com.example.app,将库称为 com.sample.solver。我们还假设集成代码是这样的:com.example.app 引用了 com.sample.solver 中的类型,这意味着 app 需要针对 solver 进行编译,进而意味着 app 必须要求 solver。
module com.example.app {
requires com.sample.solver;
}
但是,正如我们在讨论模块解析时所探讨的那样,这意味着如果 com.sample.solver 不存在,模块系统将在运行时抛出错误 - 显然依赖项不是可选的。让我们改用 requires static
。
module com.example.app {
requires static com.sample.solver;
}
为了编译 com.example.app,com.sample.solver 是必需的,并且必须存在,这意味着可以自由使用它的类型。然而,在运行时,它可能不存在,这导致了两个问题,我们将在下面回答。
- 在什么情况下可选依赖项会存在?
- 我们如何针对可选依赖项进行编码?
可选依赖项的解析
模块解析是一个过程,它从根模块开始,通过解析 requires
指令来构建模块图。当解析一个模块时,它所要求的所有模块都必须在运行时或模块路径上找到,如果找到了,它们就会被添加到模块图中;否则会发生错误。(请注意,在解析过程中没有进入模块图的模块在后面的编译或执行过程中也无法使用。)在编译时,模块解析处理可选依赖项的方式与处理常规依赖项的方式相同。然而,在运行时,它们大多被忽略。
当模块系统遇到 requires static
指令时,它不会尝试满足它,这意味着它甚至不会检查引用的模块是否可以找到。因此,即使一个模块存在于模块路径上(或者在 JDK 中),它也不会仅仅因为可选依赖项而被添加到模块图中。它只有在它也是正在解析的另一个模块的常规依赖项,或者因为它使用命令行标志 --add-modules
显式添加时,才会进入图中。在这种情况下,模块系统将从要求模式到可选依赖项添加一个可读性边。
换句话说,可选依赖项会被忽略,除非它以其他方式进入模块图,在这种情况下,生成的模块图与使用非可选依赖项时的模块图相同。
针对可选依赖项进行编码
在针对可选依赖项编写代码时,需要多加考虑。一般来说,当正在执行的代码引用一个类型时,Java 运行时会检查它是否已经加载。如果没有,它会告诉类加载器这样做,如果失败,结果将是 NoClassDefFoundError
,这通常会导致应用程序崩溃,或者至少导致正在执行的逻辑块失败。
这是 JAR 地狱臭名昭著的原因,也是模块系统希望通过在启动应用程序时检查声明的依赖项来克服的原因。但是,使用 requires static
,我们选择退出该检查,这意味着我们最终可能会遇到 NoClassDefFoundError
。
检查模块是否存在
为了避免这种情况,我们可以查询模块系统以查看模块是否存在。
public class ModuleUtils {
public static boolean isModulePresent(Object caller, String moduleName) {
return caller.getClass()
.getModule()
.getLayer()
.findModule(moduleName)
.isPresent();
}
}
调用者需要将自身传递给该方法,以便它可以确定要查询的正确层以查找所需的模块。
已建立的依赖项
然而,可能并不总是需要显式检查模块的存在。想象一个库 com.example.lib,它帮助使用各种现有 API,其中包括 java.sql 中的 JDBC API。然后,有理由假设不使用 JDBC 的代码不使用库的这一部分。换句话说,我们可以假设库的 JDBC 部分只从已经使用 JDBC 的代码中调用,这意味着 java.sql 必须是模块图的一部分。
一般来说,如果使用可选依赖项的代码只会被依赖于相同依赖项的代码调用,那么就可以假设它的存在,不需要检查。
上次更新: 2021 年 9 月 14 日