这篇文章是我关于Java平台模块系统(JPMS)的系列文章,主要讨论自动模块。JPMS的前身是Project Jigsaw,是Java SE 9中的模块系统。请参见模块基础知识、模块命名和模块与工件。
自动模块
假设你是Java的负责人,在20年后你想为平台添加一个模块系统,除了设计模块系统本身的问题外,你还必须考虑所有用Java(以及在一定程度上用其他JVM语言)编写的现有代码的迁移。
JPMS选择的解决方案是自动模块。 不幸的是,我认为这是个错误的解决方案。
为了理解自动模块,我们必须先看看将来如何指定jar文件。 除了classpath之外,Java SE 9还将有一个modulepath。 其基本思想是,模块(包含module-info.class的jar文件)将被放在modulepath上,而不是classpath上。 事实上,将一个模块放在classpath上会导致模块声明(module-info.class)被完全忽略,这通常不是你想要的结果。
作为一条基本规则,模块路径不能看到classpath。 如果你创建了一个模块并把它放在模块路径上,它的所有依赖项也必须在模块路径上。 因此,为了编写一个模块,所有的依赖项也必须被转换为模块。 而这些依赖项中有许多可能是开源项目,有不同的发布时间表。
很明显,这是个问题。 基本上,这意味着一个应用程序需要等到每个依赖关系都变成模块后才能添加module-info.java。
自动模块是一个普通的jar文件--没有module-info.class文件--被放在modulepath上。 因此modulepath将包含两种类型的模块--"真实 "和 "自动"。 由于没有module-info.class,自动模块缺少通常与模块相关的元数据:
- 没有模块名称
- 没有导出的包的列表
- 没有依赖性列表
不出所料,导出包列表被简单地设置为jar文件中的所有包。 依赖项列表被设置为模块路径上的所有东西,加上整个classpath。 因此,自动模块允许模块路径依赖classpath,这是通常不允许的。 虽然不理想,但这两个元数据的默认值都是合理的。
最后一个缺失的信息是模块名称。 这一直是Project Jigsaw的一个重要讨论点。目前,模块名称将来自jar文件的文件名。 对我来说,这是自动模块基本设计的一个关键问题(但见下文的缓解部分)。
正如我上一篇博客所说,模块不是工件。 但jar文件的文件名通常是Maven的工件名(artifactId),这个名字与模块名(应该是超级包的反向DNS)脱节。 例如,Google Guava的文件名是guava ,而正确的模块名应该是com.google.common 。
孤立地看,这一切都很正常。现在应用程序可以被模块化,而我可以依赖一个没有被模块化的项目:
module com.foo.myapp {
requires guava; // guava not yet modularised, so use the filename
}
但是为了工作, module-info.java 文件必须指定依赖的是文件名,而不是模块名。
精明的人会注意到,在依赖关系成为模块之前,通常不可能依赖模块名称。 但同样,依赖文件名是使用一个一旦依赖关系变成模块就会出错的名称:
module com.foo.myapp {
requires com.google.common; // now guava has been modularised
}
这种名称的变化才是问题的核心。 从本质上说,这意味着一旦你的库/应用程序被模块化,用于定义依赖关系的名称就会改变。 虽然这种名称的变化在私人代码库中没有问题,但在Java开源世界中,这将是地狱。
自动模块的影响
为了充分理解自动模块对开源Java社区的影响,最好是看一个用例。 让我们考虑一下,如果一个开源项目的发布依赖于一个文件名而不是模块名,会发生什么。 然后另一个开源项目的发布也依赖于此。
| 项目 | 版本 | 模块名称 | 需要 |
|---|---|---|---|
| Strata | v1 | com.opengamma.strata | org.joda.convert |
| java | |||
| Joda-Convert | v1 | org.joda.convert | 瓜娃 |
| java | v1 | (还不是一个模块) |
我们现在有一个由三个项目组成的图,其中最低的是一个自动模块,接下来的两个是依赖于自动模块的真实模块。 当Guava最终被模块化时,会出现一个新的版本。 但是Strata和Joda-Convert不能立即使用新的版本,因为他们引用的模块名称现在是错误的:
| 项目 | 版本 | 模块名称 | 需要 |
|---|---|---|---|
| Strata | v1 | com.opengamma.strata | org.joda.convert |
| 瓜娃 | |||
| Joda-Convert | v1 | org.joda.convert | 瓜娃 |
| 瓜娃 | v2 | com.google.common | |
| 模块地狱 - "guava" != "com.google.com" |
可以看出,这种设置并不奏效。我们有模块地狱 前面两个项目依赖于 "guava",而不是 "com.google.common"。 而且没有办法让同一个包在两个不同的模块名下加载。
如果Joda-Convert被更新以匹配新的Guava会怎样?(Strata不被更新)
| 项目 | 版本 | 模块名称 | 需要 |
|---|---|---|---|
| Strata | v1 | com.opengamma.strata | org.joda.convert |
| 瓜娃 | |||
| Joda-Convert | v2 | org.joda.convert | com.google.common |
| java | v2 | com.google.common | |
| 模块地狱 - "guava" != "com.google.com" |
这个配置不起作用,没有办法让相同的包在两个不同的模块名下加载。
如果Strata被更新以匹配新的Guava会怎样?(Joda-Convert不被更新)
| 项目 | 版本 | 模块名称 | 需要 |
|---|---|---|---|
| ▪ Strata | v2 | com.opengamma.strata | org.joda.convert |
| com.google.common | |||
| Joda-Convert | v1 | org.joda.convert | 瓜娃 |
| java | v2 | com.google.common | |
| 模块地狱 - "guava" != "com.google.com" |
这个配置也不行,没有办法让两个不同的模块名下加载相同的包。
唯一能让它工作的方法是一起更新整个堆栈:
| 项目 | 版本 | 模块名称 | 需要 |
|---|---|---|---|
| Strata | v2 | com.opengamma.strata | org.joda.convert |
| com.google.common | |||
| Joda-Convert | v2 | org.joda.convert | com.google.common |
| java | v2 | com.google.common |
总而言之,当一个模块依赖于一个自动模块,而这个模块又被其他模块所依赖时,整个堆栈就被链接起来了。 堆栈中的所有东西都必须一起从v1到v2。
回顾整个例子,应该很清楚,问题从一开始就开始了。 从来就不应该有一个依赖于文件名 "guava "的Strata的v1版本。相反,Strata应该等到Guava和Joda-Convert都发布了模块化的V2版本。 而且,Joda-Convert应该等到Guava发布了模块化的V2版本。为了避免模块地狱,迁移必须从下往上进行。
鉴于此,我认为这意味着自动模块本身并不能为Java开源社区提供一条可行的迁移路径。 规则如下。
不要向Maven Central发布表达对文件名依赖的模块化jar文件。相反,要等到所有依赖关系都能表达为模块名称。
Maven Central中任何表达对文件名依赖的jar文件都会成为模块地狱的原因。
缓解措施
如果没有任何缓解措施,社区将不得不从堆栈底部向上逐一对每个开源库进行模块化。 在所有依赖关系被模块化之前,任何开源项目都不能做任何事情--这是自下而上的迁移。
针对这个问题提出的主要缓解措施是,jar文件可以在MANIFEST.MF中有一个名为 "Automatic-Module-Name "的新条目。 当JPMS检查一个自动模块时,如果MANIFEST.MF条目存在,那么它将使用该值作为模块名称而不是文件名。
这在一定程度上可以用来打破这个循环。 在上面的例子中,Strata团队可以在任何时候发布一个带有新的MANIFEST.MF条目的版本,基本上说明了Strata在未来会有什么样的模块名称。同样,Joda-Convert也可以在任何时候发布,并在MANIFEST.MF条目中说明其模块名称。 完全的模块化仍然需要自下而上地进行,但任何依赖Joda-Convert或Strata的人都可以安全地发布,而不会受到Guava模块名称问题的影响。
添加MANIFEST.MF条目和添加模块声明的关键区别在于,MANIFEST.MF条目不需要指定依赖关系的名称。 因此,在添加MANIFEST.MF条目之前,没有必要等待依赖关系被模块化。
因此,上述规则也可以表述为:
不要向Maven Central发布依赖自动模块的模块化jar文件,除非该自动模块有一个 "Automatic-Module-Name "MANIFEST.MF条目。
我认为这并不理想,但我们现在的情况就是如此。 因此,给开源社区的信息分为两部分。
首先,不要添加module-info.java 模块声明直到:
- 你所有的运行时依赖已经被模块化了(无论是作为一个完整的模块还是MANIFEST.MF条目)
- 所有这些模块化的依赖都已发布到Maven中心
- 您的库依赖于更新后的版本
其次,如果您不能满足这些条件,但您的项目结构良好,适合模块化,请按照约定的模块命名规则(超级包反向DNS)添加一个MANIFEST.MF条目。
如果每个人都这样做,那么我们就有可能避免模块地狱。
总结
自动模块允许模块依赖于非模块。 但这是通过在文件名而不是模块名上指定一个requirements条款来实现的。 如果模块的发布依赖于文件名,这将导致以后的痛苦。 一个新的MANIFEST.MF条目允许任何开源项目选择一个模块名称并立即发布该选择。 当JPMS看到MANIFEST.MF条目时,它将使用该值作为自动模块的名称。