命令行构建模块
当使用模块系统为您的代码创建模块时,您可能在一个使用构建工具的项目中这样做,因此构建工具的任务是确保一切正确。但是,了解“正确”是什么以及如何正确配置javac
、jar
和java
来编译、打包和运行您的应用程序将非常有帮助。这将使您更好地理解模块系统,并在构建工具无法正确处理时帮助调试问题。
注意:您需要了解模块系统基础知识才能充分利用本文。您可能还想查看核心 JDK 工具的描述。
基本构建
给定一个包含一些源文件、模块声明和一些依赖项的项目,以下是如何以最简单的方式编译、打包和运行它
# compile sources files, including module-info.java
$ javac
--module-path $DEPS
-d $CLASS_FOLDER
$SOURCES
# package class files, including module-info.class
$ jar --create
--file $JAR
$CLASSES
# run by specifying a module by name
$ java
--module-path $JAR:$DEPS
--module $MODULE_NAME/$MAIN_CLASS
其中包含许多占位符
$DEPS
是依赖项列表。这些通常是通过:
(Unix)或;
(Windows)分隔的 JAR 文件路径,但在模块路径上,这也可以只是文件夹名称(没有在类路径上所需的/*
技巧)。$CLASS_FOLDER
是将写入*.class
文件的文件夹的路径。$SOURCES
是*.java
文件列表,必须包括module-info.java
。$JAR
是将创建的 JAR 文件的路径。$CLASSES
是编译期间创建的*.class
文件列表(因此位于$CLASS_FOLDER
中),必须包括module-info.class
。$MODULE_NAME/$MAIN_CLASS
是初始模块的名称(即模块解析开始的模块),后跟包含应用程序main
方法的类的名称。
对于具有常见src/main/java
结构的简单“Hello World”风格项目,只有一个源文件,依赖项位于deps
文件夹中,并使用 Maven 的target
文件夹,它将如下所示
$ javac
--module-path deps
-d target/classes
src/main/java/module-info.java
src/main/java/com/example/Main.java
$ jar --create
--file target/hello-modules.jar
target/classes/module-info.class
target/classes/com/example/Main.class
$ java
--module-path target/hello-modules.jar:deps
--module com.example/com.example.Main
定义主类
jar
选项--main-class $MAIN_CLASS
将$MAIN_CLASS
嵌入到模块描述符中,作为包含main
方法的类,这使您可以启动模块,而无需命名主类
$ jar --create
--file target/hello-modules.jar
--main-class com.example.Main
target/classes/module-info.class
target/classes/com/example/Main.class
$ java
--module-path target/hello-modules.jar:deps
--module com.example
请注意,可以通过像以前一样命名它来覆盖该类并启动另一个类
# create a JAR with `Main` and `Side`,
# making `Main` the main class
$ jar --create
--file target/hello-modules.jar
--main-class com.example.Main
target/classes/module-info.class
target/classes/com/example/Main.class
target/classes/com/example/Side.class
# override the main class and launch `Side`
$ java
--module-path target/hello-modules.jar:deps
--module com.example/com.example.Side
规避强封装
模块系统对内部 API 的访问非常严格:如果包未导出或打开,则将拒绝访问。但是,包不能仅仅由模块的作者导出或打开 - 还有命令行标志--add-exports
和--add-opens
,允许模块的用户也这样做。
例如,请参见以下代码,该代码尝试创建内部类sun.util.BuddhistCalendar
的实例
BuddhistCalendar calendar = new BuddhistCalendar();
要编译和运行它,我们需要使用--add-exports
javac
--add-exports java.base/sun.util=com.example.internal
module-info.java Internal.java
# package with `jar`
java
--add-exports java.base/sun.util=com.example.internal
--module-path com.example.internal.jar
--module com.example.internal
如果访问是反射性的...
Class.forName("sun.util.BuddhistCalendar").getConstructor().newInstance();
... 编译将无需进一步配置即可完成,但我们需要在运行代码时添加--add-opens
java
--add-opens java.base/sun.util=com.example.internal
--module-path com.example.internal.jar
--module com.example.internal
有关强封装和使用add-exports
和add-opens
规避它的详细信息。
扩展模块图
从一组初始的根模块开始,模块系统计算所有这些模块的依赖项并构建一个图,其中模块是节点,它们的读写关系是定向边。这个模块图可以使用命令行标志--add-modules
和--add-reads
进行扩展,这些标志分别添加模块(及其依赖项)和读写边。
例如,假设一个项目对java.sql具有可选依赖项,但该模块在其他情况下不需要。这意味着如果没有一点帮助,它不会添加到模块图中
# launch without java.sql
$ java
--module-path example.jar:deps
--module com.example/com.example.Main
# launch with java.sql
$ java
--module-path example.jar:deps
--add-modules java.sql
--module com.example/com.example.Main
可选依赖项的另一种方法是不列出依赖项,而只使用--add-modules
和--add-reads
添加它(这很少有用,通常不推荐 - 只是一个例子)
$ java
--module-path example.jar:deps
--add-modules java.sql
--add-reads com.example=java.sql
--module com.example/com.example.Main
有关使用--add-modules
和--add-reads
扩展模块图的详细信息。
上次更新: 2021 年 9 月 14 日