JPMS模块对于库包开发人员的负面效应

Java 9引入了一个主要的新功能:JPMS,即Java平台模块系统,但是对于专门提供库包开发的程序员却有负面效果。

Java 8可能是有史以来最成功的Java版本,它被广泛使用,因此,几乎所有开源库都在Java 8上运行。

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 8在Java 11发布后不久将不再受支持,因此合理的升级路径应该是11,不幸的是,迅速转移到Java 11可能存在风险。

模块路径
首先了解一下类路径和模块路径之间的区别,类路径仍然存在于Java 9+中,以和Java8相同的方式工作。

模块路径则是新的,当jar文件位于模块路径上时,任何module-info都会用于更严格的JPMS规则,例如,一个public方法不再可调用,除非它被明确配置为exported。

升级以后,需要将旧的非模块化jar文件放在类路径class-path上,同时将新的模块化jar文件放在模块路径module-path上,然而,没有人可以强制你执行,这就带来会造成混乱的情况,有四种可能性:

1. 在模块路径上的放置你的模块化后的jar包
2. 在类路径上的放置你的模块化jar包
3. 在模块路径上的放置非模块化jar包
4. 在类路径上的放置非模块化jar包

为确保你的库包正常工作,需要在类路径和模块路径上进行测试。例如,与类路径相比,模块路径上的服务加载非常不同。一些资源查找方法也完全不同。

进一步复杂化的是,JPMS没有明确支持测试,为了测试模块路径上的模块(这是一个紧密锁定的软件包集),必须使用--patch-module 命令行标志,此标志有效地扩展了模块,将测试包添加到与测试类相同的模块中,(如果你只测试公共API,你可以在不使用补丁模块的情况下执行此操作,但是在Maven中你需要一个全新的项目和pom.xml来实现这种方法,可能很少见。)

在最新的Maven surefire插件(v2.21.0及更高版本)中,使用了patch-module标志,但如果你的模块具有可选的依赖项,或者您有其他测试依赖项,则可能必须手动添加它们。

鉴于这种情况,开源库包开发人员应该做些什么?

选项1,什么都不做
在大多数情况下,在Java 8或更早版本上编译的代码在Java 9+的类路径上运行得很好,所以,你可以什么都不做,忽略了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文件可能还会放在类路径或模块路径上,两者的行为可能完全不同,事实上,现在它有一些模块信息,工具可能会以不同的方式对待它。

当Maven看到Automatic-Module-Name它时,它通常将其放在模块路径而不是类路径上,这样可能反而没有任何效果,达不到我们使用这个选项的目的;或者它可能会显示你的代码在类路径上可工作,但在模块路径上不工作的错误。

现在使用Maven,必须使用surefire插件v2.20.1来测试类路径(一个不知道模块路径的旧版本),要在模块路径上进行测试,请使用v2.21.0,在这两个版本之间进行切换当然需要一个手动过程。


选项3,添加module-info.java
这是关于JPMS的众多网页和教程中描述的完全模块化方法。


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文件,需要:

1. 使用Java 9+上的发布一个微构建Java 8的标识

2. 添加一个OSGi,实现功能过滤器,让它仍然与Java 8兼容

3. 在Java 8上构建时,从maven-javadoc-plugin中排除module-info.java

4. 使用maven-javadoc-plugin v3.0.0-M1,手动将依赖项复制到目录,并使用其他Javadoc命令行参数引用它们。

5. 从maven-checkstyle-plugin中排除module-info.java

6. 从maven-pmd-plugin中排除module-info.java

7. 手动交换maven-surefire-plugin的版本来测试模块路径和类路径

集成 Java 9 之后,代码可能会从650行增加到862行,增加了很多复杂性,包括配置文件和变通方法。

升级Java9+以后,你的项目可能再也不能被Android使用了(因为这个团队似乎在添加一个简单的“忽略模块信息”规则方面动作很慢);许多使用旧版字节码库(如ASM)的工具也会失败 - 我有一个报告称特定版本的Tomcat / TomEE无法加载模块化jar文件,我最终不得不发布一个“旧的”的非模块化jar文件来应对这些情况,这是非常令人沮丧的。

虽然我已经添加module-info.java到了一些项目,但我不能推荐其他人这样做 - 这是一个非常痛苦和耗时的过程。考虑它的时间似乎是等Java 11或更高版本被广泛采用时。

总结
截至今天,JPMS对库包发布者来说是一种痛苦,模块路径与类路径的分离是问题的核心。如果没有在模块路径和类路径上进行测试,你真的无法发布一个库包 - 有一些细微的角落情况,环境不同。