Java平台模块系统(JPMS)是Java SE 9的主要新功能。在本文中,我将对其进行介绍,而我的大部分意见将留给后续文章。 这是基于这些幻灯片 。
Java平台模块系统(JPMS)
开发为Project Jigsaw的新模块系统旨在提高Java编码的抽象级别,如下所示:
该项目的主要目标是:
- 使Java SE平台和JDK更容易地扩展到小型计算设备;
- 总体上提高Java SE平台实现的安全性和可维护性,尤其是JDK。
- 改善应用程序性能; 和
- 使开发人员更容易为Java SE和EE平台构建和维护库和大型应用程序。
为了实现这些目标,我们建议为Java SE平台设计和实现一个标准模块系统,并将该系统应用于平台本身以及JDK。 该模块系统应具有足够的功能以模块化JDK和其他大型遗留代码库,但仍可供所有开发人员采用。
但是,正如我们将看到的,项目目标并非总是能够实现。
什么是JPMS模块?
JPMS是对Java库,语言和运行时的更改。 这意味着它会影响开发人员日常编写代码的整个堆栈,因此JPMS可能会产生很大的影响。 出于兼容性原因,大多数现有代码可以忽略Java SE 9中的JPMS,这可能非常有用。
掌握的关键概念点是JPMS向JVM模块添加了新概念。 在以前的地方,代码被组织为字段,方法,类,接口和包,而Java SE 9有了一个新的结构元素-模块。
- 类是字段和方法的容器
- 包是类和接口的容器
- 模块是包装的容器
因为这是一个新的JVM元素,所以它意味着运行时可以应用强大的访问控制。 使用Java 8,开发人员可以通过将类声明为私有方法来表示其他类无法看到该类的方法。 使用Java 9,开发人员可以表示一个软件包无法被其他模块看到。 包可以隐藏在模块内。
从理论上讲,能够隐藏包对于应用程序设计应该是一个很大的好处。 不再需要使用Javadoc声明“请不要使用此程序包中的类型”的程序包命名为“ impl”或“内部”。 不幸的是,生活不会那么简单。
但是,创建模块相对简单。 模块通常只是一个jar文件,其根目录具有module-info.class
文件-称为模块化jar文件 。 该文件是从您的源数据库中的module-info.java
文件创建的(请参阅下面的更多信息)。
使用模块化jar文件涉及将jar文件添加到modulepath而不是classpath中。 如果模块化jar文件位于类路径上,则它将根本不充当模块,并且module-info.class
将被忽略。 这样,虽然modulepath上的模块化jar文件将具有由JVM强制执行的隐藏包,但classpath上的模块化jar文件将根本没有隐藏包。
其他模块系统
Java历史上有其他模块系统,最著名的是OSGi和JBoss模块。 重要的是要了解JPMS与这些系统几乎没有相似之处。
没有JVM的直接支持,OSGi和JBoss模块都必须存在,但仍然为模块提供一些其他支持。 这是通过在自己的类加载器中启动每个模块来实现的,该技术可以完成任务,但并非没有问题。
毫不奇怪,鉴于这些是现有的模块系统,因此来自这些小组的专家已被纳入开发JPMS的正式专家小组中。 但是,这种关系并不和谐。 基本上,JPMS作者(Oracle)已着手构建JVM扩展,该扩展可用于可描述为模块的事物,而现有的模块系统则从大型应用程序中的实际用例和棘手的边缘情况中获得经验和价值。今天存在。
在阅读有关模块的内容时,重要的是要考虑您正在阅读的文章的作者是否来自OSGi / JBoss Modules设计阵营。 (尽管我曾经使用过Eclipse和其他内部使用OSGi的工具,但我从未积极使用OSGi或JBoss模块。)
module-info.java
module-info.java
文件包含定义模块的指令(此处介绍了最重要的指令,但还有更多内容)。 这是一个.java
文件,但是语法与您之前看到的任何.java
文件都不一样。
创建文件必须回答两个关键问题–该模块依赖什么以及导出什么?
-
module com.opengamma.util {
-
requires org.joda.beans; // this is a module name, not a package name
-
requires com.google.guava;
-
-
exports com.opengamma.util; // this is a package name, not a module name
-
}
(用于模块的名称需要单独撰写一整篇文章 ,为此,我将使用包名样式)
该模块声明说com.opengamma.util取决于(需要)org.joda.beans和com.google.guava。 它导出一个包com.opengamma.util。 使用模块路径(由JVM强制)时,所有其他软件包均被隐藏。
对JDK的核心模块java.base存在隐式依赖。 请注意,JDK本身也是模块化的,因此,如果要依赖Swing,XML或Logging,则需要表达这种依赖关系。
-
module org.joda.beans {
-
requires transitive org.joda.convert;
-
-
exports org.joda.beans;
-
exports org.joda.beans.ser;
-
}
该模块声明说org.joda.beans
依赖于(要求) org.joda.convert
。 与简单的“ requires”相对,“ requirestransive”意味着任何需要org.joda.beans
模块也可以查看和使用org.joda.convert
的包。 这是在这里使用的,因为Joda-Beans的方法的返回类型来自Joda-Convert。 这由虚线示出。
-
module org.joda.convert {
-
requires static com.google.guava;
-
-
exports org.joda.convert;
-
}
该模块声明说org.joda.convert
依赖于(需要) com.google.guava
,但是仅在编译时才“需要静态”,而不是简单的“需要”。 这是一个可选的依赖项。 如果番石榴在模块路径上,则Joda-Convert将能够看到和使用它,并且如果不存在番石榴,则不会发生任何错误。 这由虚线示出。
访问规则
在应用了JVM访问规则的模块路径上运行模块化jar时,如果满足以下条件,则程序包A中的代码可以看到程序包B中的类型:
- 公开的类型
- 包B是从其模块导出的
- 从包含程序包A的模块到包含程序包B的模块之间存在依赖性
因此,在上面的示例中,模块com.opengamma.util
可以看到org.joda.beans
包, org.joda.beans,ser
, org.joda.convert
包以及Guava导出的任何包。 但是,它看不到包org.joda.convert.internal
(因为它没有被导出)。 另外,代码模块com.google.guava
在软件包org.joda.beans
或org.joda.convert
不到代码,因为没有模块依赖性。
有什么问题吗?
上面描述的基础很简单。 最初很容易想象如何从这些基础上构建应用程序并从隐藏包中受益。 不幸的是,很多事情都会出错。
1)仅当在modulepath上使用模块化jar时,才适用module-info文件。 为了兼容性,类路径上的所有代码都打包为一个特殊的未命名模块 ,没有隐藏的包,并且可以完全访问整个JDK。 因此,隐藏包的安全性优势至多是微不足道的。 但是,JDK本身的模块始终以模块化模式运行,因此始终可以保证安全性。
2)不处理模块的版本。 您不能两次加载相同的模块名称-您不能两次加载同一模块的两个版本。 它完全由您自己决定,然后由您的构建工具决定,以创建可以实际运行的一组连贯的模块。 因此,无法解决由版本冲突导致的类路径地狱情况。 请注意,将版本号放在模块名称中是一个坏主意,它不能解决此问题并会创建其他问题。
3)两个模块不能包含相同的包装。 在您认为它也适用于隐藏包之前,这似乎非常明智。 由于隐藏的软件包未在module-info.class
列出,因此Maven之类的工具必须解压缩jar文件才能发现其中存在哪些隐藏的软件包,以警告冲突。 作为库的用户,这样的冲突将完全令人惊讶,因为您不会在Javadoc中看到任何隐藏的包。 这是一个更普遍的迹象,表明JPMS无法在模块之间提供足够的隔离,原因目前尚不清楚。
4)在编译时和运行时,模块之间必须没有周期。 再次,这似乎是明智的–谁想让模块A依赖于B依赖于C依赖于A? 但是现有项目的现实是这种情况会发生,并且在类路径上也不是问题。 例如,考虑上例中Guava决定依赖Joda-Convert会发生什么。 这种限制将使一些现有的开源项目难以移植。
5)反思正在发生变化,因此非公共领域和方法将不再可以通过反思来访问。 由于几乎每个框架都以这种方式使用反射,因此迁移现有代码将需要大量工作。 特别是,JDK将非常难以锁定以防止反射,这可能会很痛苦(命令行标志暂时可以逃脱陷阱)。 本文没有机会探讨模块声明如何影响反射–有关更多详细信息,请参见幻灯片中的“打开”。
6)您的依赖项是否模块化? 从理论上讲,只有在所有依赖项也是模块之后,您才能将代码转换为模块。 对于具有数百个jar文件依赖项的任何大型应用程序,这将是一个问题。 “解决方案”是自动模块 ,其中放置在modulepath上的普通jar文件会自动转换为模块。 这个过程是有争议的,命名是一个大问题。 库作者不应将依赖自动模块的模块发布到Maven Central之类的公共存储库,除非他们具有“自动模块名称”清单条目。 再次,自动模块应有各自的特色!
7)模块命名尚未确定。 我已经开始相信, 在模块包含的最高软件包之后命名模块(这是使该模块“获得”子软件包的所有权)是唯一明智的策略。
8)与构建系统冲突–谁负责? Maven pom.xml
还包含有关项目的信息。 是否应该扩展它以允许添加模块信息? 我不建议这样做,因为module-info.java
文件包含您API的绑定部分,最好用.java代码表示,而不是像pom.xml
这样的元数据表示。
对于那些想要更深入地阅读本书的人,请尝试Nicolai的这本书。
摘要
不要对JPMS – Java 9中的模块感到兴奋。以上只是对module-info.java
文件的可能情况以及module-info.java
的限制的概述。 如果您打算对您的库或应用程序进行模块化,请稍等片刻,直到一切变得更加清晰。
本文最初发表在Stephen Colebourne的博客上 。
Stephen Colebourne将在伦敦的JAX上发表一个演讲,他将介绍Java平台模块系统的基础及其含义,特别是对反思的影响。
翻译自: https://jaxenter.com/java-9-modules-jpms-basics-135885.html
Stephen Colebourne's blog: Java SE 9 - JPMS module naming 关于 Java 及其他世界的思考与沉思
2017年4月20日(星期四)
Javase 9-JPMS 模块命名
Java 平台模块系统(JPMS)即将到来,开发成 Jigsaw 项目。本文紧随介绍之后,着眼于模块应该如何命名。
正如所有的“最佳实践”一样,它们最终是编写它们的人的意见。然而,我希望能说服你我的观点是正确的。作为一个社区,如果每个人都遵循同样的规则,我们肯定会受益,就像我们从每个人使用反向 dns 的包名称受益一样。
我的最佳实践
以下是我对模块命名的建议:
- 模块名称必须是反向 dns,就像包名称一样,例如 org.joda.time。
- 模块是一组包。因此,模块名称必须与包名称相关。
- 强烈建议模块名称与 super-package 的名称相同。
- 创建具有特定名称的模块将获得该包名称及其下面所有内容的所有权。
- 作为该名称空间的所有者,只要没有包在两个模块中,任何子包都可以按照需要分组为子模块。
因此,以下是一个命名良好的模块:
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. 18 n 模块。
要理解为什么这种方法有意义,以及更详细的细节,请继续阅读。
喷气推进实验室命名
在软件中命名任何东西都是困难的。毫不奇怪,对模块命名的方法达成一致也很困难。
命名规则允许点,但禁止破折号,因此关闭了许多名称选项。顺便说一句,JVM 中的模块名称更灵活,但我们这里只考虑 Java 级别的名称。
以下是我认为有意义的两个基本方法:
1) Project-style. 缩写名称,就像在 Maven Central 的 jar 文件名中常见的那样。
2)反向 DNS: 完整的名称,就像我们从 Java v 1.0开始使用的包名称一样。
这里有一些例子可以更清楚地说明:
项目风格 | 反向 dns | |
---|---|---|
Joda-Time | Joda 时间到 | Org. joda. time |
Commons-IO | commons.io | org.apache.commons.io |
地层-基础知识 | strata.basics | 基本功能 |
JUnit | 朱尼特 | Org-junit |
在所有条件相同的情况下,我们会选择较短的名称-项目风格。当读取 module-info.java 文件时,它肯定更有吸引力。但有一些明确的原因,为什么反向 dns 必须选择。
值得注意的是,markreinhold 目前表示偏好项目样式的名称。但是,链接邮件并不真正处理命名问题的全局唯一性或冲突元素,专家组中的其他人不同意项目样式的名称。
所有权和独特性
Java 的最初设计者对提议的反向 dns 软件包名做了一个非常精明的选择。随着开源软件的不可思议的兴起,这种方法已经扩展得非常好。它提供了两个关键属性——所有权和惟一性。
反向 DNS 的所有权方面将全局 DNS 命名空间的一部分控制权委托给个人或公司。这是一个普遍认可的方法,具有足够宽度的标识符,使冲突罕见。在该名称空间中,开发人员负责确保惟一性。总之,这两个方面导致全局唯一的包名称。因此,尽管现代应用程序拉入了数百个相互依赖的 jar 文件,但是代码中有两个相互冲突的包是非常罕见的。例如,尽管 Spark 框架和 apachespark 具有相同的简单名称,但它们是共存的。但是看看如果我们只使用项目样式的名称会发生什么:
项目风格 | 反向 dns | |
---|---|---|
Spark 框架 | 火花,核心 | com.sparkjava.core |
Apache-Spark | 火花,核心 | org.apache.spark.core |
正如可以看到的,项目风格的名称冲突!JPMS 会简单地拒绝启动两个模块名称相同的模块路径,即使它们包含不同的包。(由于这些项目还没有选择模块名称,我调整了示例以使它们相互冲突。但是这个例子并不是不可能的,这就是重点!)
不相信?想象一下如果包名称不是反向 dns 会发生什么。如果您的应用程序引入了数百个依赖项,您认为不会有重复项吗?
当然,我们现在在 Maven 中有项目样式的名称—— jar 文件名是 artifactId,是一个项目样式的名称。既然如此,为什么我们今天没有问题呢?事实证明,Maven 足够聪明,如果发生冲突,它会重命名这个工件。JPMS 没有提供这种能力——你唯一的选择就是重写有问题的模块的模块信息类文件,以及所有引用它的其他模块。
作为项目样式名称冲突如何发生的最后一个示例,请考虑创建一个新项目的启动程序——“ willow”。因为它们很小,所以它们选择了一个名为“ willow”的模块。在接下来的一年里,这家创业公司取得了惊人的成功,以指数级的速度增长,这意味着公司内部现在有100多个模块依赖于“柳树”。但是随后一个新的开源项目启动了,并称自己为“ 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; }
同样,我认为这并不是一个真正的辩论。有一个清晰的 super-package,在这种情况下,它应该作为模块名称-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; }
通过使用 super-package 名称作为模块名称,模块开发人员获得了该包及其下面所有内容的所有权。只要所有未导出的包在概念上都是子包,最终用户应用程序就不会看到任何隐藏的包冲突。
自动模块
JPMS 包含一个特性,即一个没有 module-info.class 文件的普通 jar 文件,只需将其放在 modulepath 上,就可以变成一种特殊类型的模块。自动模块特性通常是有争议的,但其中一个关键部分是模块的名称来自 jar 文件的文件名。此外,这意味着编写 module-info.java 文件的人必须猜测其他人将用于某个模块的名称。在我看来,猜测一个名字,让 Java 平台根据 jar 文件的文件名来选择一个名字都是错误的想法,也是其他许多想法的错误,但是我们阻止它们的努力似乎失败了。
本文中概述的命名方法提供了一种减轻这种情况的最坏影响的方法。如果每个人都使用基于超级包的反向 dns,那么人们的猜测应该相当准确,因为名称的选择过程应该相当简单。
如果没有一个明确的超级包呢?
有两种情况需要考虑。
第一种情况是真的有一个超级包,只是它没有代码。在这种情况下,应该使用隐含的 super-package。(请注意,这个例子是 Google 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 在转换为模块时将成为一个隐藏包,那么它不是一个单独的模块,而是可以在模块的 super-package 下重新打包(即阴影部分) :
module com.foo.util { exports com.foo.util; exports com.foo.util.money; // not exported: com.foo.util.shade.com.bar.client; }
你能有子模块吗?
是的。当选择模块名称时,开发人员将控制命名空间。该命名空间由模块名称和它下面的所有子名称组成——子包名称和子模块名称。
该名称空间的所有权允许开发人员释放一个或多个模块。主要的限制是不应该有两个包含相同包的已发布模块。
作为这种做法的一个副作用,大型项目释放“ all”jar 的做法将需要停止。当项目有许多单独的 jar 文件时,使用“ all”jar,但是也希望允许最终用户依赖于单个 jar 文件。这些“所有”jar 文件对于 Maven 依赖树来说是一个痛苦,但是对于 JPMS 来说是一个灾难,因为没有办法覆盖元数据,不像 Maven。
如果我现有的项目不符合这些准则怎么办?
严厉的建议是以不兼容的方式改变项目,使其符合指导方针。9中的 JPMS 是破坏性的。它没有提供满足当前部署中所有边缘情况所需的所有工具。因此,一些 jar 文件和一些项目需要一些重大的返工就不足为奇了。
为什么忽略 Maven artifactId?
JPMS 是 Java 平台(语言和运行时)的扩展。Maven 是一个构建系统。两者都是必要的,但它们有不同的目的、需要和惯例。
是关于软件包的,将它们组合在一起形成模块并将它们链接起来。通过这种方式,开发人员正在使用源代码,就像任何其他源代码一样。源代码包含哪些工件是一个单独的问题。理解这种分离是很困难的,因为目前在模块和 jar 文件之间存在一对一的映射,但是,我们不应该假设将来总是这样。
这种分离的另一个例子是版本控制。JPMS 对版本几乎没有支持,但是建立了像 Maven 这样的系统。在运行应用程序时,Maven 负责收集一组连贯的构件(jar 文件)来运行应用程序,就像以前一样。只是其中一些可能是模块。
最后,Maven artifactId 并不是孤立存在的。Maven 通过组合 groupId、 artifactId 和分类器来生成唯一标识符。只有这种组合在全球范围内足够独特,才有用。仅仅选择 artifactId 并试图从中创建一个唯一的模块名称是自找麻烦。
也可以看看这篇关于模块 vs 工件的后续文章。
摘要
JPMS 模块名称,以及通常的模块 -info.java,需要真正的思考才能正确。模块声明将与方法签名一样成为 API 的一部分。
重要性之所以被提高,是因为与 Maven 和其他模块系统不同,JPMS 没有办法修复损坏的元数据。如果您依赖于一些模块化 jar 文件,并且在模块声明中发现了冲突或其他错误,那么您唯一的选择就是不使用 JPMS 或者自己重写模块声明。鉴于这种困难,目前还不清楚 JPMS 是否会成功,因此您最好的选择可能是不将代码模块化。
请参阅 TL; DR 部分以获得模块名称提案的摘要。欢迎提出反馈和问题。
为了清晰起见,我个人的兴趣是确保 Java 成功,在我看来这需要一致的命名。