24段魔尺拼图指南
Jigsaw项目将把模块化引入Java平台,根据原始计划,它将在12月10日完成功能。 所以我们在这里,但拼图在哪里?
在过去的六个月中肯定发生了很多事情: 原型问世 ,内部API的迫在眉睫的删除引起了很大的骚动 , 邮件列表中充斥着有关项目设计决策的重要讨论 ,而JavaOne看到了一系列很棒的介绍性演讲 。拼图团队。 然后Java 9因拼图而延迟了半年 。
但是,让我们暂时忽略所有这些,仅关注代码。 在本文中,我们将使用一个现有的演示应用程序并将其与Java 9进行模块化。如果要继续学习,请转到GitHub ,在该处可以找到所有代码。 设置说明对于使脚本在Java 9中运行非常重要。为简便起见,我从本文的所有程序包,模块和文件夹名称中删除了前缀org.codefx.demo
。
拼图之前的应用
即使我竭尽全力不理会整个圣诞节,但让演示程序保持本季精神似乎是审慎的做法。 因此,它为出现日历建模:
- 有一个日历,其中包含24个日历表。
- 每张纸都知道它的月份,并包含一个惊喜。
- 即将到圣诞节的死亡游行象征着将床单(以及惊喜)印刷到控制台上。
当然,需要首先创建日历。 它可以自己做到这一点,但它需要一种创造惊喜的方法。 为此,它得到了一个惊喜工厂清单。 main
方法如下所示:
public static void main(String[] args) {List<SurpriseFactory> surpriseFactories = Arrays.asList(new ChocolateFactory(),new QuoteFactory());Calendar calendar =Calendar.createWithSurprises(surpriseFactories);System.out.println(calendar.asText());
}
该项目的初始状态绝不是拼图之前最好的。 相反,这是一个简单的起点。 它由一个包含所有必需类型的模块(从抽象的意义上讲,不是Jigsaw解释)组成:
- “惊喜API” –
Surprise
和SurpriseFactory
(均为接口) - “日历API” –
Calendar
和CalendarSheet
用于创建日历 - 惊喜–几个
Surprise
和SurpriseFactory
实现 - Main –连接并运行整个过程。
编译和运行很简单(Java 8的命令):
# compile
javac -d classes/advent ${source files}
# package
jar -cfm jars/advent.jar ${manifest and compiled class files}
# run
java -jar jars/advent.jar
进入拼图土地
下一步虽小,但很重要。 它不会更改代码或其组织,只会将其移至Jigsaw模块中。
模组
那么什么是模块? 引用强烈推荐的模块系统状态 :
模块是命名的,自描述的代码和数据集合。 它的代码被组织为一组包含类型(即Java类和接口)的软件包。 其数据包括资源和其他种类的静态信息。
为了控制其代码如何引用其他模块中的类型,模块声明其需要哪些其他模块才能进行编译和运行。 为了控制其他模块中的代码如何引用其包中的类型,模块声明要导出的包中的哪个。
因此,与JAR相比,模块具有JVM可以识别的名称,声明其依赖于其他模块,并定义哪些包是其公共API的一部分。
名称
模块的名称可以是任意的。 但是为了确保唯一性,建议坚持使用包的反向URL命名模式。 因此,虽然这不是必需的,但通常意味着模块名称是它包含的软件包的前缀。
依存关系
一个模块列出了要编译和运行的其他模块。 对于应用程序和库模块而言,这都是正确的,但对于JDK本身中的模块而言,也是如此,该模块被分成了约80个(请使用java -listmods
)。
再次从设计概述中:
当一个模块直接依赖于模块图中的另一个模块时,第一个模块中的代码将能够引用第二个模块中的类型。 因此,我们说第一模块读取第二模块,或者等效地,第二模块可被第一模块读取 。
[…]
模块系统确保每个依赖关系都由另一个模块精确地满足,没有两个模块互相读取,每个模块最多读取一个定义给定程序包的模块,并且定义同名程序包的模块不会互相干扰。
当违反任何属性时,模块系统将拒绝编译或启动代码。 这是对脆弱类路径的巨大改进,在脆弱类路径中,例如丢失的JAR仅在运行时才发现,从而使应用程序崩溃。
还值得指出的是,只有模块直接依赖于另一个模块,才能访问另一个模块的类型。 因此,如果A依赖于B ,而B依赖于C ,则除非明确要求A ,否则A无法访问C。
出口产品
一个模块列出了它导出的软件包。 这些包中的公共类型只能从模块外部访问。
这意味着public
不再是真正的公众。 非导出包中的公共类型与导出包中的非公共类型一样,对外界隐藏。 这比当今的私有包类型更加隐蔽,因为模块系统甚至不允许反射访问它们。 由于拼图是当前实现的,命令行标志是解决此问题的唯一方法。
实作
为了能够创建模块,项目在其根源目录中需要一个module-info.java
:
module advent {// no imports or exports
}
等等,我不是说我们也必须声明对JDK模块的依赖吗? 那么,为什么我们在这里没有提到什么呢? 所有Java代码都需要Object
,并且该类以及演示使用的其他少数几个类也是模块java.base
一部分。 因此,实际上每个 Java模块都依赖于java.base
,这导致Jigsaw团队决定自动要求它。 因此,我们不必明确提及它。
最大的变化是要编译和运行的脚本(Java 9的命令):
# compile (include module-info.java)
javac -d classes/advent ${source files}
# package (add module-info.class and specify main class)
jar -c \--file=mods/advent.jar \--main-class=advent.Main \${compiled class files}
# run (specify a module path and simply name to module to run)
java -mp mods -m advent
我们可以看到编译几乎相同–我们只需要在类列表中包括新的module-info.java
。
jar命令将创建所谓的模块化JAR,即包含模块的JAR。 与之前不同,我们不再需要任何清单,而是可以直接指定主类。 注意如何在目录mods
创建JAR。
完全不同的是启动应用程序的方式。 这个想法是要告诉Java在哪里可以找到应用程序模块(使用-mp mods
,这称为模块路径 ),以及我们想启动哪个模块(使用-m advent
)。
分成模块
现在是时候真正地了解Jigsaw并将其拆分为单独的模块了。
虚构理由
“惊喜API”(即Surprise
和SurpriseFactory
取得了巨大的成功,我们希望将其与整体分离。
创造惊喜的工厂非常活跃。 这里要做很多工作,它们经常更改,并且使用的工厂因版本而异。 因此,我们想隔离它们。
同时,我们计划创建一个大型的圣诞节应用程序,日历仅是其中的一部分。 因此,我们也希望为此提供一个单独的模块。
我们最终得到以下模块:
- 惊喜 –
Surprise
andSurpriseFactory
- 日历 –日历,使用Surprise API
- 工厂 –
SurpriseFactory
实现 - main –原来的应用程序,现在已经镂空到
Main
类
通过查看它们之间的依赖关系,我们可以发现惊喜并不取决于其他模块。 日历和工厂都使用它的类型,因此必须依赖它。 最后, main使用工厂来创建日历,因此它依赖于两者。
实作
第一步是重新组织源代码。 我们将遵循官方快速入门指南建议的目录结构,并将所有模块放在src
的自己的文件夹中:
src- advent.calendar: the "calendar" module- org ...module-info.java- advent.factories: the "factories" module- org ...module-info.java- advent.surprise: the "surprise" module- org ...module-info.java- advent: the "main" module- org ...module-info.java
.gitignore
compileAndRun.sh
LICENSE
README
为了保持可读性,我删节了org
下面的文件夹。 缺少的是软件包以及每个模块的最终源文件。 完整地在GitHub上查看它。
现在,让我们看看这些模块信息必须包含什么以及如何编译和运行应用程序。
没有必要的条款,因为Surprise没有依赖性。 (除了java.base
,它始终是隐式必需的。)它导出包advent.surprise
因为它包含两个类Surprise
和SurpriseFactory
。
因此, module-info.java
如下所示:
module advent.surprise {// requires no other modules// publicly accessible packagesexports advent.surprise;
}
编译和打包与上一节非常相似。 实际上,这甚至更容易,因为意外事件不包含任何主要类别:
# compile
javac -d classes/advent.surprise ${source files}
# package
jar -c --file=mods/advent.surprise.jar ${compiled class files}
日历使用来自Surprise API的类型,因此模块必须依赖Surprise 。 向模块添加requires advent.surprise
即可实现。
该模块的API由Calendar
类组成。 为了使其可公开访问,必须导出包含软件包advent.calendar
。 请注意,同一包专用的CalendarSheet
在模块外部将不可见。
但有一个附加的扭曲:我们刚刚作出Calendar.createWithSurprises(
公布,从惊喜模块暴露类型。 因此,除非读取日历的模块也需要惊讶 ,否则Jigsaw将阻止它们访问这些类型,这将导致编译和运行时错误。 List<SurpriseFactory>
)
将require子句标记为public
可解决此问题。 有了它,任何依赖日历的模块也会让人吃惊 。 这称为隐式可读性 。
最终的模块信息如下所示:
module advent.calendar {// required modulesrequires public advent.surprise;// publicly accessible packagesexports advent.calendar;
}
编译几乎像以前一样,但是当然必须在此反映对惊奇的依赖。 为此,将编译器指向目录mods
就足够了,因为它包含所需的模块:
# compile (point to folder with required modules)
javac -mp mods \-d classes/advent.calendar \${source files}
# package
jar -c \--file=mods/advent.calendar.jar \${compiled class files}
该工厂实现SurpriseFactory
所以这个模块必须依靠惊喜 。 并且由于它们从已发布的方法返回Surprise
实例,因此与上述相同的思路导致了requires public
子句的出现。
可以在包advent.factories
找到工厂,因此必须将其导出。 请注意,在另一个模块中找到的公共类AbstractSurpriseFactory
在此模块外部无法访问。
这样我们得到:
module advent.factories {// required modulesrequires public advent.surprise;// publicly accessible packagesexports advent.factories;
}
编译和打包类似于日历 。
我们的应用程序需要日历和工厂这两个模块进行编译和运行。 它没有要导出的API。
module advent {// required modulesrequires advent.calendar;requires advent.factories;// no exports
}
编译和打包与上一节的单个模块相似,不同之处在于编译器需要知道在哪里寻找所需的模块:
#compile
javac -mp mods \-d classes/advent \${source files}
# package
jar -c \--file=mods/advent.jar \--main-class=advent.Main \${compiled class files}
# run
java -mp mods -m advent
服务
拼图通过实现服务定位器模式实现松散耦合,其中模块系统本身充当定位器。 让我们看看情况如何 。
虚构理由
最近有人读了一篇有关冷松耦合的博客文章。 然后她从上面看我们的代码,抱怨主机和工厂之间的紧密关系。 主为什么还要知道工厂 ?
因为…
public static void main(String[] args) {List<SurpriseFactory> surpriseFactories = Arrays.asList(new ChocolateFactory(),new QuoteFactory());Calendar calendar =Calendar.createWithSurprises(surpriseFactories);System.out.println(calendar.asText());
}
真? 只是为了实例化完美抽象的某些实现( SurpriseFactory
)?
而且我们知道她是对的。 让其他人为我们提供实现将消除直接依赖。 更好的是,如果说中间人能够在模块路径上找到所有实现,则可以通过在启动之前添加或删除模块来轻松配置日历的惊喜。
拼图确实可以做到这一点。 我们可以有一个模块来指定它提供接口的实现。 另一个模块可以表示它使用所述接口,并使用ServiceLocator
查找所有实现。
我们利用这个机会将工厂分割成巧克力,然后报价并最终得到以下模块和依赖项:
- 惊喜 –
Surprise
andSurpriseFactory
- 日历 –日历,使用Surprise API
- 巧克力 –
ChocolateFactory
即服务 - quote –
QuoteFactory
即服务 - 主要 –应用程序; 不再需要单个工厂
实作
第一步是重新组织源代码。 与以前相比,唯一的变化是src/advent.factories
被src/advent.factory.chocolate
和src/advent.factory.quote
。
让我们看一下各个模块。
两者都没有改变。
除某些名称外,两个模块都是相同的。 让我们看一下巧克力,因为它更美味。
与工厂以前一样,该模块requires public
惊喜模块。
更有趣的是其出口。 它提供了SurpriseFactory
的实现,即ChocolateFactory
,其实现如下指定:
provides advent.surprise.SurpriseFactorywith advent.factory.chocolate.ChocolateFactory;
由于此类是其公共API的全部,因此不需要导出其他任何内容。 因此,没有其他出口条款是必要的。
我们最终得到:
module advent.factory.chocolate {// list the required modulesrequires public advent.surprise;// specify which class provides which serviceprovides advent.surprise.SurpriseFactorywith advent.factory.chocolate.ChocolateFactory;
}
编译和打包很简单:
javac -mp mods \-d classes/advent.factory.chocolate \${source files}
jar -c \--file mods/advent.factory.chocolate.jar \${compiled class files}
关于main的最有趣的部分是它如何使用ServiceLocator查找SurpriseFactory的实现。 从其主要方法 :
List surpriseFactories = new ArrayList<>();
ServiceLoader.load(SurpriseFactory.class).forEach(surpriseFactories::add);
我们的应用程序现在仅需要日历,但必须指定它使用SurpriseFactory
。 它没有要导出的API。
module advent {// list the required modulesrequires advent.calendar;// list the used servicesuses advent.surprise.SurpriseFactory;// exports no functionality
}
编译和执行与以前一样。
我们确实可以通过简单地从模块路径中删除工厂模块之一来更改日历最终将包含的惊喜。 整齐!
摘要
就是这样了。 我们已经看到了如何将单片应用程序移动到单个模块中,以及如何将其拆分为多个模块。 我们甚至使用服务定位器将我们的应用程序与服务的具体实现分离开来。 所有这些都在GitHub上,因此请查看更多代码!
但是还有更多要讨论的! 拼图带来了一些不兼容问题,但也解决了许多不兼容问题。 而且我们还没有讨论反射如何与模块系统交互以及如何迁移外部依赖项。
如果您对这些主题感兴趣,请在我的博客上观看Jigsaw标签 ,因为在接下来的几个月中我一定会写关于它们的。
翻译自: https://www.javacodegeeks.com/2015/12/project-jigsaw-hands-guide.html
24段魔尺拼图指南