Java 9引入了一个重要的新功能--JPMS,即Java平台模块系统。 经过六个月的时间,我得出结论,JPMS目前为开源库开发者提供了 "负面的好处"。 请继续阅读以了解原因。
面向库开发人员的模块
Java 8可能是有史以来最成功的一个Java版本。它被广泛使用并广受喜爱。 因此,几乎所有的开源库都运行在Java 8上(因为库作者希望他们的代码被使用!)。 一些历史悠久的库也仍然运行在旧版本上。 Joda-Convert有一个Java 6基线,而Joda-Time有一个Java 5基线。 其他人有一个Java 8基线,如ThreeTen-Extra。
Java 9是在2017年9月发布的,但它并不是一个会被支持若干年的版本。 相反,它的寿命是六个月,现在已经过时了,因为Java 10已经出来了。 而在六个月后,Java 11会出来,使Java 10被淘汰,以此类推。
虽然大多数版本持续六个月,但有些版本更幸运。 Java 11将是一个 "长期支持"(LTS)版本,有几年的安全和错误支持(Java 8也是一个LTS版本)。 因此,即使Java 10出来了,Java 8仍然是开源库开发者现在的明智的Java版本,因为它是当前的LTS版本。
但是,当Java 11发布时,会发生什么呢? 因为Java 11发布后,Java 8将很快不被支持,你会认为明智的基线是11。 不幸的是,我相信许多公司将长期坚持使用Java 8。 一个积极的开源项目可能会迅速转向Java 11基线,但这样做将是一个冒险的采用策略。
模块之路
在讨论开源库开发者的JPMS选项之前,有必要介绍一下类路径和模块路径之间的区别。 我们都知道并喜爱的类路径仍然存在于Java 9+中,而且它的工作方式基本相同。
模块路径是新的。 当一个jar文件在模块路径上时,任何模块信息都被用来应用新的更严格的JPMS规则。例如,一个public 方法不再是可调用的,除非它已经从它所包含的模块中exported (并由调用者的模块required )。
基本想法很简单,你把老式的非模块化jar文件放在class-path上,而把模块化jar文件放在module-path上。 然而,没有任何东西可以强制执行,而且事实证明这是一个问题。 因此有四种可能性。
- 模块化jar放在模块路径上
- 模块化jar文件在class-path上
- 模块路径上的经典非模块化jar
- 经典的非模块化jar在类路径上
为了确保你的库能正常工作,你需要在类路径和模块路径上都进行测试。 例如,与类路径相比,服务加载在模块路径上是非常不同的。 而且一些资源查找方法的工作方式也完全不同。
更加复杂的是,JPMS没有明确的测试支持。 为了测试模块路径上的模块(这是一个严格锁定的软件包集),你必须使用--patch-module命令行标志。该标志有效地扩展了模块,将测试包添加到与被测类相同的模块中。(如果你只测试公共API,你可以不使用patch-module,但在Maven中,你需要一个全新的项目和pom.xml来实现这种方法,所以它可能很少。
在最新的Maven surefire插件(v2.21.0及以后版本)中,使用了patch-module标志,但如果你的模块有可选的依赖项,或者你有额外的测试依赖项,你可能需要手动添加它们,见这个问题和这个问题。
鉴于这一切,一个开源库的开发者应该怎么做呢?
选项1,什么都不做
在大多数情况下,但不是全部,在Java 8或更早的版本上编译的代码在Java 9+的class-path上可以正常运行。 所以,你可以什么都不做,忽略JPMS。
问题是其他项目会依赖你的库,如果你完全不采用JPMS,你就会阻止这些项目的模块化进程(一个项目只有在其所有的依赖关系都模块化后才能选择完全模块化)。
当然,如果你的代码不能在Java 9+上运行,因为你使用了sun.misc.Unsafe 或其他你不应该做的事情,那么你还有其他事情要解决。
别忘了,用户可以把你的jar文件放在class-path或module-path上。你对这两种情况都进行了测试吗?即。事实是,"什么都不做 "是不可能的--至少你要做额外的测试,即使只是为了记录你的项目在模块路径上不工作。
选项2,添加一个模块名称
Java 9+在MANIFEST.MF 文件中识别了一个新条目。Automatic-Module-Name 条目允许一个jar文件声明它在变成适当的模块化jar文件时将使用什么名字。下面是如何使用Maven来添加它。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>org.foo.bar</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
这是一个很好的简单方法。 它保留了模块名称,并允许依赖你的jar文件的其他项目根据自己的意愿进行完全模块化。
但是,因为它太简单了,所以很容易忘记测试方面的问题。 同样,你的jar文件可能放在class-path上,也可能放在module-path上,两者的表现可能大相径庭。 事实上,现在它有了一些模块信息,工具可能会对它进行不同处理。
当Maven看到Automatic-Module-Name ,它通常会把类放在模块路径上,而不是放在类路径上。这可能没有影响,也可能显示出一个错误,即你的代码在类路径上可以工作,但在模块路径上却不行。 现在,在Maven中,你必须使用surefire插件v2.要在模块路径上测试,请使用v2.21.0版。 当然,在这两个版本之间切换是一个手动过程,请看这个问题的改进请求。
在升级一些项目时,我添加了Automatic-Module-Name ,但没有在模块路径上测试。当我最终在模块路径上测试时,测试失败了,因为代码根本无法在模块路径上运行。不幸的是,我现在在Maven-Central上的一些版本有Automatic-Module-Name ,但却无法在模块路径上运行,真是幸福......
为了强调这一点,仅仅在MANIFEST.MF文件中添加一些东西就会对项目的运行和测试产生影响。 你需要在class-path和module-path上进行测试。
选项3,添加module-info.java
module org.foo.bar {
requires org.threeten.extra;
exports org.foo.bar;
exports org.foo.bar.util;
}
那么,这样做对开源项目有什么影响呢?
与选项2不同,你的代码现在有一个Java 9+的基线。Java 8编译器不会理解这个文件。我们真正想要的是一个包含Java 8类文件的jar文件,但只有module-info.java 文件在Java 9+下编译。理论上,在Java 8上运行时,如果不使用module-info.class 文件,它将被忽略。
Maven有一种技术可以实现这一点。 虽然该技术效果还可以,但事实证明它远远不足以实现目标。 要想真正得到一个在Java 8和9+上都能运行的单个jar文件,你需要。
- 在Java 9+上使用发布标志,为Java 8进行构建
- 添加一个OSGi require能力过滤器,告知它仍然兼容Java 8
- 在Java 8上构建时从maven-javadoc-plugin中排除module-info.java
- 使用maven-javadoc-plugin v3.0.0-M1(非更高版本),手动将依赖关系复制到一个目录中,并使用额外的Javadoc命令行参数来引用它们,见本问题
- 从maven-checkstyle-plugin中排除module-info.java
- 从maven-pmd-plugin中排除module-info.java
- 手动调换maven-surefire-plugin的版本,以测试模块路径和类路径。
可能还有一些我忘了。这是集成Java 9之前的 pom.xml ��这是集成Java 9之后的 。从650行增加到862行,有很多复杂性、配置文件和变通方法。
如果以Java 11为基线,该项目将再次变得简单,但这个基线在若干年内不会出现。 请注意,我的评论不应该被解释为反对Maven。那里的一个小团队正在努力工作,尽力做到最好--JPMS很复杂。
只是为了好玩,你的项目不能再被安卓系统使用(因为那里的团队在添加一个简单的 "忽略模块信息 "规则方面似乎非常缓慢)。 许多使用旧版本字节码库(如ASM)的工具也会失败--我曾接到报告说,一个特定版本的Tomcat/TomEE无法加载模块化jar文件。 我最终不得不发布一个 "经典 "的非模块化jar文件来应对这些情况,这一点令人深感沮丧。
虽然我已经在我的一些项目中添加了module-info.java ,但我不建议其他人这样做--这是一个非常痛苦和耗时的过程。考虑它的时机似乎是在Java 11或更高版本被广泛采用并成为你的项目的基线时。
负面的好处
现在是有争议的部分。
我的结论是,目前设计的JPMS对开源库有 "负面的好处"。
如上所述,完全模块化的成本对库的开发者来说是很高的。 需要保留Java 8的兼容性使得JPMS真的很难使用(模块信息应该是文本的,而不是一个类文件)。 工具仍然不完整/有问题。 如果你真的去做,许多老项目不能应付新的jar文件。 这些大部分会随着时间的推移而改善,但我们说的是在Java 11被广泛采用之前的若干年。 但不要被忽悠,只相信等待会解决关键问题。
模块路径与类路径的分离(bifurcation)绝对是一场噩梦。 一下子,现在有两种不同的方式可以运行你的库,而且这两种环境有相当不同的质量。 在类路径上编译和运行的代码往往不能在模块路径上编译或运行。 反之亦然。作为一个库的作者,你无法控制是使用类路径还是模块路径。 你别无选择--你必须同时测试这两种环境,但你可能不会想到这样做。(而且Maven目前没有提供在一个pom.xml 。
考虑到这些努力和额外的复杂性,我们应该得到一些巨大的好处,对吗? 不是的。
JPMS应该确保可靠的配置(启动时所有的依赖都可用)和强大的封装(其他代码无法看到或使用你想隐藏的包)。 但由于没有办法阻止你的模块化jar文件在class-path上被使用,你得不到这些好处。
你花了很多精力来选择要隐藏的包吗?没有意义,因为用户可以直接把jar文件放在class-path上并调用你的内部包。 你相信JVM会在启动前检查你所有的依赖模块是否可用?恐怕不是,当用户把jar文件放在class-path上时,不会进行任何检查。
由于我们没有得到JPMS所宣称的任何好处,但却得到了很多额外的测试工作和构建工具的复杂性,我觉得 "负面的好处 "是一个相当准确的总结。
总结
从今天起,JPMS对库的作者来说是一种痛苦。模块路径和类路径的分离是问题的核心。 你真的不能在不对模块路径和类路径进行测试的情况下发布一个库--在环境不同的情况下有一些微妙的角落。
现在迫切需要的是对JPMS的一个小改动。需要有一种方法,让库的作者坚持他们生产的模块化jar文件只能在模块路径上运行(任何试图在类路径上使用它的尝试都会阻止应用程序的启动)。这将消除同时测试类路径和模块路径的需要。随着时间的推移,JPMS可能会实现它的目标,并从负面的好处变成正面的好处。