Java 模块9 - JPMS模块的命名

151 阅读12分钟

Java平台模块系统(JPMS)即将到来,作为项目Jigsaw开发。本文在介绍之后,探讨了模块应该如何命名。

与所有的 "最佳实践 "一样,它们最终都是写作者的观点。但我希望能说服你,我的观点是正确的;-)。作为一个社区,如果每个人都遵守同样的规则,我们肯定会受益,就像我们从每个人使用反向DNS的包名中受益一样。

TL;DR- 我的最佳实践

这些是我对模块命名的建议:

  • 模块名称必须是反向DNS,就像包名一样,例如org.joda.time。
  • 模块是一组包。因此,模块名称必须与包的名称相关。
  • 强烈建议模块名称与超级包的名称相同
  • 用一个特定的名字创建一个模块**,就取得了这个包名和它下面的一切的所有权**。
  • 作为该命名空间的所有者,任何子包都可以根据需要归入子模块,只要没有包在两个模块中。

因此,下面是一个命名良好的模块:

  module org.joda.time {
    requires org.joda.convert;

    exports org.joda.time;
    exports org.joda.time.chrono;
    exports org.joda.time.format;
    // not exported: org.joda.time.base;
    // not exported: org.joda.time.tz;
  }

可以看出,该模块包含一组包(导出的和隐藏的),都在一个超级包之下。 模块的名称与超级包的名称相同。该模块的作者主张控制所有低于org.joda.time 的名称,如果需要,可以在未来创建一个模块org.joda.time.18n

要了解为什么这种方法有意义,以及更细的细节,请继续阅读。

TABLE.sjc { border-collapse: collapse; border:1px solid black; } TABLE.sjc TD, TABLE.sjc, TH { border:1px实心黑色; padding:2px 8px; }

JPMS的命名

在软件中给任何东西命名都是困难的。不出所料,商定一个命名模块的方法也被证明是困难的

命名规则允许点,但禁止破折号,因此很多名字的选择被关闭了。 顺便提一下,JVM中的模块名称更加灵活,但我们在这里只考虑Java层面的名称。

这是我认为有意义的两种基本方法:

1)项目式:短名称,如Maven中心的jar文件名中常见的那样。

2)反向DNS:全名,与我们从Java v1.0开始用于包名的方式完全相同。

下面是一些例子,可以更清楚地说明问题:

项目式反向DNS
Joda-Timejoda.timeorg.joda.time
共用-IO共识.IOorg.apache.commons.io
Strata-基础知识strata.basicscom.opengamma.strata.basics
架构淘宝网org.junit

在同等条件下,我们会选择更短的名字--project-style。 在阅读module-info.java文件时,它当然更有吸引力。 但有一些明显的理由表明,必须选择反向DNS。

值得注意的是,马克-莱因霍尔德目前表示更倾向于项目式名称。然而,链接的邮件并没有真正处理命名问题中的全局唯一性或冲突元素,专家组中的其他人不同意项目式名称。

所有权和唯一性

Java的最初设计者做出了一个非常精明的选择,为包提出了反向DNS名称。这种方法在开放源码软件令人难以置信的崛起中得到了很好的扩展。它提供了两个关键属性--所有权和唯一性。

逆向DNS的所有权方面将全球DNS名称空间的一部分控制权交给了个人或公司。这是一个普遍同意的方法,有足够的标识符的广度,使冲突很少发生。在该名称空间内,开发人员负责确保唯一性。这两个方面结合起来,就形成了全球唯一的包名。因此,尽管现代的应用程序有数以百计的依赖性jar文件,但代码中出现两个相撞的包是相当罕见的。例如,Spark框架Apache Spark尽管有相同的简单名称,但却共存。但看看如果我们只使用项目式的名字会发生什么。

项目式反向-DNS
Spark框架spark.corecom.sparkjava.core
Apache-Spark火花网org.apache.spark.core

可以看出,项目风格的名字发生了冲突!JPMS将简单地拒绝启动两个模块名称相同的模块路径,即使它们包含不同的包。 由于这些项目还没有选择模块名称,我已经调整了这个例子,使它们发生冲突。但这个例子远非不可能,这才是重点!)

想象一下,如果包名不是反向DNS,会发生什么? 如果你的应用程序拉入数百个依赖,你认为会没有重复的吗?

当然,我们今天在Maven中使用了项目式名称--jar文件名就是artifactId ,这是一个项目式名称。既然如此,为什么我们今天没有问题呢?事实证明,如果出现冲突,Maven会很聪明地重命名工件。 JPMS没有这种能力--在出现冲突时,你唯一的选择就是重写有问题模块的模块信息类文件以及所有引用该模块的其他模块。

作为项目名称冲突的最后一个例子,考虑到一家初创公司创建了一个新的项目--"willow"。 由于他们规模很小,他们选择了 "willow "作为模块名称。 在接下来的一年里,这家初创公司变得非常成功,以指数级的速度增长,这意味着现在公司内部有100多个模块都依赖于 "willow"。 但后来一个新的开源项目启动了,并称自己为 "willow"。 现在,该公司不能使用该开源项目。 该公司也不能将 "willow "作为开源项目发布。 如果使用反向DNS名称,这些冲突就可以避免了。

总结这一节,我们需要反向DNS,因为模块名称需要全球唯一,即使是在编写注定要保持私有的模块时也是如此。逆向DNS的所有权方面为公司提供了足够的命名空间分离,以获得必要的唯一性。 毕竟,你不会想把Joda-Time和也叫Joda的货运公司搞混吧?

模块作为包的聚合体

JPMS的设计从根本上来说很简单--它扩展了JVM的访问控制,增加了一个新的概念 "模块",将一组包聚集在一起。鉴于此,模块的概念和包的概念之间存在着非常强的联系。

关键的限制是,一个包必须在一个而且只有一个模块中找到

鉴于一个模块是由一个或多个包组成的,那么在概念上可以选择的最简单的名字是什么?我认为是构成模块的包的名字之一。因此是一个你已经选择的名字。 现在,考虑我们有一个有三个包的项目,这三个包中哪一个应该是模块的名字?

  module ??? {
    exports org.joda.time;
    exports org.joda.time.chrono;
    exports org.joda.time.format;
  }

同样,我认为这并不存在真正的争论。有一个明确的超级包,这就是应该被用作模块名称的东西--在这个例子中,org.joda.time

隐藏包

在 JPMS 中,一个模块可以隐藏包。隐藏后,内部包在 Javadoc 中不可见,在module-info.java文件中也不可见。这意味着模块的消费者没有办法立即知道一个模块有哪些隐藏包。

现在再考虑一下关键的限制,即一个包必须在一个且只有一个模块中找到。 这个限制适用于隐藏的包和导出的。 因此,如果你的应用程序依赖于两个模块,而这两个模块都有相同的隐藏包,你的应用程序就不能运行,因为包会发生冲突。 而且由于隐藏包的信息很难获得,这种冲突会令人吃惊。(有一些高级方法可以使用层来解决这些冲突,但这些方法是为容器而不是应用程序设计的)。

解决这个问题的最好办法正是上一节所描述的。 考虑一个有三个导出包和两个隐藏包的项目。 只要隐藏包是模块名称的子包,我们就应该没事:

  module org.joda.time {
    exports org.joda.time;
    exports org.joda.time.chrono;
    exports org.joda.time.format;
    // not exported: org.joda.time.base;
    // not exported: org.joda.time.tz;
  }

通过使用超级包的名字作为模块的名字,模块开发者已经拥有了这个包和它下面所有东西的所有权。 只要所有非导出的包在概念上都是子包,最终用户的应用程序应该不会看到任何隐藏包的冲突。

自动模块

JPMS包括一个功能,即一个普通的jar文件,如果没有module-info.class文件,只需将其放在modulepath上,就会变成一种特殊的模块。 自动模块功能在总体上是有争议的,但其中的一个关键部分是,模块的名称是从jar文件的文件名派生出来的。 此外,这意味着编写module-info.java文件的人必须猜测别人将用于模块的名称。在我和其他许多人看来,不得不猜测一个名字,以及让Java平台根据jar文件的文件名来选择一个名字,都是不好的想法,但我们阻止它们的努力似乎已经失败

本文概述的命名方法提供了一种缓解这种最坏影响的手段。 如果每个人都使用基于超级包的反向DNS,那么人们的猜测应该是相当准确的,因为名字的选择过程应该是相当直接的。

如果没有一个明确的超级包呢?

有两种情况需要考虑。

第一种情况是,确实有一个超级包,只是它没有代码。在这种情况下,应该使用隐含的超级包。(注意,这个例子是Google Guava,它的包名中没有guava!):

  module com.google.common {
    exports com.google.common.base;
    exports com.google.common.collect;
    exports com.google.common.io;
  }

第二种情况是,一个jar文件有两个完全不相关的超级包:

  foo.jar
  - package com.foo.util
  - package com.foo.util.money
  - package com.bar.client

这里正确的做法是将jar文件分成两个独立的模块。

  module com.foo.util {
    requires com.bar.client;
    exports com.foo.util;
    exports com.foo.util.money;
  }
  module com.bar.client {
    exports com.bar.client;
  }

如果不这样做,极有可能在某些时候引起冲突,因为com.foo.util 不可能要求对com.bar.client 命名空间的所有权。

如果com.bar.client 在转换为模块时要成为一个隐藏的包,那么可以不把它作为一个独立的模块,而是在模块的超级包下重新打包(也就是阴影):

  module com.foo.util {
    exports com.foo.util;
    exports com.foo.util.money;
    // not exported: com.foo.util.shade.com.bar.client;
  }

你可以有子模块吗?

是的。当一个模块的名字被选中时,开发者就控制了一个命名空间。这个命名空间由模块名称和它下面的所有子名称组成 - 子包名称和子模块名称。

对该命名空间的所有权允许开发者发布一个或多个模块。主要的限制条件是,不应该有两个包含相同包的发布模块。

这样做的一个副作用是,大型项目发布 "所有 "jar的做法将需要停止。当项目有很多独立的jar文件,但又想让最终用户依赖一个jar文件时,就会使用 "全部 "jar。这些 "所有 "jar文件在Maven依赖树中是个麻烦,但在JPMS依赖树中则是个灾难,因为没有办法覆盖元数据,这与Maven不同。

如果我的现有项目不符合这些准则怎么办?

严厉的建议是以不兼容的方式改变项目,使其符合准则。 Java SE 9中的JPMS是破坏性的。它没有采取提供所有必要的工具来满足当前部署中的所有边缘案例的方法。因此,一些jar文件和一些项目需要进行一些重大改造,这并不令人惊讶。

为什么忽略Maven artifactId?

JPMS是对Java平台(语言和运行时)的一种扩展。Maven是一个构建系统。两者都是必要的,但它们有不同的目的、需求和惯例。

JPMS是关于包的,把它们组合在一起形成模块,并把这些模块连接起来。 这样一来,开发者是在使用源代码,就像其他源代码一样。 源代码被打包成什么工件是一个独立的问题。 理解这种分离是很难的,因为目前模块和jar文件之间是一对一的映射,然而,我们不应该认为将来会一直是这样的。

这种分离的另一个例子是版本控制。JPMS几乎不支持版本,但Maven等构建系统却支持。运行应用程序时,Maven负责收集一套连贯的工件(jar文件)来运行应用程序,就像以前一样。只是其中有些可能是模块。

最后,Maven的artifactId并不是孤立存在的。Maven通过组合groupId、artifactId和分类器来制作唯一的标识符。只有这种组合才是全局唯一的,才能发挥其作用。仅仅挑出artifactId,并试图用它来制作一个独特的模块名称是自找麻烦。

也请看这篇关于模块与工件的后续文章。

总结

JPMS的模块名称,以及一般的module-info.java,都需要真正的思考才能正确。模块声明和方法签名一样,都是你的API的一部分。

之所以如此重要,是因为与Maven和其他模块系统不同,JPMS没有办法修复损坏的元数据。 如果你依赖一些模块化的jar文件,并在模块声明中出现冲突或发现其他错误,你唯一的选择就是不使用JPMS或自己重写模块声明。 鉴于这种困难,目前还不清楚JPMS是否会取得成功,因此你最好的选择可能是不将你的代码模块化。

有关模块名称建议的摘要,请参见上面的TL;DR部分, 欢迎反馈和提问。