文 by / 林本托
Tips
做一个终身学习的人。
在这个章节中,主要介绍以下内容:
- 如何编写模块化的Java程序
- 如何编译模块化程序
- 如何将模块的项目打包成模块化的JAR文件
- 如何运行模块化程序
在本章中,将介绍如何使用模块 —— 从编写源代码到编译,打包和运行程序。 本章分为两部分。 第一部分显示使用命令行编写和运行模块程序的所有步骤。 第二部分使用NetBeans IDE重复相同的步骤。
到目前,NetBeans IDE仍在开发中,并且不支持所有JDK 9功能。 例如,目前需要NetBeans为创建的每个模块创建一个新的Java项目。 在最终版本中,NetBeans将允许在一个Java项目中拥有多个模块。 当使用命令提示符时,会更具体的JDK 9选项。
本章介绍的程序非常简单。 当程序运行时,它打印一个消息和主类所属模块的名称。
一. 使用命令提示符
以下小节介绍使用命令提示符创建和运行第一个模块的步骤。
1. 设置目录
将使用以下目录层次结构来编写,编译,打包和运行源代码:
C:Java9Revealed
| --> C:Java9Revealedlib
| --> C:Java9Revealedmods
| --> C:Java9Revealedsrc
| --> C:Java9Revealedsrccom.jdojo.intro
这些目录在Windows系统上设置的。 在非Windows操作系统上,你也可以设置类似的目录层次结构。 C:Java9Revealed是顶层目录,它包含三个子目录:lib,mods和src。
src目录用于保存源代码,其中包含一个com.jdojo.intro的子目录,并创建一个同名的com.jdojo.intro的模块,并将其源代码保存在这个子目录下。 在这种情况下,是否有必要将子目录命名为com.jdojo.intro? 答案是不。 子目录可以是不同的名字,或者可以将源直接存储在src目录中,而不需要com.jdojo.intro子目录。 但是,最好将目录命名为与模块名称相同的模块的源代码。 如果遵循这个命名约定,Java编译器将有一些选项可一次编译多个模块的源代码。
使用mods目录将已编译的代码保存在展开的目录层次结构中。 如果需要,可以使用此目录中的代码运行应用程序。
在编译源代码之后,将其打包成一个模块化的JAR并将其存储在lib目录中。 可以使用模块化JAR来运行程序,也可以将模块JAR提供给可以运行程序的其他开发人员。
在本节的剩余部分会使用一个目录(如src或srccom.jdojo.intro)的相对路径。 这些相对路径是相对于C:Java9Revealed目录。 例如,src表示C:Java9Revealedsrc。 如果使用非Windows操作系统或其他目录层次结构,请进行适当的调整。
2. 编写源代码
你可以选择自己喜欢的文本编辑器(例如Windows上的记事本)来编写源代码。 首先创建一个名为com.jdojo.intro的模块。下面是模块声明的代码。
// module-info.java
module com.jdojo.intro {
// No module statements
}
模块声明很简单。 它不包含模块语句。 将其保存在srccom.jdojo.intro目录下名为module-info.java的文件中。
然后创建一个名为Welcome的类,它将保存在com.jdojo.intro包中。 请注意,包的名字与模块具有相同的名称。 但必须保持模块和包名称相同吗? 答案是不。 也可以选择所需的任何其他包名称。 该类将具有一个主方法public status void main(String [] args)
。 该方法将作为应用程序的入口点。 在此方法内打印消息。
要打印模块的名称,Welcome 类是它的一个成员。 JDK 9在java.lang包中添加了一个名为Module的类。 Module类的一个实例代表一个模块。 JDK 9中的每个Java类型都是模块的成员,甚至是int,long和char等原始类型。 所有原始类型都是java.base模块的成员。 JDK 9中的Class类有一个名为getModule()
的新方法,它返回该类作为其成员的模块引用。 以下代码打印了Welcome类的模块名称。
Class<Welcome> cls = Welcome.class;
Module mod = cls.getModule();
String moduleName = mod.getName();
System.out.format("Module Name: %s%n", moduleName);
Tips
所有原始数据类型都是java.base模块的成员。 可以使用int.class.getModule()
获取int基本数据类型的模块的引用。
下面的代码包含是Welcome类中代码。并保存在名为Welcome.java的文件中,目录是comjdojointro,它是srccom.jdojo.intro目录的子目录。 此时,源代码文件的路径将如下所示:
- C:Java9Revealedsrccom.jdojo.intromodule-info.java
- C:Java9Revealedsrccom.jdojo.introcomjdojointroWelcome.java
// Welcome.java
package com.jdojo.intro;
public class Welcome {
public static void main(String[] args) {
System.out.println("Welcome to the Module System.");
// Print the module name of the Welcome class
Class<Welcome> cls = Welcome.class;
Module mod = cls.getModule();
String moduleName = mod.getName();
System.out.format("Module Name: %s%n", moduleName);
}
}
3. 编译源代码
使用Java编译器javac命令来编译源代码并将编译的代码保存在C: java9Revealedmods目录下。 javac命令位于JDK_HOMEin目录中。 以下命令编译源代码。 命令输入是一行,而不是三行:
C:Java9Revealed>javac -d mods --module-source-path src
srccom.jdojo.intromodule-info.java
srccom.jdojo.introcomjdojointroWelcome.java
注意,运行此命令时,C:Java9Revealed是当前目录。 -d mods
选项告诉Java编译器将所有编译的类文件保存到mods目录下。 注意,正在从C:java9revealed目录运行命令,因此命令中的mods目录表示C:Java9Revealedmods目录。 如果需要,可以用-d C:Java9Revealedmods
替换此选项。
第二个选项--module-source-path src
指定src目录的子目录包含多个模块的源代码,其中每个子目录名称与包含源代码的子目录的模块名称相同 。 这个选项有一些含义:
- 在src目录下,必须将模块的源文件保存在子目录下,该目录必须与模块命名相同。
- Java编译器将镜像src目录下的目录结构,同时将生成的类文件保存在mods目录中。 也就是说,com.jdojo.intro模块的所有生成的类文件将保存在包层次结构的modscom.jdojo.intro目录中。
- 如果不指定此选项,生成的类文件将直接放在mods目录下。
javac命令的最后两个参数是源文件 —— 一个是模块声明,一个Welcome类。 如果javac
命令成功运行,则在C:Java9Revealedmodscom.jdojo.intro目录下生成以下两个类文件:
- module-info.class
- comjdojointroWelcome.class
你完成了源代码的编译。
以下命令使用JDK 9之前存在的样式来编译com.jdojo.intro模块的源代码。它仅使用-d
选项,该选项指定放置编译的类文件的位置。
C:Java9Revealed>javac -d modscom.jdojo.intro srccom.jdojo.intromodule-info.java srccom.jdojo.introcomjdojointroWelcome.java
第二个命令的输出与上一个命令的输出是相同的。 但是,如果要在一个命令中编译多个模块的源代码,并将编译的代码放在特定于模块的目录中,则不起作用。
使用javac
的--module-version
选项,可以指定正在编译的模块的版本。 模块版本保存在module-info.class文件中。 以下命令将生成与上一个命令相同的一组编译文件,还在module-info.class文件中保存了1.0作为模块版本:
C:Java9Revealed>javac -d modscom.jdojo.intro
--module-version 1.0
srccom.jdojo.intromodule-info.java
srccom.jdojo.introcomjdojointroWelcome.java
如何确认javac
命令在module-info.class文件中保存了模块版本? 您可以使用javap
命令来解析Java类文件。 如果指定了一个module-info.class文件的路径,那么javap
命令会在模块名称之后打印模块的定义,其中包含模块的版本(如果存在)。 如果模块版本存在,则打印的模块名称的格式为moduleName @ moduleVersion
。 运行以下命令以验证上一个命令记录的模块名称:
C:Java9Revealed>javap modscom.jdojo.intromodule-info.class
Compiled from "module-info.java"
module com.jdojo.intro@1.0 {
requires java.base;
}
在JDK 9中增强了jar工具。它允许在创建模块化JAR时指定模块版本。
如果要编译多个模块,则需要将每个源文件指定为javac命令的参数。 这里提供一个Windows和UNIX的快捷命令来一次性编译所有的模块。 在Windows中的一行中使用以下命令:
C:Java9Revealed>FOR /F "tokens=1 delims=" %A in ('dir src*.java /S /B') do javac -d mods --module-source-path src %A
该命令循环遍历src目录下的所有".java"文件,并一次编译一个Java文件。
如果将命令保存在批处理文件中并运行批处理文件来编译所有源文件,则需要将%A
替换为%%A
。
该命令的UNIX系统中等价于如下命令:
$ javac -d mods --module-source-path src $(find src -name "*.java")
4. 打包模块代码
我们将模块的编译代码打包成一个模块化的JAR。 需要使用位于JDK_HOMEin目录中的jar工具。注意, 该命令在一行中输入,命令的最后一部分是一个点,表示当前目录。
C:Java9Revealed>jar --create
--file lib/com.jdojo.intro-1.0.jar
--main-class com.jdojo.intro.Welcome
--module-version 1.0
-C mods/com.jdojo.intro .
这个命令有如下选项:
--create
选项表示要创建一个新的模块化JAR。--file
选项用于指定新的模块化JAR的位置和名称。将新的模块化JAR保存在lib目录中,其名称将为com.jdojo.intro-1.0.jar。将模块化JAR的版本指定为1.0。--main-class
选项指定public static void main(String[])
方法作为应用程序入口。当您指定此选项时,jar工具将在module-info.class文件中添加一个属性,其值是指定类的名称。 jar工具还使用此选项将Main-Class属性添加到MANIFEST.MF文件中。--module-version
选项将模块的版本指定为1.0。 jar工具将把这些信息记录在module-info.class文件的属性中。请注意,将模块版本指定为1.0不会影响模块化JAR的名称。包含1.0以指示其文件名的版本。该模块的实际版本由此选项指定。-C
选项用于指定执行jar命令时将用作设置当前目录。将modscom.jdojo.intro目录指定为jar工具的当前目录。这将使jar工具从该目录中读取所有要包含在模块化JAR中的文件。- 命令的最后一部分是一个点(.),这意味着jar工具需要包括当前目录modscom.jdojo.intro下所有文件和目录。请注意,这个参数和
-C
选项一起使用。如果不提供-C
选项,则该点将被解释为C:Java9Revealed目录,因为该目录是当前命令行运行的目录。
当命令成功运行,它创建以下文件:
C:Java9Revealedlibcom.jdojo.intro-1.0.jar
要确保你的模块化JAR包含com.jdojo.intro模块的定义,请运行以下命令。
C:Java9Revealed>java --module-path lib --list-modules com.jdojo.intro
该命令将模块路径指定为lib目录,这意味着lib目录将用于搜索应用程序模块。 将com.jdojo.intro作为模块名称传递给--list-modules
选项,该选项将打印模块描述以及模块的位置。 如果获得类似于以下内容的输出,则模块化JAR已正确创建:
module com.jdojo.intro@1.0 (file:///C:/Java9Revealed/lib/com.jdojo.intro-1.0.jar)
requires mandated java.base (@9-ea)
contains com.jdojo.intro
5. 运行程序
使用java
命令来运行Java程序。 语法如下:
java --module-path <module-path> --module <module>/<main-class>
这里,--module
选项指定要与其主类一起运行的模块。 如果您的模块化JAR包含主属性,则需要指定
Tips
可以分别使用-module-path
和-module
选项的简写版本-p
和-m
。
以下命令在com.jdojo.intro模块中运行com.jdojo.intro.Welcome类。 当前的目录是C: Java9Revealed,并且模块化的JAR位于C: java9Revealedlibcom.jdojo.intro-1.0.jar。
C:Java9Revealed>java --module-path lib
--module com.jdojo.intro/com.jdojo.intro.Welcome
Welcome to the Module System.
Module Name: com.jdojo.intro
输出表示程序已正确执行。 如果在模块代码打包到模块化JAR中时指定主类,则可以从命令中省略主类名称。 我们已经将com.jdojo.intro.Welcome类指定为此模块的主类,因此以下命令的作用与上一个相同:
C:Java9Revealed>java --module-path lib --module com.jdojo.intro
Welcome to the Module System.
Module Name: com.jdojo.intro
还可以指定包含模块代码的目录作为模块路径。 已将模块代码编译到mods目录中。 以下命令的工作原理相同:
C:Java9Revealed>java --module-path mods
--module com.jdojo.intro/com.jdojo.intro.Welcome
Welcome to the Module System.
Module Name: com.jdojo.intro
我们尝试从mods目录运行模块,只使用模块名称:
C:Java9Revealed>java --module-path mods --module com.jdojo.intro
module com.jdojo.intro does not have a MainClass attribute, use -m <module>/<main-class>
收到一个错误。 错误消息指示在modscom.jdojo.intro目录中找到的module-info.class不包含主类名称。当声明模块时,不能在模块声明中指定主方法或版本。 编译模块时,只能指定模块版本。 使用jar工具打包时,可以指定模块的主类及其版本。 libcom.jdojo.intro-1.0.jar中的module-info.class文件包含主类名,而modscom.jdojo.intro目录中的module-info.class文件则不包含。 如果要运行其编译代码位于分解目录中的模块,则必须指定主类名称以及模块名称。
JDK还提供了-jar
选项来从JAR文件运行主类。 我们使用以下命令运行此模块:
C:Java9Revealed>java -jar libcom.jdojo.intro-1.0.jar
Welcome to the Module System.
Module Name: null
看来只有输出中的第一行是正确的,第二行是不正确的。 它找到了main()
方法中执行了代码。 它正确打印消息,但模块名称为空。
需要了解JDK 9中java命令的行为。-jar
选项存在于JDK 9之前。在JDK 9中,类型作为模块的一部分,可以通过模块路径或类路径加载。如果通过类路径加载类型,则该类型成为未命名模块的成员。该类型从其原始模块中失去其成员资格,即使该类型是从模块化JAR加载的。实际上,如果一个模块化的JAR放置在类路径上,那么它被视为一个JAR(而不是一个模块化的JAR),忽略它的module-info.class文件。每个应用程序类加载器都有一个未命名的模块。类加载器从类路径加载的所有类型都是该类加载器的未命名模块的成员。一个未命名的模块也被表示为一个Module类的实例,该类的getName()
方法返回null。
在上一个命令中,模块化JAR com.jdojo.intro-1.0.jar被视为一个JAR,并在其中定义了所有类型 ,被加载为类加载器的未命名模块的一部分。这就是为什么在输出中将模块名称设为null的原因。
java
命令如何找到主类名?为jar工具指定主类名时,该工具将主类名称存储在两个位置:
- module-info.class
- META-INF/MANIFEST.MF
该命令从META-INF/MANIFEST.MF文件读取主类的名称。
还可以使用--class-path
选项的java
命令来运行Welcome类。 可以将libcom.jdojo.intro-1.0.jar模块放置在类路径上,在这种情况下,它将被视为JAR,Welcome类将被加载到应用程序类加载器的未命名模块中。 就像在JDK 9之前运行类一样。执行命令如下:
C:Java9Revealed>java --class-path libcom.jdojo.intro-1.0.jar
com.jdojo.intro.Welcome
Welcome to the Module System.
Module Name: null
二. 使用NetBeans IDE
如果你使用命令提示符按照上一节创建第一个模块,则本部分将更容易掌握。 在本节中,将使用NetBeans IDE完成创建第一个模块的步骤。 有关如何安装支持JDK 9开发的NetBeans IDE,请参阅第1章。 从现在开始,我将使用NetBeans来编写,编程,编译,打包和运行所有程序。
1. 配置IDE
启动NetBeans IDE。 如果首次打开IDE,将显示一个名为起始页的窗口, 如果不希望再次显示,可以取消选中标签为“启动时显示”的复选框,该复选框位于窗口的右上角。 可以通过单击窗口标题中的X来关闭起始页。
选择“工具”➤“Java平台”,显示“Java 平台管理器”对话框,如果之前配置过JDK 1.8,则会显示在“平台”列表中。
如果在“平台”列表中看到JDK 9,则你的IDE已配置为使用JDK 9,单击“关闭”按钮关闭对话框。 如果在“平台”列表中没有看到JDK 9,请单击“添加平台”按钮打开“添加Java平台”对话框,选择Java Standard Edition单选按钮。 单击下一步。
接下来选择JDK 9的安装目录:
然后点击“完成”按钮。
最后返回“Java 平台管理”对话框,JDK 9 已经显示在其中。
2. 创建Java工程
选择“文件” ➤ “新建项目”,弹出对话框,选中“Java 应用程序”,然后下一步。
在接下来的窗口中,输入如下信息,你可以根据你自己的实际需要做出不同的调整。
项目名称:com.jdojo.intro
项目位置: C:Java9Revealed
取消下面两个复选框的。
点击“结束”按钮,Java工程创建完成。
创建Java项目时,NetBeans会创建一组标准的目录。 已在C: Java9Revealed目录中创建了com.jdojo.intro NetBeans项目。 NetBeans创建子目录来存储源文件,编译类文件和模块化JAR。 它将为项目本身创建安装目录和文件。 创建以下子目录来存储源代码,编译代码和模块化JAR。
C:Java9Revealed
com.jdojo.intro
build
classes
dist
src
com.jdojo.intro目录保存此项目的所有类型的文件。 它是以NetBeans项目名称命名的。 src目录用于保存所有源代码。 build目录保存所有生成和编译的代码。 项目的所有编译代码保存在buildclasses目录下。 dist目录存储模块化JAR。 请注意,当类添加到项目时,build和dist目录是由NetBeans创建的。
3. 设置工程属性
当前项目仍然设置为使用JDK 1.8。 需要将其更改为使用JDK 9。在项目上右键,弹出对话框。
再设置源/二进制格式:
最后点击“确定”按钮完整工程的配置。
4. 添加模块声明
在本节中,介绍如何在NetBeans项目中定义名为com.jdojo.intro的模块。 要添加模块定义,需要module-info.java的文件添加到项目中。 右键单击项目,从菜单中选择新建, 如果看到"Java Module Info"菜单项,请选择该项。 否则,选择其他。
单击下一步按钮,显示“新建Java模块信息”对话框。 单击完成按钮完成模块的定义。 将包含模块声明的module-info.java文件添加到源代码目录的根目录下。
默认情况下,NetBeans提供的模块名称与项目名称相同不过删除了“.”,名称的每个部分的初始字母现在是大写。com.jdojo.intro作为项目名称,这就是为什么module-info.java文件中的模块名称是ComJdojoIntro的原因。 现在将模块名称更改为com.jdojo.intro。
module com.jdojo.intro {
}
5. 查看模块图
NetBeans IDE允许查看模块图。 在编辑器中打开模块的module-info.java文件,并在编辑器中选择“图形”选项卡以查看模块图。
可以放大和缩小模块图,更改其布局,并将其另存为图像。 在图区域中右键单击这些图形相关选项。 可以在图中选择一个节点,仅查看以节点结尾或从节点结尾的依赖关系。 还可以通过移动节点来重新排列模块图。
6. 编写源代码
在本节中,向com.jdojo.intro项目添加一个Welcome类。 该类保存在com.jdojo.intro包中。 右键单击项目,在弹出的菜单中,选择新建➤Java类,打开“新建Java类”对话框。 输入Welcome作为类名,com.jdojo.intro作为包。 单击完成按钮关闭对话框。
用下面的代码替换掉自动生成的代码。
package com.jdojo.intro;
public class Welcome {
public static void main(String[] args) {
System.out.println("Welcome to the Module System.");
// Print the module name of the Welcome class
Class<Welcome> cls = Welcome.class;
Module mod = cls.getModule();
String moduleName = mod.getName();
System.out.format("Module Name: %s%n", moduleName);
}
}
7. 编译源代码
使用NetBeans IDE时,Java源文件将在保存时自动编译。 也可以通过取消选择“保存时编译”复选框,在“项目属性”页面上关闭项目的“保存编译”功能默认情况下,此复选框被选中。
如果关闭“项目保存时编译”功能,则需要通过构建项目手动编译源文件。 可以选择“运行”➤“构建项目”或按F11构建项目。 所以还是建议打开保存时自动编译的功能。
8. 打包模块代码
需要构建项目,为你的模块创建一个模块化JAR。 选择“运行”➤“构建项目”,或按F11构建项目。 模块化的JAR是在
目前,NetBeans不支持在模块化JAR中添加主类名称和模块版本名称。 可以使用jar命令行工具来更新模块化JAR中的模块信息。 使用--update
选项,如下所示,要写在一行。
C:Java9Revealed>jar --update
--file com.jdojo.introdistcom.jdojo.intro.jar
--module-version 1.0
--main-class com.jdojo.intro.Welcome
可以使用以下命令验证com.jdojo.intro的模块化JAR是否正确更新。 应该得到类似的输出:
C:Java9Revealed>java --module-path com.jdojo.introdist
--list-modules com.jdojo.intro
module com.jdojo.intro@1.0 (file:///C:/Java9Revealed/com.jdojo.intro/dist/com.jdojo.intro.jar)
requires mandated java.base (@9-ea)
contains com.jdojo.intro
NetBeans IDE的最终版本将与JDK 9相同时间发布,到那时就允许通过IDE添加这些属性。
9. 运行程序
选择"运行"➤"运行项目"或按F6运行程序。
如果运行类,右键单击NetBeans IDE中“项目”选项卡中包含main()
方法的源文件(.java文件),然后选择运行文件或选择文件,然后按Shift + F6。 Welcome类运行后将打印信息显示在输出面板中。
三. 总结
使用模块开发Java应用程序不会改变Java类型被组织成包的方式。 模块的源代码在包层次结构的根目录下包含一个module-info.java文件。 也就是说,module-info.java文件放在未命名的包中。 它包含模块声明。
JDK 9中已经增强了javac编译器,jar工具和java启动器以与模块配合使用。 javac编译器接受新的选项,例如用于定位应用程序模块的--module-path
,找到模块源代码的--module-source-path
,以及--module-version
来指定正在编译的模块的版本。 该jar工具允许分别使用--main-class
和-module-version
选项为模块化JAR指定主类和模块版本。 java启动器可以在类路径模式,模块模式或混合模式下运行。 要在模块中运行类,需要使用--module-path
选项指定模块路径。 需要使用--module
选项指定主类名称。 主类以main()
方法的类的完全限定名称,作为应用程序的执行入口。
在JDK 9中,每个类型都属于一个模块。 如果从类路径加载类型,它属于加载它的类加载器的未命名模块。 在JDK 9中,每个类加载器维护一个未命名的模块,其成员都是该类加载器从类路径加载的所有类型。 从模块路径加载的类型属于定义的模块。
Module类的一个实例在运行时表示一个模块。 Module类在java.lang包中。 使用Module类在运行时了解有关模块的所有内容。 Class类在JDK 9中得到了增强。它的getModule()
方法返回一个表示此类成员的模块的模块实例。 Module类包含一个getName()
方法,它以String形式返回模块的名称; 对于未命名的模块,该方法返回null。
NetBeans IDE正在更新,以支持JDK 9和开发模块化Java应用程序。 目前,NetBeans允许创建模块,编译,将其打包成模块化JAR,并从IDE中运行它们。 需要为模块创建一个单独的Java项目。 其最终版本将允许在一个Java项目中拥有多个模块。 支持添加module-info.java文件。 NetBeans 具有非常酷的功能,可查看和保存模块图。