模块化是 Java 9 一个非常重要的特性,终于有时间整理一下这方面的内容了。
模块化是软件工程中非常重要的一个概念。把独立的功能封装成模块,并提供接口供外部使用是我们在开发中努力实现的目标。模块化有很多好处:
- 代码内聚,容易维护;
- 能够有效降低复杂度;
- 能提供更好的伸缩性和扩展性。
Java 9 的 Jigsaw 项目致力于为 Java 9 带来平台级的模块化系统。这个项目从 2008 年就已经开始,由于很多原因一直没有发布。现在,Jigsaw 涵盖了 6 个 JEP,终于作为 Java 9 的一部分发布出来。
从整体到模块
Java 一直坚持向后兼容,每更新一个版本,都变得更大更复杂。在过去的 JDK 版本中,没有强制要求模块化,使得 Java 平台和 JDK 越来越让人纠结。即便只是一个小小的 hello world,也需要加载几乎所有的 API。Jigsaw 项目致力于把 Java 平台和 JDK 分解成更小更有组织的模块。用户可以使用模块来构建软件,并且不需要包含所有的 API。
这项工作并不容易,Oracle 的 Java 平台组首席架构师 Mark Reinhold 曾经说过:
JDK代码库在API和实现级别上都是相互关联的,多年来主要以单片软件系统的方式构建。我们花了相当多的努力消除或至少简化了尽可能多的API和实现依赖关系,以便平台和它的实现可以作为一组相互依赖的模块来呈现,但是一些特别棘手的情况仍然存在。我们希望尽可能多地保持与先前版本的兼容性,特别是对于现有的基于类路径的应用程序,同时在可能的情况下,对于由模块组成的应用程序也是如此。
所有 Java 9 模块都在 $JAVA_HOME/jmods 目录下面。每个模块都具有暴露给其他模块的接口。这些模块互相依赖,并通过导出的包进行交互。 开发人员可以编译、打包、部署和执行仅由所选模块组成的应用程序,而无需其他任何操作。
如何使用模块
下面我们把自己的 Java 代码也进行模块化。新建两个简单的模块:
- com.timeteller.clock:包含SpeakingClock的模块,用于将当前时间打印到stdout的简单类;
- com.timeteller.main:使用com.timeteller.clock模块提供的功能的模块。
整个项目结构如下所示:
├── com.timeteller.clock
│ ├── com
│ │ └── timeteller
│ │ └── clock
│ │ └── SpeakingClock.java
│ └── module-info.java
└── com.timeteller.main
├── com
│ └── timeteller
│ └── main
│ └── Main.java
└── module-info.java
在 main 函数中调用方法与之前并无多少不同。
public static void main (String[] args) {
SpeakingClock clock = new SpeakingClock();
clock.tellTheTime(); // displays the time to stdout.
}
Java 9 平台级模块化系统最重要的部分在 module-info.java 中,这个文件定义了模块和元数据。内容如下所示。一个文件定义了 com.timeteller.clock 模块,并导出了 com.timeteller.clock 包;另一个文件定义了 com.timeteller.main 模块,并引用了 com.timeteller.clock 模块。
module com.timeteller.clock {
exports com.timeteller.clock;
}
module com.timeteller.main {
requires com.timeteller.clock;
}
module-info.java 的配置比较简单,可以看到:
- 模块描述符文件放置在模块的根文件夹中;
- 每个模块都有一个唯一的名称;
- 模块描述符定义从模块导出的包以及它们需要哪些模块;
模块中的包如果没有导出,它的作用域就仅限于当前模块中,其他模块无法使用。这一特性使得 Java 9 中的 public 含义有所变化,模块中声明为 public 的类不再是可以随意访问的,只有导出以后才能从模块外访问到。基于这个特性,可以有效地隐藏模块内的 API。
编译 Java 程序
编译时可以这样打包:
$ mkdir -p jars
$ mkdir -p build/classes/com.timeteller.clock
$ javac -d build/classes/com.timeteller.clock/ com.timeteller.clock/module-info.java com.timeteller.clock/com/timeteller/clock/SpeakingClock.java
$ jar -c -f jars/clock.jar -C build/classes/com.timeteller.clock/ .
$ mkdir -p build/classes/com.timeteller.main
$ javac -d build/classes/com.timeteller.main/ --module-path=jars com.timeteller.main/module-info.java com.timeteller.main/com/timeteller/main/Main.java
$ jar -c -f jars/timeTeller.jar --main-class com.timeteller.main.Main -C build/classes/com.timeteller.main .
我们必须告诉编译器模块的路径(modulepath),编译器才能准确编译。模块路径的概念与 classpath 的概念非常相似。当我们构建 com.timeteller.main 模块时,需要在它的模块路径上放一个带有 com.timeteller.clock 的 jar。否则编译器找不到必需的代码并会导致编译错误:
$ javac -d build/classes/com.timeteller.main/ com.timeteller.main/module-info.java com.timeteller.main/com/timeteller/main/Main.java
com.timeteller.main/module-info.java:2: error: module not found: com.timeteller.clock
requires com.timeteller.clock;
这里编译后创建的 jar 也是模块化,模块的 jar 与常规的 jar 基本一样,除了里面包括 module-info.class 文件。Java 9 即将到来,让我们一起期待吧。
分享学习笔记和技术总结,内容涉及 Java 进阶、架构设计、前沿技术、算法与数据结构、数据库、中间件等多个领域。关注作者第一时间获取最新内容,公众号同名(阅读体验更佳)。