java platform
Java平台模块系统(JPMS)对依赖项有很强的见解:默认情况下,需要它们(可以访问),然后在编译时和运行时都将它们存在。 但是,这不适用于可选的依赖项,因为代码是针对运行时不一定存在的工件编写的。 幸运的是,JPMS有一个require静态子句,可以在这些确切情况下使用。
我将向您展示几个示例,其中默认行为的严格性会导致问题,然后将模块系统的解决方案介绍给可选的依赖项:需要静态。 但是,对它们进行编码并非易事,因此我们也将对此进行仔细研究。
总览
不需要的依赖之谜
为了确定常规的require子句的严格性会导致问题的原因,我想从两个示例开始。 尽管在某些方面相似,但是稍后在我们讨论如何针对可能缺少的依赖项进行编码时,差异变得很重要。
实用程序库
让我们从我们正在维护的虚构库uber.lib开始,该库与少数其他库集成。 它的API提供了基于它们的功能,从而公开了它们的类型。 我们将通过com.google.guava的示例进行演示 ,在我们的假设场景中,该示例已经变成了uber.lib想要针对其进行编码的Java模块。
作为uber.lib的维护者,我们假设没有使用Guava的人永远不会调用我们库的Guava部分。 在某些情况下,这很有意义:如果没有这样的图,为什么还要在uber.lib中调用为com.google.common.graph.Graph实例创建漂亮报告的方法?
对于uber.lib ,这意味着它无需com.google.guava即可完美运行:如果Guava将其放入模块图中 ,则客户端可能会调用uber.lib API的该部分。 如果没有,他们也不会,图书馆也会很好。 我们可以说uber.lib从不需要它自己的依赖。
具有常规依赖性,无法实现可选关系。
但是,使用常规的require子句无法实现这种可选关系。 根据可读性和可访问性规则, uber.lib必须要求com.google.guava对其类型进行编译,但这会强制所有客户端在启动其应用程序时始终在模块路径上使用Guava。
如果与图书馆屈指可数uber.lib集成,它将使客户依赖于所有的人,即使他们可能永远不会使用超过一个。
这不是我们的好举动。
花式统计图书馆
第二个示例来自演示应用程序 ,该应用程序包含一个模块monitor.statistics 。 假设有一些高级统计信息库,其中包含monitor.statistics要使用的模块stats.fancy ,但是对于应用程序的每次部署,该信息都不会出现在模块路径中。 (这样做的原因无关紧要,但让我们一起使用一个许可证,该许可证可以防止将花哨的代码“用于邪恶”,但是,由于我们是邪恶的策划者,我们有时只是想这样做。)
我们想在monitor.statistics中编写代码,该代码使用fancy模块中的类型,但是要使其正常工作,我们需要使用require子句来依赖它。 但是,如果执行此操作,则在不存在stats.fancy的情况下,模块系统将不会启动应用程序。
僵局。 再次。
带有“需要静态”的可选依赖项
当一个模块需要针对另一个模块的类型进行编译,但又不想在运行时依赖它时,可以使用require静态子句。 如果foo需要静态bar,则模块系统在编译和运行时的行为会有所不同:
- 在编译时,必须存在bar ,否则会出现错误。 在编译过程中的酒吧是FOO可读。
- 在运行时,可能不存在bar ,这将不会导致错误或警告。 如果存在,则foo可以读取。
我们可以立即将其付诸实践,并创建一个可选的依赖项,从monitor.statistics到stats.fancy :
module monitor.statistics {
requires monitor.observer;
requires static stats.fancy;
exports monitor.statistics;
}
如果在编译过程中缺少stats.fancy,则在编译模块声明时会出现错误:
monitor.statistics/src/main/java/module-info.java:3:
error: module not found: stats.fancy
requires static stats.fancy;
^
1 error
但是,在启动时 ,模块系统不在乎stats.fancy是否存在。
同样, uber.lib的模块描述符将所有依赖项声明为可选:
module uber.lib {
requires static com.google.guava;
requires static org.apache.commons.lang;
requires static org.apache.commons.io;
requires static io.javaslang;
requires static com.aol.cyclops;
}
现在我们知道了如何声明可选的依赖项,还有两个问题需要回答:
- 在什么情况下会出现?
- 我们如何针对可选依赖项进行编码?
接下来,我们将回答两个问题。
解决可选依赖项
模块解析是这样的过程:给定初始模块和可观察模块的范围,该模块通过解析require子句构建模块图。 解析模块时,必须在可观察模块的范围中找到它需要的所有模块。 如果是,则将它们添加到模块图;否则,将它们添加到模块图。 否则会发生错误。 重要的是要注意,在解析期间未放入模块图中的模块在以后的编译或执行期间也不可用。
在编译时,模块解析会像常规依赖项一样处理可选的依赖项。 但是,在运行时,要求静态子句通常被忽略。 当模块系统遇到一个模块系统时,它不会尝试实现它,这意味着它甚至不检查命名模块是否存在于可观察模块的范围中。
仅是可选依赖项的模块在运行时将不可用。
结果,即使模块存在于模块路径上(或与此相关的JDK中),也不会仅仅由于可选的依赖关系而将其添加到模块图中。 仅当它也是正在解析的某个其他模块的常规依赖项,或者因为它是使用命令行标志–add-modules显式添加的,它才会进入图表。
也许您偶然发现了“ 大部分都忽略了可选依赖项”这一短语。 为什么大多数? 嗯,模块系统要做的一件事是,如果一个可选的依赖关系使其成为一个图形,则会添加一个可读性边缘。 这样可以确保如果存在可选模块,则可以立即访问其类型。
针对可选依赖项进行编码
可选的依赖项在针对它们编写代码时需要多加考虑,因为这是在monitor.statistics使用stats.fancy中的类型但运行时不存在该模块时发生的:
Exception in thread "main" java.lang.NoClassDefFoundError:
stats/fancy/FancyStats
at monitor.statistics/monitor.statistics.Statistician
.<init>(Statistician.java:15)
at monitor/monitor.Main.createMonitor(Main.java:42)
at monitor/monitor.Main.main(Main.java:22)
Caused by: java.lang.ClassNotFoundException: stats.fancy.FancyStats
... many more
哎呀。 我们通常不希望我们的代码这样做。
一般而言,当当前正在执行的代码引用类型时,Java虚拟机会检查它是否已加载。 如果不是,它将告诉类加载器执行此操作,如果失败,则结果为NoClassDefFoundError,该错误通常使应用程序崩溃或至少从正在执行的逻辑块中失败。
对于可选的依赖项,我们选择退出使模块系统安全的检查。
这是JAR hell著名的东西,模块系统希望通过在启动应用程序时检查声明的依赖项来克服 。 但是,由于需要static,因此我们选择退出该检查,这意味着我们最终可能会遇到NoClassDefFoundError。 我们该怎么做呢?
建立的依存关系
但是,在研究解决方案之前,我们需要查看我们是否确实有问题。 对于uber.lib,我们希望仅在调用库的代码已使用它们的情况下才使用来自可选依赖项的类型,这意味着类加载已成功。
换句话说,调用uber.lib时,必须存在所有必需的依赖项,否则将无法进行调用。 因此,我们毕竟没有问题,也不需要做任何事情。
内部依赖
不过,一般情况有所不同。 带有可选依赖项的模块很可能会首先尝试从中加载类,因此NoClassDefFoundError的风险非常高。
一种解决方案是确保在访问依赖项之前,必须对具有可选依赖项的模块进行所有可能的调用。 该检查点必须评估该依赖项是否存在,如果不存在,则将到达它的所有代码发送到不同的执行路径。
模块系统提供了一种检查模块是否存在的方法。 我在时事通讯中解释了如何到达那里以及为什么使用新的stack-walking API ,所以当我说这是可行的方式时,在这里您只需要信任我:
public class ModuleUtils {
public static boolean isModulePresent(String moduleName) {
return StackWalker
.getInstance(RETAIN_CLASS_REFERENCE)
.walk(frames -> frames
.map(StackFrame::getDeclaringClass)
.filter(declaringClass ->
declaringClass != ModuleUtils.class)
.findFirst()
.orElse((Class) ModuleUtils.class));
.getModule();
.getLayer()
.findModule(moduleName)
.isPresent();
// chain all the methods!
}
}
(在实际的应用程序中,缓存值可能并不总是重复相同的检查。)
用“ stats.fancy”之类的参数调用此方法将返回该模块是否存在。 如果使用常规依赖项的名称(简单的require子句)进行调用,则结果将始终为true,因为否则模块系统将无法启动应用程序。 如果使用可选依赖项的名称(需要static子句)进行调用,则结果将为true或false。
如果存在可选依赖项,则模块系统将建立可读性,因此沿着使用模块中类型的执行路径进行操作是安全的。 如果不存在,选择这样的路径将导致NoClassDefFoundError,因此必须找到其他路径。
摘要
有时您想针对运行时并不总是存在的依赖关系编写代码。 为了使依赖项的类型在编译时可用,但在启动时不强制其存在,模块系统提供了require静态子句。 但是请注意,如果仅以这种方式引用模块,则在解析过程中不会拾取该模块,并且需要特别注意确保在运行时不存在可选依赖项时代码不会崩溃。