Java模块
Java模块是一种将Java应用程序和Java软件包打包为Java模块的机制。一个Java模块可以指定它包含的Java包中的哪个对使用该模块的其他Java模块可见。 Java模块还必须指定完成其工作所需的其他Java模块。稍后将在此Java模块教程中对此进行详细说明。
Java模块是Java 9中通过Java平台模块系统(JPMS)的一项新函数。根据我们在何处阅读,Java平台模块系统有时也称为Java拼图或者项目拼图。拼图是开发过程中内部使用的项目名称。后来Jigsaw将名称更改为Java Platform Module System。
Java模块的好处
Java平台模块系统为我们的Java开发人员带来了很多好处。我将在下面列出最大的好处。
通过模块化Java平台进行的较小的应用程序分发
作为Project Jigsaw的一部分,所有Java平台API均已拆分为单独的模块。将所有Java API分成模块的好处是,我们现在可以指定应用程序需要Java平台的哪些模块。知道应用程序需要哪些Java平台模块后,Java可以打包应用程序,仅包括应用程序实际使用的Java平台模块。
在Java 9和Java Platform Module System之前,我们必须将所有Java Platform API与Java应用程序打包在一起,因为没有可靠的官方方法来检查Java应用程序使用了哪些类。由于这些年来Java平台API的规模已经很大,因此应用程序将在其发行版中包含大量Java类,应用程序可能不会使用其中的许多Java类。
未使用的类使应用程序可分发的数量超出了需要。在手机,Raspberry Pi等小型设备上,这可能是个问题。使用Java平台模块系统,我们现在可以仅将应用程序实际使用的Java平台API的模块打包到应用程序中。这将导致较小的应用程序可分配量。
内部包装的封装
Java模块必须明确指出要使用该模块将模块内的哪些Java包导出(可见)到其他Java模块。 Java模块可以包含未导出的Java包。未导出的程序包中的类不能被其他Java模块使用。此类软件包只能在包含它们的Java模块内部使用。
未导出的程序包也称为隐藏程序包或者封装程序包。
缺少模块的启动检测
从Java 9开始,Java应用程序也必须打包为Java模块。因此,应用程序模块指定其使用哪些其他模块(Java API模块或者第三方模块)。因此,Java VM可以在Java VM启动时从应用程序模块检查整个模块依赖关系图,然后进行转发。如果在启动时未找到任何必需的模块,则Java VM将报告缺少的模块并关闭。
在Java 9之前,直到应用程序实际尝试使用缺少的类之前,才会检测到缺少的类(例如,来自丢失的JAR文件)。这将在运行时的某个时间发生,具体取决于应用程序何时尝试使用缺少的类。
与尝试使用缺少的模块/ JAR /类的运行时相比,在应用程序启动时报告丢失的模块是一个很大的优势。
Java模块基础
现在我们知道了什么是Java模块以及Java模块的好处是什么,让我们看一下Java模块的基础知识。
模块包含一个或者多个软件包
Java模块是属于一个或者多个的Java程序包。模块可以是完整的Java应用程序,Java Platform API或者第三方API。
模块命名
必须为Java模块指定一个唯一的名称。例如,有效的模块名称可以是
com.Hyman.mymodule
Java模块名称遵循与Java包相同的命名规则。但是,我们不应在Java 9及更高版本的模块名称(或者包名称,类名称,方法名称,变量名称等)中使用下划线(_),因为Java将来希望将下划线用作保留的标识符。 。
如果可能的话,建议使用与模块中包含的根Java软件包相同的名称来命名Java模块(某些模块可能包含多个根软件包)。
模块根目录
在Java 9之前,应用程序或者API的所有Java类都直接嵌套在根类目录(已添加到类路径)中,或者直接嵌套在JAR文件中。例如,com.Hyman.mymodule的已编译软件包的目录结构如下所示:
com/Hyman/mymodule
以图形方式显示,如下所示:
- com Hyman mymodule
从Java 9开始,模块必须嵌套在与该模块同名的根目录下。在上面的示例中,我们有一个名为com.Hyman.mymodule
的包的目录结构。该Java包将包含在同名的Java模块中(也称为" com.Hyman.mymodule")。
包含在同名Java模块中的上述Java包的目录结构如下所示:
com.Hyman.mymodule/com/Hyman/mymodule
以图形方式显示,如下所示:
- com.Hyman.mymodule com Hyman mymodule
请注意模块根目录名称中的句号(.
)。这些句点必须存在,因为它们是模块名称的一部分!它们不应被解释为子目录路径分隔符!
模块根目录用于Java模块的源文件和已编译的类。这意味着,如果Java项目有一个名为src / main / java
的源根目录,那么我们项目内的每个模块都将在src / main / java
下拥有自己的模块根目录。例如:
src/main/java/com.Hyman.module1 src/main/java/com.Hyman.module2
在Java编译器的输出目录中可以看到相同的目录结构。
每个项目通常只有一个Java模块。在这种情况下,我们仍然需要模块根目录,但是源和编译器输出根目录将仅包含一个模块根目录。
模块描述符(module-info.java)
每个Java模块都需要一个名为" module-info.java"的Java模块描述符,该描述符必须位于相应的模块根目录中。对于模块根目录src / main / java / com.Hyman.mymodule,模块的模块描述符路径为src / main / java / com.Hyman.mymodule / module-info.java。
模块描述符指定模块导出哪些软件包,以及模块需要哪些其他模块。这些细节将在以下各节中说明。这是一个基本的空Java模块描述符的外观:
module com.Hyman.mymodule { }
首先是module
关键字,其后是模块的名称,然后是一组花括号。导出的软件包和所需的模块将在大括号内指定。
还请注意,模块描述符的后缀是.java,但是它在文件名(module-info.java)中使用连字符。 Java类名称中通常不允许使用连字符,但是模块描述符文件名称中必须使用连字符!
导出模块
Java模块必须显式导出该模块中所有可用于其他模块使用该模块访问的包。导出的软件包在模块描述符中声明。这是一个简单的导出声明在模块描述符中的外观:
module com.Hyman.mymodule { exports com.Hyman.mymodule; }
这个例子导出了名为com.Hyman.mymodule的包。
请注意,仅列出的软件包本身被导出。没有导出的软件包的"子软件包"被导出。这意味着,如果mymodule包中包含一个名为util的子包,则不会因为com.Hyman.mymodule的存在而将com.Hyman.mymodule.util包导出。
要也导出子包,必须在模块描述符中显式声明它,如下所示:
module com.Hyman.mymodule { exports com.Hyman.mymodule; exports com.Hyman.mymodule.util; }
我们不必导出父包即可导出子包。以下模块描述符导出语句完全有效:
module com.Hyman.mymodule { exports com.Hyman.mymodule.util; }
此示例仅导出com.Hyman.mymodule.util
包,而不导出com.Hyman.mymodule
包。
模块要求
如果Java模块需要另一个模块来完成其工作,则该另一个模块也必须在模块描述符中指定。这是一个需要声明的Java模块的示例:
module com.Hyman.mymodule { requires javafx.graphics; }
这个示例模块描述符声明它需要名为javafx.graphics
的标准Java模块。
不允许循环依赖
不允许模块之间具有循环依赖关系。换句话说,如果模块A需要模块B,那么模块B也不能同时需要模块A。模块依赖图必须是非循环图。
不允许拆分包
同一Java包只能在运行时由单个Java模块导出。换句话说,我们不能有两个(或者更多)模块同时导出使用中的同一软件包。如果我们这样做,Java VM将在启动时进行投诉。
使两个模块导出相同的程序包有时也称为拆分程序包。拆分包是指包的总内容(类)在多个模块之间拆分。这是不允许的。
编译Java模块
为了编译Java模块,我们需要使用Java SDK随附的javac
命令。请记住,我们需要Java 9或者更高版本才能编译Java模块。
javac -d out --module-source-path src/main/java --module com.Hyman.mymodule
请记住,我们必须在路径(环境变量)上的JDK安装中包含javac
命令,此命令才能起作用。或者,我们可以将上述命令中的javac部分替换为javac命令所在位置的完整路径,如下所示:
"C:\Program Files\Java\jdk-9.0.4\bin\javac" -d out --module-source-path src/main/java --module com.Hyman.mymodule
也可以使用Ant编译Java模块。我们可以在有关如何使用Ant构建Java模块的教程中看到这一点。
当javac编译模块时,它将编译的结果写入到javac命令的-d参数之后指定的目录中。在该目录中,我们将找到一个带有模块名称的目录,在该目录中,我们将找到编译后的类以及名为module-info.class
的module-info.java
模块描述符的编译后版本。
--module-source-path应该指向源根目录,而不是模块根目录。源根目录通常比模块根目录高一级。
--module参数指定要编译的Java模块。在上面的示例中,它是名为" com.Hyman.mymodule"的模块。我们可以通过用逗号分隔模块名称来指定要编译的多个模块。例如:
... --module com.Hyman.mymodule1,com.Hyman.mymodule2
运行Java模块
为了运行Java模块的主类,可以使用java
命令,如下所示:
java --module-path out --module com.Hyman.mymodule/com.Hyman.mymodule.Main
" --module-path"参数指向所有编译模块所在的根目录。请记住,这是模块根目录之上的一级。
--module参数告诉运行哪个模块和主类。在该示例中,模块名称是com.Hyman.mymodule
部分,主类名称是com.Hyman.mymodule.Main
。请注意,模块名称和主类名称之间如何用斜杠(/
)分隔。
构建Java模块JAR文件
我们可以将Java模块打包在标准JAR文件中。我们可以使用Java SDK随附的标准jar
命令来执行此操作。包目录层次结构必须从JAR文件的根开始,就像Java 9之前的JAR文件一样。此外,Java模块JAR文件在JAR文件的根目录中包含模块描述符的编译版本。
这是从已编译的Java模块生成JAR文件所需的jar
命令:
jar -c --file=out-jar/com-Hyman-mymodule.jar -C out/com.Hyman.mymodule .
-c
参数告诉jar
创建一个新的JAR文件。
--file参数告诉输出文件创建的JAR文件的路径。我们希望输出JAR文件位于的任何目录必须已经存在!
-C(大写C)参数告诉jar命令将目录更改为out / com.Hyman.javafx(编译后的模块根目录),然后由于以下原因而包含该目录中的所有内容。 .
参数(表示"当前目录")。
也可以将Java模块与Ant打包在一起。我们可以在有关如何使用Ant构建Java模块的教程中看到这一点。
设置JAR主类
生成模块JAR文件时,我们仍然可以设置JAR主类。我们可以通过提供--main-class
参数来实现。这是设置Java模块JAR文件的主类的示例:
jar -c --file=out-jar/com-Hyman-mymodule.jar --main-class=com.Hyman.mymodule.Main -C out/com.Hyman.mymodule .
现在,我们可以使用快捷方式运行此JAR文件的主类。下一节将说明此快捷方式。
从JAR运行Java模块
将Java模块打包成JAR文件后,就可以像运行普通模块一样运行它。只需在模块路径中包含模块JAR文件即可。这是从Java模块JAR文件运行主类的方式:
java --module-path out-jar -m com.Hyman.mymodule/com.Hyman.mymodule.Main
为了使该命令生效,模块JAR文件必须位于out-jar
目录中。
从具有主类集的JAR运行Java模块
如果Java模块JAR文件设置了一个主类(请参见本教程前面的几节,以了解如何执行此操作),则可以使用较短的命令行来运行Java模块主类。这是从设置了主类的JAR文件中运行Java模块的示例:
java -jar out-jar/com-Hyman-javafx.jar
注意没有设置--module-path参数。这要求Java模块不使用任何第三方模块。否则,我们也应该提供一个--module-path参数,以便Java VM可以找到模块所需的第三方模块。
将Java模块打包为独立应用程序
我们可以将Java模块以及所有必需的模块(递归地)和Java Runtime Environment打包到一个独立的应用程序中。这样的独立应用程序的用户不需要预先安装Java即可运行该应用程序,因为该应用程序附带了Java。也就是说,与应用程序实际使用的Java平台一样多。
我们可以使用Java SDK附带的" jlink"命令将Java模块打包到独立的应用程序中。这是用jlink
打包Java模块的方法:
jlink --module-path "out;C:\Program Files\Java\jdk-9.0.4\jmods" --add-modules com.Hyman.mymodule --output out-standalone
" --module-path"参数指定要其中查找模块的模块路径。上面的示例设置了我们先前已将模块编译到的" out"目录和JDK安装的" jmods"目录。
" --add-modules"参数指定要打包到独立应用程序中的Java模块。上面的示例仅包含com.Hyman.mymodule
模块。
--output参数指定将生成的独立Java应用程序写入哪个目录。该目录必须不存在。
运行独立应用程序
打包后,我们可以通过打开控制台(或者终端)来运行独立的Java应用程序,将目录更改为独立的应用程序目录,然后执行以下命令:
bin\java --module com.Hyman.mymodule/com.Hyman.mymodule.Main
独立的Java应用程序包含一个bin目录,其中包含一个Java可执行文件。该Java可执行文件用于运行该应用程序。
--module参数指定要运行的模块和主类。
未命名的模块
从Java 9开始,所有Java类都必须位于模块中,Java VM才能使用它们。但是,对于只具有编译类或者JAR文件的旧Java库,我们该怎么办?
在Java 9中,我们仍可以在运行应用程序时对Java VM使用-classpath
参数。在类路径上,可以包括所有较早的Java类,就像在Java 9之前所做的一样。在类路径上找到的所有类都将包含在Java所谓的未命名模块中。
未命名的模块将导出其所有软件包。但是,未命名模块中的类只能由未命名模块中的其他类读取。没有命名模块可以读取未命名模块的类。
如果软件包是由命名模块导出的,但也在未命名模块中找到,则将使用命名模块中的软件包。
未命名模块中的所有类都需要在模块路径上找到的所有模块。这样,未命名模块中的所有类都可以读取模块路径上找到的所有Java模块导出的所有类。
自动模块
如果我们正在模块化自己的代码,但是代码使用尚未模块化的第三方库,该怎么办?虽然我们可以在类路径中包括第三方库,从而将其包含在未命名模块中,但是我们自己的命名模块无法使用它,因为命名模块无法从未命名模块读取类。
该解决方案称为自动模块。自动模块由具有未模块化Java类的JAR文件制成,这意味着JAR文件没有模块描述符。使用Java 8或者更早版本开发的JAR文件就是这种情况。当我们将普通的JAR文件放在模块路径(而不是类路径)上时,Java VM将在运行时将其转换为自动模块。
自动模块需要模块路径上的所有命名模块。换句话说,它可以读取模块路径中所有命名模块导出的所有软件包。
如果应用程序包含多个自动模块,则每个自动模块都可以读取所有其他自动模块的类。
自动模块可以读取未命名模块中的类。这与不能读取未命名模块中的类的显式命名模块(实际Java模块)不同。
自动模块会导出其所有软件包,因此模块路径上的所有命名模块都可以使用自动模块的类。但是,命名模块仍必须明确要求使用自动模块。
关于不允许拆分软件包的规则也适用于自动模块。如果多个JAR文件包含(并因此导出)相同的Java软件包,则这些JAR文件中只有一个可用作自动模块。
自动模块是命名模块。自动模块的名称是从JAR文件的名称派生的。如果JAR文件的名称为com-Hyman-mymodule.jar
,则相应的模块名称为com.Hyman.mymodule
。模块名称中不允许使用"-"(破折号)字符,因此将其替换为"。"字符。后缀.jar被删除。
如果JAR文件的文件名包含版本控制,例如然后使用" com-Hyman-mymodule-2.9.1.jar",然后在导出自动模块名称之前,从文件名中删除版本控制部分。因此,生成的自动模块名称仍为" com.Hyman.mymodule"。
服务
Java 9带来了一个称为服务的新概念。 Java服务与Java平台模块系统有关,因此我将在此Java模块教程中解释Java服务。
服务包括两个主要部分:
- 服务接口。
- 一种或者多种服务实现。
服务接口通常位于服务接口Java模块中,该模块仅包含服务接口以及与服务接口相关的所有类和接口。
服务实现是由单独的Java模块而不是服务接口模块提供的。通常,服务实现Java模块将包含一个服务实现。
Java模块或者应用程序可能需要服务接口模块并针对服务接口进行编码,而无需确切知道哪个其他模块可以交付服务实现。服务实现是在运行时发现的,并且取决于启动应用程序时Java模块路径上可用的服务实现模块。
服务接口模块
Java服务接口模块不需要服务接口的特殊声明。我们只需创建一个常规的Java模块。这是一个Java服务模块描述符示例:
module com.Hyman.myservice { exports com.Hyman.myservice }
注意如何不提及实际的服务接口。服务接口模块仅导出包含服务接口的Java包。服务接口只是一个普通的Java接口,因此我没有显示它的示例。
服务实施模块
想要从服务接口模块实现服务接口的Java模块必须:
- 在其自己的模块描述符中要求服务接口模块。
- 用Java类实现服务接口。
- 在其模块描述符中声明服务接口实现。
想象一下com.Hyman.myservice模块包含一个名为com.Hyman.myservice.MyService的接口。还要想象一下,我们想为此服务接口创建一个服务实现模块。想象一下,实现名为" com.blabla.myservice.MyServiceImpl"。要声明该服务实现,服务实现模块的模块描述符必须如下所示:
module com.blabla.myservice { requires com.Hyman.myservice; provides com.Hyman.myservice.MyService with com.blabla.myservice.MyServiceImpl }
模块描述符首先声明它需要服务接口模块。其次,模块描述符声明它通过类com.blabla.myservice.MyServiceImpl
提供了服务接口com.Hyman.myservice.MyService
的实现。
现在,该模块声明它实现了服务接口,我们需要了解Java模块如何在运行时查找服务接口的实现。
服务客户端模块
一旦拥有服务接口模块和服务实现模块,就可以创建使用该服务的客户端模块。有时,服务客户端模块称为服务使用者模块或者服务用户模块,但含义与使用外部模块中指定的服务并由另一个外部模块实现的模块相同。
为了使用该服务,客户端模块必须在其模块描述符中声明其使用该服务。这是在模块描述符中声明服务使用的方法:
module com.client.myservicelient { requires com.Hyman.myservice; uses com.Hyman.myservice.MyService; }
注意客户端模块描述符如何也声明它需要包含服务接口的com.Hyman.myservice
模块。它不需要服务实现模块。这些是在运行时查找的。仅需要服务接口模块。
不必声明服务实现模块的优点是,可以在不破坏客户代码的情况下交换实现模块。我们可以通过将所需的服务实现模块拖放到模块路径中,来决定在组装应用程序时使用哪种服务实现。客户端模块和服务接口模块因此与服务实现模块分离。
现在,服务客户端模块可以在运行时查找服务接口实现,如下所示:
Iterable<MyService> services = ServiceLoader.load(MyService.class);
返回的Iterator包含MyService接口的实现列表。实际上,它将包含在模块路径上找到的模块中找到的所有实现。客户端模块现在可以迭代Iterator
并找到它要使用的服务实现。
模块版本控制
Java平台模块系统不支持Java模块的版本控制。运行Java 9+应用程序时,模块路径上不能有模块的多个版本。我们将需要使用Maven或者Gradle之类的构建工具来处理模块的版本控制,以及模块所依赖的外部模块(使用/要求)。
多Java版本模块JAR文件
从Java 9中可以为Java模块创建JAR文件,其中包含专门为不同版本的Java编译的代码。这意味着,我们可以为模块创建一个JAR文件,其中包含为Java 8,Java 9,Java 10等编译的代码,并且都在同一JAR文件中。
JAR文件根目录下的com
文件夹包含Java 9之前版本的已编译Java类。 Java的早期版本不理解多Java版本的JAR文件,因此它们使用此处找到的类。因此,我们只能支持Java 9之前的一个Java版本。
" META-INF"目录包含" MANIFEST.MF"文件和一个名为" versions"的目录。 MANIFEST.MF
文件需要一个特殊的条目来将JAR文件标记为多版本JAR文件。此项的外观如下:
Multi-Release: true
versions
目录可以包含模块的Java不同版本的已编译类。在上面的示例中,versions
目录中有两个子目录。 Java 9的一个子目录,Java 10的一个子目录。目录名与要支持的Java版本号相对应。
迁移至Java 9
Java Platform Module System的某些部分旨在简化将Java 9之前的版本的应用程序迁移到Java 9的过程。模块系统旨在支持"自下而上的迁移",这意味着我们首先迁移了小型实用程序库,最后迁移了主要应用程序。
迁移的目的是这样的:
- 升级到Java 9,并在不模块化任何内容的情况下运行应用程序。正常将类和JAR文件放在类路径上。然后,这些类成为未命名模块的一部分。
- 将实用程序库JAR文件移动到模块路径。这样,它们就成为自动模块。对内部和第三方实用程序库都执行此操作。主要应用程序仍应位于未命名模块中的类路径上。未命名的模块可以读取所有已命名的模块,无论是自动的还是非自动的。
- 尽可能将内部和第三方实用程序库迁移到Java模块。将Java模块放在模块路径上。从没有其他模块/ JAR依赖关系的库开始,然后在依赖关系层次结构中上移。
- 将主要应用程序迁移到Java模块。
通过自下而上的升级,在将所有内容升级到Java模块之前的这段时间内,应用程序能够工作的机会就更高。应用程序应该能够在上述任何一个阶段中运行。
从依赖关系层次结构的底部开始,首先将实用程序库升级为自动模块,然后再升级为完整模块,应确保库在升级期间仍可以相互读取,并且可由未命名模块中类路径上的主要应用程序读取。