Java 模块化系统初探

jopen 8年前

Java 模块化系统自提出以来经历了很长的时间,直到 2014 年晚些时候才最终以 JSR(JSR-376) 定稿,而且这个部分有可能在 Java 9 中出现。但是一直以来都没有可以使用的原型。9 月 11 日,OpenJDK 发布的早期构建版本终于包含了 Jigsaw 项目。

昨天,我和同事 Paul Bakker 在 JavaZone 上对于 Java 模块化系统进行了讨论。整个讨论都建立在JSR-376 需求文档以及身边一些珍贵的信息上。在年初提出举行这个报告的时候,我们曾深信不疑地认为在这个会上我们能够展示一个原型,但是事情却没有按预想的那样发展。现在的情况是,这个原型将在我们的报告结束之后发布。这也意味着,报告中的一些内容已经有点过时了,但是主要的思想还是很有新意的。如果你对 Java 模块化系统方案一无所知的话,建议你在阅读这篇文章之前先去看一下我们的报告。我们的报告介绍了现在的方案,并进一步与 OSGi 进行了比较。

为什么要使用模块?

什么是模块?我们为什又需要它们?如果希望有一个深入的讨论,请阅读“State of the module system”或者看一下我们的报告。对这块还不是很了解的人来说,这里有Cliff 的注释版本。

我们都知道 Java 有 jar 文件。但是,事实上这些都只是包含一些class(类)的压缩文件,这些 jar 包内部都是一些 package (包)。当你利用一些不同的 jar 包来运行应用程序的时候(复杂一点的程序也适用),你需要把它们放到指定的类路径中。然后默默祈祷。因为没有有效的工具来帮助你知道,你是否已经把应用所需要的 jar 包都放入类路径中了。或者有可能你在不经意间将同样的类文件(在不同的 jar 包中)都放入了类路径中。类路径灾难(类似于 DLL 灾难)是真实存在的。这会导致运行时出现糟糕的状况。同时,在运行时我们也无法得知 jar 中包含哪些类。从 JRE 角度来说只知道有一堆类文件。事实上 jar 包之间是相互依赖的,但目前还不能把这种依赖关系记录到数据文件中去。理想的情况是,你可以隐藏 jar 包中类文件具体的实现,只是提供一些公共的 API 。在 Java 中提出模块化系统就是为了解决这些问题的:

  • 模块成为首先要考虑的部分,它能够分装实现细节并且只暴露需要的接口。
  • 模块准确地描述了他们能够提供的接口,以及他们的需要部分(依赖)。由此,我们可以在开发的过程中弄清和处理依赖关系。

模块系统极大地提升了大型系统的可维护性、可靠性、安全性。至少 JDK 本身还缺少这样的系统。通过这样的模块系统,模块图能够自动地构建。这个图只包括了你的应用程序运行时所须要的模块。

安装 JDK9 预览版

如果你想亲自尝试编写示例代码,你需要安装包含 Jigsaw 原型的 JDK9 早期构建版本。在 OSX 上,你需要解压文档,然后把解压出来的目录移动到 Library/Java/JavaVirtualMachines/ 下。然后你需要设置环境变量,将 JAVA_HOME 环境变量指向 JDK9 的目录。我使用了非常好用的setjdk 脚本,通过它可以在命令窗口中实现 Java 安装的命令切换。你很有可能不愿意使用这个早期构建版本作为你的 Java 安装版本。你可以通过 java -version 来确认安装完成。输出如下面所示:

java version "1.9.0-ea"  Java(TM) SE Runtime Environment (build 1.9.0-ea-jigsaw-nightly-h3337-20150908-b80)  Java HotSpot(TM) 64-Bit Server VM (build 1.9.0-ea-jigsaw-nightly-h3337-20150908-b80, mixed mode)

只要输出中包含 Jigsaw ,你就可以继续了。文章后面的示例代码可以去 https://github.com/sandermak/jigsaw-firstlook 下载。

一个简单的例子

你仍旧可以通过类、jar包以及类路径这样“传统方式”的方式来使用 JDK9 。但是明显地我们想要采用模块的方式。所以我们将创建一个包含两个模块的工程:模块一使用了模块二中的代码。

首先要做的就是,构建我们的工程并把两个模块很好地区分开来。然后,模块中需要以 module-info.java 文件的形式添加元数据。我们的示例构建如下:

src    module1       module-info.java       comtestTestClassModule1.java    module2       module-info.java       commoretestTestClassModule2.java

接着,我们将介绍 package (包)层最顶上的一层(module1、 module2),这部分你在之前已经构建好了。在这些“模块目录”中,可以看到 module-info.java 文件在根目录下。此外请注意,这两个类都是在显示命名的包中的。

请看 TestClassModule1 的代码:

package com.test;    import com.moretest.TestClassModule2;    public class TestClassModule1 {       public static void main(String[] args) {         System.out.println("Hi from " + TestClassModule2.msg());       }    }

看起来很普通对吧?这里并没有涉及模块,而是导入了 TestClassModule2 ,主函数之后会去调用其中的 msg() 方法。

package com.moretest;    public class TestClassModule2 {       public static String msg() {         return "from module 2!";       }    }

到目前为止,module-info.java 还是空的。

对 Java 模块进行编译

现在进行下一步:编译我们的模块,并关联源文件。为了做这项工作,我们将介绍一个新的 javac 编译参数:

javac -modulesourcepath src -d mods $(find src -name '*.java')

使用上面语句时,我们假设命令程序已经处于 src 文件夹的上级目录中了。-modulesourcepath 参数会让 javac 从传统编译模式进入模块模式。-d 标记指出了编译好的模块的输出目录。javac 将以非打包文件的形式输出这些模块。如果我们这之后想以 jars 的形式使用这些模块的话,需要一个单独的步骤。

那么当我们调用上面的 javac 命令行的时候会发生什么那?编译出错了!

src/module1/module-info.java:1: error: expected 'module'  src/module2/module-info.java:1: error: expected 'module'

空的 module-info.java 文件导致了这个错误。所以,一些新的关键字将被引入到这些文件中来,这些都是模块中非常重要的部分。这些关键字的作用域就是 module-info.java 的定义部分。你还可以在 java 的源文件中使用 module 类型的变量。

我们采用了最少的描述信息,并更新了模块描述文件:

module module1 { }

然后是模块2:

module module2{ }

现在,模块已经被准确地命名了,但是还没有包含其它的数据。再次编译会导致新的错误:

src/module1/com/test/TestClassModule1.java:3: error: TestClassModule2 is not visible because package com.moretest is not visible

封装出现了!默认情况下,模块内部的类或者其他类型对外都是隐藏的。这就是 javac 不允许使用 TestClassModule2 的原因,即使它是一个公共的类。如果我们还是使用基于传统类路径的编译的话,一切都可以正常运作。当然我们也可以通过明确地将 TestClassModule2 暴露给外部来解决这个问题。接下来的这些改变对于 module2 中的 module-info.java 来说是必须的:

module module2 {      exports com.moretest;    }

这还不够。如果你将修改后的编译,你会得到同样的错误。那是因为,虽然现在 module2 已经暴露了所需的包(包含所有的公共类型),但是 module1 还没有声明它对 module2 的依赖。我们同样可以改变 module1 的 module-info.java 文件来解决这个问题:

module module1 {       requires module2;    }

通过指定名字的方法可以表示对其它模块的依赖,尽管在这些模块中是以包的形式导出的。这方面还有很多可以说的东西,但是我并不想在初步的介绍中涉及。在做完这一步之后,我们使用 Jigsaw 第一次成功编译了多模块项目。如果你打开 /mods 目录,你能看到编译出来的东西被整齐地划分为两个目录。这就成功了!

运行模块化代码

只是编译的话并没有多大乐趣。我们希望应用程序能够运行起来。幸运的是,JRE 和 JDK 已经在这个原型中支持模块关联。这个应用可以通过指定模块路径的方式来启动,而不是类路径:

java -mp mods -m module1/com.test.TestClassModule1

我们把模块路径指向 mods 文件夹,这个文件就是 javac 编译时写输出模块的地方。而 -m 指出了最初要启动的模块,通过这个模块可以逐步启动其他模块。我们同样添加了在初始化时需要调用的启动类的名字,运行结果如下所示:

Hi from from module 2!

未来

这部分介绍可以让你初步了解可以使用 Java 9 中的模块可以做什么。这部分还是需要更多的探索。就像打包一样:除了jar包,即将会有一种新的形式叫做 jmod 。这个模块化系统同样包括一个服务层,它可以通过接口绑定服务提供者和服务使用者。可以把这个看成反转控制:模块系统担任服务注册管理的角色。还有一个值得期待的地方是,JDK 本身将会如何使用模块化系统进行模块化。这有可能支持一些非常棒的技术,比如创建一个运行时镜像,这个镜像可以只包括 JDK 和你应用所需要的那些模块。好处有:占用更少的空间,对于程序整体的优化可以有更多的选择等等。这些前景都是很光明的。

我接下来将尝试移植一个简单的 OSGi 应用程序(该程序会使用一些模块和服务)到 Java 9 模块系统上。敬请关注!

原文链接: dzone 翻译: ImportNew.com - 闵 大为
译文链接: http://www.importnew.com/16761.html