Java平台模块系统(JPMS)将模块化带入Java和JVM,并改变了我们在大型应用中的编程方式。 为了充分利用它,我们需要很好地了解,第一步是学习基础知识。 在本教程中,我首先向你展示一个简单的Hello World示例,然后我们用Java 9将一个现有的demo程序模块化。我们将创建模块声明(module-info。java),使用模块路径来编译,打包,以及运行程序——先代码,然后再解释,所有你可以直入主题。
教程使用了两个项目,都可以在GitHub上找到:第一个是非常简单的Hello World示例,另一个是ServiceMonitor,这和我一本关于模块系统的书上是用的同一个项目。如果你想仔细看下就把它检出来(check out)。所有的命令(如javac、jar、java)都是指Java 9的变体。
你好,模块化世界
我们从最简单的应用开始,一个打印Hello,Modular World的应用! 下面是这个类:
package org.codefx.demo.jpms;
public class HelloModularWorld {
public static void main(String[] args) {
System.out.println("Hello, modular World!");
}
}
要成为一个模块,它在项目源码的根目录下需要一个module-info.java:
module org.codefx.demo.jpms_hello_world {
// 这个模块只需要基础模块‘java.base’里的类型;
// 因为每一个Java模块都需要'java.base',没必要明确引入它——这里是为了演示做的
requires java.base;
// 此处的导出在这个应用里是没有意义的,再次是为演示做的
exports org.codefx.demo.jpms;
}
按通用的目录结构src/main/java,程序的目录如下:
这些是编译,打包,以及启动的命令:
$ javac
-d target/classes
${source-files}
$ jar --create
--file target/jpms-hello-world.jar
--main-class org.codefx.demo.jpms.HelloModularWorld
-C target/classes .
$ java
--module-path target/jpms-hello-world.jar
--module org.codefx.demo.jpms_hello_world
除了使用一种称为“模块路径”的东西以及可以定义项目的主类(没有清单)外,非常类似于我们在非模块化应用里所做的。 我们看下它是如何工作的。
模块
JPMS的基本构建块是模块(惊喜!)。 像JAR一样,它们是类型和资源的容器; 但与JAR不同,它们有其他特征 - 这些是最基础的:
- 名称,最好是全局唯一
- 依赖其他模块的声明
- 由导出的包组成的清晰定义的API
JDK被分为大约一百个所谓的平台模块。 可以使用java --list-modules列出它们,使用java --describe-module $ {module}查看单个模块。 继续,试用下java.sql或java.logging:
$ java --describe-module java.sql
> java.sql@9
> exports java.sql
> exports javax.sql
> exports javax.transaction.xa
> requires java.logging transitive
> requires java.base mandated
> requires java.xml transitive
> uses java.sql.Driver
模块的属性在模块声明中定义,它在项目根目录下的module-info.java文件里,如下所示:
module ${module-name} {
requires ${module-name};
exports ${package-name};
}
它被编译为module-info.class,称为模块描述符,放在JAR的根目录下。 这个描述符是普通JAR和模块化JAR之间的唯一区别。
我们逐个看模块的三个属性:name,dependencies,exports。
名字(Name)
JAR缺少的最基本的属性是编译器和JVM可以用来识别JAR的名称。 因此,它是模块最突出的特征。我们将有可能,甚至是有义务给每个模块创建一个名字。
命名模块通常是很自然的,因为我们每天使用的大多数工具,无论是IDE,构建工具,甚至发布问题跟踪以及版本控制系统,都已经让我们命名了项目。但是为了让名字在搜索模块名时有意义,明智的选择很重要!
模块系统严重依赖模块的名字。 冲突或演变的名字特别容易引起麻烦,所以重要的是名字是:
- 全局唯一
- 稳定
实现这一目标的最佳方式是已经常用于包名的反域名的命名方案:
module org.codefx.demo.jpms {
}
依赖(Dependencies)
JAR包里缺失的另一件是声明依赖关系的能力,但是使用模块系统,这些时代已经结束了:所有在JDK模块以及第三方库或框架上的依赖关系必须明确。
依赖关系用requires语句声明,语句包括关键字本身以及后面的模块名称。 扫描模块时,JPMS将构建一个模块图,其中模块是节点,而requires语句则变为所谓的可读边界 —— 如果模块org.codefx.demo.jpms requires模块java.base,在运行时org.codefx.demo .jpms读取java.base。
如果模块系统找不到所需正确名称的模块,这意味着如果模块缺失,编译以及启动应用将会失败。 这实现了模块系统其中一个目标——可靠配置,但可以非常严格 —— 检出我关于可选依赖的帖子,看下更宽松的替代方案。
Hello World示例所需的所有类型都可以在JDK模块java.base(所谓的基础模块)里找到。因为它包含了核心的类型如Object,所有的Java代码都需要它,所有它不必明确要求。我在这个例子里仍然这样做,以便给你们演示requires语句:
module org.codefx.demo.jpms {
requires java.base;
}
导出(Exports)
模块列出了它导出的包。 一个模块的代码(例如org.codefx.demo.jpms)访问另一个模块的类型(例如java.base中的String),必须满足以下可访问性规则:
- 访问类型(String)必须是public
- 包含类型(java.lang)的包必须被其模块(java.base)导出
- 访问模块(org.codefx.demo.jpms)必须读取被访问的包(java.base),通常是用requires来实现。
如果在编译或运行时违反了任何这些规则,模块系统会抛出错误。 这意味着public不再是真正公开。 非导出包里的public类型和导出包里的非public类型对外部来说都是不可访问的。 还要注意,反射也失去了超能力。 除非使用命令行标志,否则它受到完全相同的可访问性规则的约束。
我们的示例没有有意义的API,没有外部代码需要访问它,所以我们实际上不需要导出任何东西。这里的导出仅是为了演示:
module org.codefx.demo.jpms_hello_world {
requires java.base;
exports org.codefx.demo.jpms;
}
模块路径(Module Path)
现在已知道如何定义模块及其基本属性。 还有一点不清楚的是我们如何告诉编译器和运行时。 答案是一个与类路径(class path)相似的新概念:
模块路径是一个构件或包含构件目录的列表。 根据操作系统不同,模块路径在基于Unix的操作系统下使用冒号:隔开,在Windows使用分号;隔开。 模块系统用它来定位在平台模块中找不到的所需模块。 javac和java以及其他与模块相关的命令都可以处理它 —— 命令行选项是--module-path和-p。
模块路径上的所有构件都变成模块。 普通的JAR包则变成自动模块。
编译、打包、运行(Compiling, Packaging, Running)
编译工作很像没有模块系统一样:
$ javac
-d target/classes
${source-files}
(你必须用实际的文件替换$ {source-files},但是这是示例,我就不这样做了)。
源文件里一旦有module-info.java,模块系统就会启动。 编译所需模块的所有非JDK依赖项都需要在模块路径里。 对于Hello World示例,没有这样的依赖关系。
使用jar的打包也不变。 唯一的区别是,我们不再需要一个清单来声明应用程序的入口点 —— 我们可以使用--main-class来表示:
$ jar --create
--file target/jpms-hello-world.jar
--main-class org.codefx.demo.jpms.HelloModularWorld
-C target/classes .
最后,启动看起来有点不同。 我们使用模块路径而不是类路径来告诉JPMS哪里可以找到模块。 我们需要做的只是用--module来命名主模块:
$ java
--module-path target/jpms-hello-world.jar
--module org.codefx.demo.jpms_hello_world
就是这样! 我们创建了一个非常简单但仍然是模块化的Hello-World应用程序,成功构建并启动。 现在是时候来看一个稍微不那么简单的例子来看看依赖和导出等机制。