Java模块化
为什么要模块化?
- 精简运行时
在JDK 9之前,Java的运行时库(Runtime Library,JRE)包含一个庞大的rt.jar,其大小超过60MB,包含了Java大部分运行时类。随着JDK的发展,JDK增加了许多API,但几乎没有删除任何API,有些类现在己经很少用了,但仍然保留在JRE中,保留它们的唯一目的就是为了保证兼容性。再比如javafx相关的类,即使应用程序用不到,但是还是被包含在最终的生产jar包中。模块化的作用就是将JDK进行模块拆分,让应用程序选择依赖的模块,打包时只打包这些模块,从而可以起到精简运行时的目的。
- 保持封装性
模块强调的是封装性,针对于库的开发者来说,经常有这种需求:类A被当前项目中其他包的其他类使用,但是不希望类A被使用这个库的其他项目使用,也就是保持一些API只在项目内部可见。虽然java提供了访问权限,但是只能做到包级别的访问控制。同时为了使代码组织看上去更清晰,私有的API也是需要跨包互相引用的。
模块化基础
JPMS:JPMS 全称Java Platform Module System,Java模块化系统。由JEP261引入(https://openjdk.org/jeps/261)
Jigsaw项目:https://openjdk.org/projects/jigsaw/
模块化的JDK
如果你下载JDK8之后版本的JDK,你会发现安装之后里面少了很多在JDK8存在的jar包,这一点就是模块化带来的变化,因为JDK9之后JDK不再以jar包形式发布,而是以模块的形式进行发布,这种JDK就是经过模块化的JDK。
下面分别展示oracle发布的jdk8和jdk11安装后的样子:
[root@localhost java]# ll
total 353836
drwxr-xr-x. 9 root root 126 Jul 7 12:56 jdk-11.0.27
drwxr-xr-x. 8 root root 190 Apr 15 21:37 jdk8u452-b09
[root@localhost java]# find ./jdk-11.0.27/ -type f -name "*.jar"
./jdk-11.0.27/lib/jrt-fs.jar
[root@localhost java]# find ./jdk8u452-b09/ -type f -name "*.jar"
./jdk8u452-b09/lib/dt.jar
./jdk8u452-b09/lib/tools.jar
./jdk8u452-b09/lib/sa-jdi.jar
./jdk8u452-b09/lib/jconsole.jar
./jdk8u452-b09/jre/lib/rt.jar
./jdk8u452-b09/jre/lib/jsse.jar
./jdk8u452-b09/jre/lib/resources.jar
./jdk8u452-b09/jre/lib/ext/sunjce_provider.jar
./jdk8u452-b09/jre/lib/ext/nashorn.jar
./jdk8u452-b09/jre/lib/ext/zipfs.jar
./jdk8u452-b09/jre/lib/ext/sunpkcs11.jar
./jdk8u452-b09/jre/lib/ext/sunec.jar
./jdk8u452-b09/jre/lib/ext/cldrdata.jar
./jdk8u452-b09/jre/lib/ext/jaccess.jar
./jdk8u452-b09/jre/lib/ext/localedata.jar
./jdk8u452-b09/jre/lib/ext/dnsns.jar
./jdk8u452-b09/jre/lib/management-agent.jar
./jdk8u452-b09/jre/lib/security/policy/unlimited/US_export_policy.jar
./jdk8u452-b09/jre/lib/security/policy/unlimited/local_policy.jar
./jdk8u452-b09/jre/lib/security/policy/limited/US_export_policy.jar
./jdk8u452-b09/jre/lib/security/policy/limited/local_policy.jar
./jdk8u452-b09/jre/lib/jce.jar
./jdk8u452-b09/jre/lib/jfr.jar
./jdk8u452-b09/jre/lib/charsets.jar
JDK9之后安装完成后会有一个jmods文件夹,里面包容了很多*.jmod文件,这些就是模块化后的JDK
[root@localhost java]# tree ./jdk-11.0.27 -L 1
./jdk-11.0.27
├── bin
├── conf
├── include
├── jmods
├── legal
├── lib
├── man
├── README.html
└── release
可以通过/path/to/java.exe --list-modules命令列出JDK的模块清单
➜ $JAVA17_HOME/java --list-modules
java.base@17.0.7
java.compiler@17.0.7
java.datatransfer@17.0.7
java.desktop@17.0.7
......
模块描述符 module-info.java
任意一个 jar 文件,只要加上一个合法的模块描述符 (module descriptor),就可以变成为一个模块。
这个模块描述符就是位于 jar 包根路径下的 module-info.class 文件,此文件是由位于根目录下的module-info.java文件编译而来的,是 Java 模块化系统的核心文件。它用于定义模块的元数据和配置,明确模块的边界、依赖关系和访问权限
module-info.java 文件是应该位于源文件的根目录(也就是默认包名所在的目录)。主要内容如下:
- 定义模块名称:每个模块都有一个唯一的名称,通过 module 关键字定义。模块名称是模块的唯一标识符,不同的两个模块不能有相同的包名。通常采用反向域名命名法(如 com.example.mymodule)。
- 声明模块依赖:使用 requires 关键字声明模块所依赖的其他模块。依赖的模块可以是标准库模块(如 java.base)或其他自定义模块。
- 导出包:使用 exports 关键字声明模块对外公开的包。只有导出的包才能被其他模块访问,未导出的包是模块私有的。
- 提供Java的SPI服务:使用 provides 关键字声明模块提供的 SPI 服务实现。
- 使用 SPI 服务:使用 uses 关键字声明模块使用的服务接口。模块可以通过 SPI 机制动态获取服务实现。
- 开放反射访问:使用 opens 关键字开放某些包,允许其他模块通过反射访问这些包中的类和成员。默认情况下,模块中的类不允许通过反射访问。
通过/path/to/java --describe-module 模块名命令可以查看模块的信息,这个信息实际上就是 module-info.java 文件中的内容
➜ .\bin\java --describe-module java.base
java.base@11.0.18
exports java.io
exports java.lang
exports java.lang.annotation
exports java.lang.invoke
exports java.lang.module
......
exports java.nio.charset.spi
exports java.nio.file
exports java.nio.file.attribute
......
完整的内容如下:
module com.example.mymodule {
// 依赖其他模块
requires java.base;
// 导出包,允许其他模块访问
exports com.example.mymodule.api;
exports com.example.mymodule.util;
// 提供 SPI 服务
provides com.example.mymodule.spi.MyService with com.example.mymodule.impl.MyServiceImpl;
// 使用服务
uses com.example.mymodule.spi.MyService;
// 开放包,允许其他模块通过反射访问
opens com.example.mymodule.internal;
}
requires
不同模块之前的类要互相使用,需要先通过 require 声明需要的模块
module com.example.mymodule {
requires java.base;
requires com.example.othermodule;
// 传递依赖
requires transitive com.example.othermodule;
}
exports
模块 A 要使用模块 B 中包 com.example.moduleb.api 下的类,首先需要 require 该模块,然后需要在模块 B 的模块描述符中声明导出该包,然后模块 A 中才能 import com.example.moduleb.api,否则,IDE 也不会提示 com.example.moduleb.api 包下的类
module moduleb {
exports com.example.moduleb.api;
}
opens
允许其他模块通过反射访问指定包下的类,如果未指定,则会在运行时进行反射操作时报错
module com.example.mymodule {
opens com.example.mymodule.internal;
}
provides/uses
provide 声明提供的 SPI 服务实现,use 声明此模块使用的 SPI 服务
module mymodule {
// 依赖其他模块
requires java.base;
// 提供 SPI 服务
provides com.example.mymodule.spi.MyService with com.example.mymodule.impl.MyServiceImpl;
// 使用服务
uses com.example.mymodule.spi.MyService;
}
Classpath和Modulepath
module-path是和classpath类似的一个概念,如同只会加载classpath下的类一样,模块系统使用它来定位平台模块中找不到的所需模块,只有在module-path路径下的才会在启动时被识别为模块。所有 modulepath 下的元素都会被视为模块,即使是普通的 jar 包。
模块路径是一个列表,每个元素是一个模块的路径或者包含模块的路径。根据操作系统的不同,模块路径元素可以用以下方式分隔:
Windows下是;,Linux下是:,这一点和 classpath 也是一样的。
JDK9及以后,javac 和 java 以及其他与模块相关的命令都可以处理 modulepath:
- --module-path 参数 或者 -p:此参数用于指定模块路径,模块位置
- --add-modules 参数
例如java --add-modules my.module.name -jar myapp.jar,这个参数主要有2个作用
- 添加模块:允许在启动 Java 应用程序时指定要加载的模块,特别是对于未在模块路径中列出的模块。
- 解决依赖问题:在某些情况下,应用程序可能依赖于特定的模块,而这些模块未被自动加载。使用 --add-modules 可以明确指定这些模块,确保它们在运行时可用。
在很多方面,可以将Modulepath视为Classpath的替代者,但是并不是说Classpath这个概念就被移除了,因为要保证兼容性。
不使用模块路径,而是通过类路径来构建和运行应用程序时,只是没有在应用中使用新的模块特性而已。这对现有代码的改动需求极小,甚至无需改动。大致来说,只要应用程序及其依赖项仅使用 JDK 中官方认可的 API,它在JDK9及以上版本就应该能顺利编译和运行。
从 Java 9 开始,无论你的应用程序是否使用模块,它所运行的 JDK 始终由模块组成。在多数情况下,模块化的 JDK 不会给基于类路径的应用程序带来问题。由于 JDK 本身已经模块化了,因此某些场景下不可避免地需要做一些改动,这些场景大多与一些三方库相关。尽管在这些场景下,应用程序层面几乎无需关注模块系统,但 JDK 结构的变化是客观存在的。
模块化迁移-不模块化
Java一直保持了很好的向后兼容性,因此即使有了模块系统,但是对于应用来说,模块化升级不是必须的。我们可以只使用高版本的JDK,不使用模块化,这样和JDK8的代码在很多场景下没有区别。但是毕竟JDK底层是做了改动了,所以在一些场景下还是有一些问题的
下面将描述JDK为模块化做了哪些兼容性工作,以及迁移过程中的一些常见场景及解决方案
反射操作
某些库使用了平台模块中一些私有的API,通过反射强行暴露出来,这个操作与模块化所提倡的封装理念是相违背的
以javassist这个库为例:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
下面是一个示例:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.reflect.Method;
public class CreateClassExample {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass newClass = pool.makeClass("com.example.MyClass");
String method = "public void sayHello() { System.out.println(\"Hello, World!\"); }";
CtMethod method = CtMethod.make(method, newClass);
newClass.addMethod(method);
Class<?> clazz = newClass.toClass(); // 将类加载到 JVM 中
Object obj = clazz.getConstructor().newInstance(); // 实例化并调用方法
Method sayHello = obj.getClass().getDeclaredMethod("sayHello");
sayHello.invoke(obj);
}
}
JDK8下运行这段代码没有问题
JDK11下这段代码执行会报警告

除了这条警告外,应用程序仍会照常运行。正如警告信息所提示的,这一行为将在 Java 的后续版本中发生改变。未来,JDK 会对平台模块强制执行严格的封装,即便对于类路径上的代码也是如此。在未来的 Java 版本中,相同的应用程序在默认设置下将无法运行。例如:JDK17运行这段代码直接报错

这个错误表示javaassist正在使用java.lang.ClassLoader#defineClass(byte[], int, int)这个非公开的API
默认情况下,首次发生非法访问尝试时只会生成一条警告,后续的尝试不会产生额外的错误或警告。如果我们想进一步排查问题的原因,可以通过为 --illegal-access 命令行标志设置不同的值来调整其行为:
--illegal-access=permit
- 行为:这是默认行为。允许对封装类型进行非法访问,但在首次通过反射进行非法访问时会生成一条警告信息。
- 说明:此模式下,程序可以正常执行非法访问操作,仅在第一次出现时提醒开发者存在不合规的访问行为,后续访问不再警告。
--illegal-access=warn
- 行为:与
permit类似,允许非法访问,但每次非法访问尝试都会生成错误信息(注意这里的 “error” 更偏向于警告级别,不会直接终止程序执行)。 - 说明:相比
permit,此模式更严格,通过反复提醒来强调非法访问的频繁性,促使开发者修复问题。
--illegal-access=debug
- 行为:在
warn模式的基础上,额外显示非法访问尝试的堆栈跟踪(stack traces)。 - 说明:堆栈跟踪能精确显示非法访问发生的代码位置(类、方法、行号等),方便开发者定位问题根源,常用于调试阶段。
--illegal-access=deny
- 行为:不允许任何非法访问尝试,一旦发生非法访问,会直接阻止操作(可能导致程序抛出异常或终止)。
- 说明:这是 Java 未来计划采用的默认模式,旨在强化封装性和安全性,符合模块化系统(如 Java 9 引入的模块系统)的设计理念,强制开发者通过合规方式访问类成员。
注意,在设计上是禁止消除这个打印的警告的,并且--illegal-access=deny 是未来版本的默认设置,所以必须从根本上解决非法访问警告
--add-opens / --add-exports
对于一些无法控制的模块,例如系统模块,可以手动使反射合法化,添加启动参数:
--add-opens java.base/java.lang=ALL-UNNAMED
这个的意思是ALL-UNNAMED访问java.base/java.lang,而ALL-UNNAMED代表整个类路径,因为目前迁移是未添加模块化特性的,所以是ALL-UNNAMED
同样的,对于某些未被导出的API,如果要使用它,可以通过--add-exports来强行导出,例如
--add-exports java.base/sun.security.x509=ALL-UNNAMED
命令行参数过长
一些操作系统对命令的长度有限制,所以如果使用--add-opens和--add-exports时很容易超过限制。这时可以将参数放到文件里面,然后执行java或javac:java @arguments.txt
-cp application.jar:javassist.jar
--add-opens java.base/java.lang=ALL-UNNAMED
--add-exports java.base/sun.security.x509=ALL-UNNAMED
-jar application.jar
sun.misc.Unsafe使用
JDK中有些API只应该在JDK内部使用,常见的比如下面这些:
sun.misc.Signal
sun.misc.SignalHandler
sun.misc.Unsafe
sun.reflect.Reflection::getCallerClass(int)
sun.reflect.ReflectionFactory::newConstructorForSerialization
对于模块化系统来说,这些API应该是要封装起来的,但是一些库广泛使用了这些API,因此JDK9做了一个妥协:这些API被放在了jdk.unsupported这个模块中,并且这个模块export了这些应该被封装的API
./jdk-11.0.27/bin/java --describe-module jdk.unsupported
[root@localhost java]# ./jdk-11.0.27/bin/java --describe-module jdk.unsupported
jdk.unsupported@11.0.27
exports com.sun.nio.file
exports sun.misc
exports sun.reflect
requires java.base mandated
opens sun.reflect
opens sun.misc
Unnamed Module
UNNAMED-MODULE 是一种特殊的模块,用于兼容非模块化的代码(即未明确声明模块的代码)。它是 Java 模块化系统的一部分,旨在确保在模块化环境中仍然能够运行传统的、非模块化的 JAR 包和类。
当类路径(--class-path)上的 JAR 包或类没有被明确声明为模块时,Java 会将它们放入 UNNAMED-MODULE 中,即类路径下所有未模块化的类和jar包在一个叫做UNNAMED-MODULE 的模块下
这样可以确保传统的、非模块化的代码仍然可以在模块化系统中运行。
默认访问权限:UNNAMED-MODULE 默认可以读取所有其他模块(包括命名模块和自动模块)。但是,命名模块不能读取 UNNAMED-MODULE 中的类,除非显式地开放包或使用反射。
自动模块的依赖:自动模块(Automatic Module)可以读取 UNNAMED-MODULE 中的类。
UNNAMED-MODULE 的特点
- 没有模块名称:或者说模块名称就是 UNNAMED-MODULE
- 默认导出所有包:默认导出所有包,但这些包只能被自动模块访问,而不能被命名模块访问。
- 无法显式依赖:命名模块无法在 module-info.java 中声明对 UNNAMED-MODULE 的依赖(例如 requires UNNAMED-MODULE; 是无效的)。
- 反射访问:通过反射可以访问 UNNAMED-MODULE 中的类,但需要显式地开放包或使用 --add-opens 参数。例如
--add-opens javafx.controls/javafx.scene.control.skin=ALL-UNNAMED
在这个例子中,将 javafx.controls 中的 javafx.scene.control.skin 包被开放给所有匿名模块(ALL-UNNAMED),这样所有匿名模块就可以使用反射访问该包中的私有类或成员。
当运行一个非模块化的应用程序时,所有的类都会被加载到 UNNAMED-MODULE 中。
在模块化应用程序中,如果需要使用非模块化的 JAR 包,可以将这些 JAR 包放在类路径上,它们会被加载到 UNNAMED-MODULE 中。
模块化迁移-支持模块化
参考自:https://www.youtube.com/watch?v=p7jCvbzqnS0
如果想创建一个完全模块化的应用,但是依赖的某些第三方jar包还未完成模块化,等待这些jar包的维护者完成模块化是不现实的,所以我们需要手动迁移这些未模块化的jar包。下面将以实际案例来完成这一过程:
环境准备
操作环境Windows或者Linux都可以,不使用IDE,只需要安装java即可,同时安装jdk8和jdk11
首先准备一个简单的项目,结构如下所示
[root@localhost java]# tree ./tweet/
./tweet/
├── lib
│ ├── jackson-annotations-2.6.7.jar
│ ├── jackson-core-2.6.7.jar
│ └── jackson-databind-2.6.7.jar
└── src
├── MyApp.java
└── Tweet.java
下面是源码文件的内容:
Tweet.java
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.sql.Timestamp;
public class Tweet {
String text;
@JsonProperty("create_at")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "EEE MMM d HH:mm:ss Z yyy", timezone = "GMT")
Timestamp time;
public String getText() { return text; }
public void setText(String text) { this.text = text; }
public Timestamp getTime() {return time;}
public void setTime(Timestamp time) { this.time = time; }
@Override
public String toString() { return time + ": " + text; }
}
MyApp.java
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.List;
public class MyApp {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
List<Tweet> tweets = mapper.readValue(System.in, new TypeReference<List<Tweet>>() {
});
tweets.forEach(t -> System.out.format("%n%s: %s%n", t.getTime(), t.getText()));
}
}
接下来编译项目
[root@localhost tweet]# javac -d classes -cp lib/jackson-annotations-2.6.7.jar:lib/jackson-core-2.6.7.jar:lib/jackson-databind-2.6.7.jar $(find src/ -name '*.java')
[root@localhost tweet]# tree ./classes/
./classes/
├── MyApp$1.class
├── MyApp.class
└── Tweet.class
# PowerShell 使用下面这个命令
javac -d classes -cp "lib/jackson-annotations-2.6.7.jar;lib/jackson-core-2.6.7.jar;lib/jackson-databind-2.6.7.jar" (Get-ChildItem -Recurse -Path "src/" -Filter "*.java").FullName
通过下面的命令将项目打成 jar 包
jar --create --file ./lib/tweet.jar -C ./classes .
下面是测试数据
[ {
"text" : "AI won't replace you. But someone using AI might. Time to level up!",
"create_at" : "2025-08-12 14:27:37"
}, {
"text" : "Building a startup is like jumping off a cliff and assembling the plane on the way down.",
"create_at" : "2016-08-12 14:27:37"
} ]
执行程序,验证程序功能正常。
# Linux
java -cp "./lib/jackson-core-2.6.7.jar;./lib/jackson-annotations-2.6.7.jar;./lib/jackson-databind-2.6.7.jar;./lib/tweet.jar" MyApp <tweet.json
# PowerShell
Get-Content "tweet.json" -Encoding UTF8 | java -cp "./lib/jackson-core-2.6.7.jar;./lib/jackson-annotations-2.6.7.jar;./lib/jackson-databind-2.6.7.jar;./lib/tweet.jar" MyApp
jdeps 工具
jdeps 是 Java 8 及更高版本中提供的工具,用于分析 Java 类文件的依赖关系。java8 就提供了这个工具
常用参数
- 输入控制参数
| 参数 | 描述 |
|---|---|
--class-path <path> |
指定查找用户类文件的位置 |
--module-path <path> |
指定模块路径 |
--upgrade-module-path <path> |
指定升级模块路径 |
--system <jdk> |
指定系统 JDK 路径 |
--multi-release <version> |
指定多版本 JAR 文件的版本 (如 9, 11) |
- 依赖分析参数
| 参数 | 描述 |
|---|---|
-v 或 -verbose |
显示详细依赖信息 |
-s 或 -summary |
显示依赖摘要 |
-dot-output <dir> |
生成 DOT 格式的依赖图文件 |
--package <pkg> |
仅分析指定包的依赖 |
--regex <regex> |
仅分析与正则表达式匹配的依赖 |
--require <module> |
仅分析指定模块的依赖 |
-jdkinternals |
分析使用 JDK 内部 API 的情况 |
-apionly |
仅分析从API导出的依赖 |
- 输出控制参数
| 参数 | 描述 |
|---|---|
-q 或 -quiet |
静默模式,不显示类级依赖 |
-p <pkg> |
仅显示指定包的依赖 |
-e <regex> |
仅显示与正则表达式匹配的依赖 |
--filter <regex> |
过滤掉匹配的依赖 |
--filter:archive |
过滤掉归档内部的依赖 |
--filter:package |
过滤掉同一包内的依赖 |
--filter:module |
过滤掉同一模块内的依赖 |
- 模块化分析参数
| 参数 | 描述 |
|---|---|
--module <module> |
仅分析指定模块 |
--generate-module-info <dir> |
生成模块描述符 (module-info.java) |
--generate-open-module <dir> |
生成开放模块描述符 |
--check <module> |
检查模块的依赖 |
--list-deps |
列出依赖的模块和 JDK 内部 API |
--list-reduced-deps |
列出简化后的依赖关系 |
--print-module-deps |
打印模块依赖关系 |
- 其他参数
| 参数 | 描述 |
|---|---|
-h 或 -help |
显示帮助信息 |
--version |
显示版本信息 |
-cp 或 -classpath |
同 --class-path |
-m 或 --module |
同 --module |
实际案例
基本依赖分析
# 分析单个JAR文件的依赖
jdeps ./jackson-core-2.9.0.jar
# 显示详细依赖信息: 具体到一个jar包中的每个类依赖的哪些类,以及依赖的这些类是在哪个jar包或是哪个模块
➜ jdeps -v ./jackson-core-2.9.0.jar
jackson-core-2.9.0.jar -> java.base
com.fasterxml.jackson.core.Base64Variant -> com.fasterxml.jackson.core.Base64Variants jackson-core-2.9.0.jar
com.fasterxml.jackson.core.Base64Variant -> com.fasterxml.jackson.core.util.ByteArrayBuilder jackson-core-2.9.0.jar
com.fasterxml.jackson.core.Base64Variant -> java.io.Serializable java.base
......
com.fasterxml.jackson.core.Base64Variant -> java.lang.Character java.base
com.fasterxml.jackson.core.Base64Variant -> java.lang.IllegalArgumentException java.base
com.fasterxml.jackson.core.Base64Variant -> java.lang.Integer java.base
com.fasterxml.jackson.core.Base64Variant -> java.lang.Object java.base
com.fasterxml.jackson.core.Base64Variant -> java.lang.String java.base
com.fasterxml.jackson.core.Base64Variant -> java.lang.StringBuilder java.base
# 显示依赖摘要: 这个信息就比较简单了,只会列出依赖哪个jar包或者模块
> jdeps -s ./jackson-core-2.9.0.jar
jackson-core-2.9.0.jar -> java.base
列出模块依赖
列出jar包依赖的jar包或者模块,例如:jdeps --list-deps myapp.jar
# 列出了jar包依赖的 jar 包
[root@localhost tweet]# $JAVA8_HOME/bin/jdeps -s ./lib/jackson-*.jar
jackson-annotations-2.6.7.jar -> /root/develop/java/jdk8u452-b09/jre/lib/rt.jar
jackson-core-2.6.7.jar -> /root/develop/java/jdk8u452-b09/jre/lib/rt.jar
jackson-databind-2.6.7.jar -> ./lib/jackson-annotations-2.6.7.jar
jackson-databind-2.6.7.jar -> ./lib/jackson-core-2.6.7.jar
jackson-databind-2.6.7.jar -> /root/develop/java/jdk8u452-b09/jre/lib/rt.jar
# 使用JDK11执行是下面这样:列出了 jar 包依赖的模块
[root@localhost tweet]# $JAVA11_HOME/bin/jdeps -s ./lib/jackson-*.jar
jackson-annotations-2.6.7.jar -> java.base
jackson-core-2.6.7.jar -> java.base
jackson-databind-2.6.7.jar -> ./lib/jackson-annotations-2.6.7.jar
jackson-databind-2.6.7.jar -> ./lib/jackson-core-2.6.7.jar
jackson-databind-2.6.7.jar -> java.base
jackson-databind-2.6.7.jar -> java.sql
jackson-databind-2.6.7.jar -> java.xml
生成.dot文件保存的依赖图
dot 是一个绘图语言,可以表达有向图和无向图,用dot 语言描述的图可以render 成png、jpeg、pdf、svg 等等各种格式,非常方便。可视化dot文件,参考:https://blog.csdn.net/qq_44846964/article/details/134888257
> jdeps --dot-output ./ ./jackson-core-2.6.7.jar
> cat .\summary.dot
digraph "summary" {
"jackson-core-2.6.7.jar" -> "java.base (java.base)";
}
生成module-info.java
jdeps可以直接基于没有模块化的jar包生成module-info.java文件,利用这个我们可以制作模块化的 jar 包。使用方法如下:
jdeps --generate-module-info <output_directory> <path_to_your_class_files>
- <output_directory>:指定生成的 module-info.java 文件的输出目录。
- <path_to_your_class_files>:指定你的 .class 文件的路径。可以是jar包
举个例子:以下的命令会在<output_directory>下输出一个模块名称对应的目录,目录下就是生成的 module-info.java 文件,这个模块名称是根据前面所说针对Auto-Module的命名策略进行生成的
[root@localhost tweet]# $JAVA11_HOME/bin/jdeps --generate-module-info src ./lib/jackson-core-2.6.7.jar
writing to src/jackson.core/module-info.java
[root@localhost tweet]# cat ./src/jackson.core/module-info.java
module jackson.core {
exports com.fasterxml.jackson.core;
exports com.fasterxml.jackson.core.base;
exports com.fasterxml.jackson.core.filter;
exports com.fasterxml.jackson.core.format;
exports com.fasterxml.jackson.core.io;
exports com.fasterxml.jackson.core.json;
exports com.fasterxml.jackson.core.sym;
exports com.fasterxml.jackson.core.type;
exports com.fasterxml.jackson.core.util;
provides com.fasterxml.jackson.core.JsonFactory with
com.fasterxml.jackson.core.JsonFactory;
}
还可以一次性生成所有jar包的module-info.java文件
[root@localhost tweet]# $JAVA11_HOME/bin/jdeps --generate-module-info src ./lib/jackson*
writing to src/jackson.annotations/module-info.java
writing to src/jackson.core/module-info.java
writing to src/jackson.databind/module-info.java
自动生成的 module-info.java 文件如果有需要修改的内容,需要进行适当调整。
有了 module-info.java 文件这个模块描述符以后,我们需要将其编译写入 jar 包中。而编译 module-info.java 则需要该 jar 包的源码文件
检查是否使用了JDK内部API
使用下面的方式检查是否使用了 jdk.internals 相关 API
在 JDK 内部 API 上查找类级别的被依赖对象。除非指定了 -include 选项,否则默认情况下, 它会分析 --class-path 上的所有类和输入文件。此选项不能与 -p, -e 和 -s 选项一起使用。警告: 无法访问 JDK 内部 API。
jdeps -jdkinternals ./jackson-core-2.6.7.jar
# 或者
jdeps --jdk-internals ./jackson-core-2.6.7.jar
多版本JAR分析
分析多版本JAR的特定版本,指定处理多发行版 jar 文件时的版本。<版本> 应为大于等于 9 的整数或基数。例如
jdeps --multi-release 11 ./jackson-core-2.6.7.jar
过滤输出
# 只显示com.example包的依赖
jdeps -p com.example ./jackson-core-2.6.7.jar
# 过滤掉java.base模块的依赖
jdeps --filter:module java.base ./jackson-core-2.6.7.jar
Automatic Modules
jackson是开源的,我们可以基于它的源码构建一个模块化的版本,但是在依赖很多的情况下, 一个一个进行模块化显然不太现实。因此JPMS提供了一个特性:automatic modules(自动模块)
可以通过将现有 JAR 文件从类路径移动到模块路径,而不更改其内容。这会将 JAR 转换为带有模块描述符的模块,由模块系统动态生成。相比之下,显式模块总是有一个用户定义的模块描述符。到目前为止,我们看到的所有模块,包括平台模块,都是显式模块。
自动模块的行为与显式模块不同。自动模块具有以下内容
自动模块的特性:
- 不包含module-info.class文件
- 模块名基于META-INF/MANIFEST.MF文件Automatic-Module-Name的这个属性指定或者从文件名进行推断
- requires transitive了模块关系图所有其他模块,也就是说require了一个自动模块,那么就自动所有其他模块
- 导出自身的所有包
- 会读取classpath
- 不能与其他模块进行拆分包
对于普通的jar包,没有module-info.class文件,如果要参与模块化系统的构建中,那么需要为其指定模块信息。此时JPMS只能根据一定的规则来推测生成模块信息
对于一个模块重点需要关注的有3点:
- 模块名称
确定模块名称有2种策略
- 在签名文件中定义了Automatic-Module-Name这个头,那么该属性的值就是模块名称
- 取该jar包的名称作为模块名称:这种方式是不可控的,因为jar包名称就是普通的文件名称,可以随便指定
模块名称,模块取名算法如下:dash(-)替代为dot(.);省略版本号
- 该模块依赖哪些东西
简单来说,就是确定module-info.java中的requires部分,由于未显示指定,JPMS也不知道这个jar包依赖哪些东西
- Exports/Opens
导出所有的公共内容,并隐式从其他模块导入所有公共内容。
模块化 jar 包
将一个非模块化的 jar 包转化为模块化的 jar 包要经过以下几个步骤:
-
使用 jdeps 生成 module-info.java
jdeps --generate-module-info <output_dir> <jar_path> -
使用 javac编译 module-info.java 文件
javac --patch-module <module_name>=<jar_path> <module_name>/module-info.java使用
--patch-module这个选项用来在编译时用指定的 JAR 文件或目录中的类和资源覆盖或者扩展模块,本文后面会详细介绍这个参数。 -
使用 jar 将编译后的 module-info.class 文件添加到 jar 包中
jar uf <jar_path> -C <module_name> module-info.class
以前面的 jackson 这些 jar 包为例,目录结构如下:
├─lib
│ jackson-annotations-2.6.7.jar
│ jackson-core-2.6.7.jar
│ jackson-databind-2.6.7.jar
├─src
│ MyApp.java
│ Tweet.java
└─target
└─classes
MyApp.class
第一步:生成 module-info.java 文件
jdeps --generate-module-info ./mods ./lib/*.jar
第二步:使用下面的命令进行编译
javac -d ./mods/jackson.core --patch-module jackson.core=./lib/jackson-core-2.6.7.jar `
./mods/jackson.core/module-info.java
javac -d ./mods/jackson.annotations --patch-module jackson.annotations=./lib/jackson-annotations-2.6.7.jar `
./mods/jackson.annotations/module-info.java
javac -d ./mods/jackson.databind --patch-module jackson.databind=./lib/jackson-databind-2.6.7.jar `
./mods/jackson.databind/module-info.java
注意:上面的命令添加了换行符(Windows是 `, Linux 是 \),使用时有问题的话注意去掉
当处理 jackson.databind 时报下面的错误:
.\mods\jackson.databind\module-info.java:2: 错误: 找不到模块: jackson.annotations
requires transitive jackson.annotations;
^
.\mods\jackson.databind\module-info.java:3: 错误: 找不到模块: jackson.core
requires transitive jackson.core;
这是因为前面为 jackson.databind 的 jar 包生成的 module-info.java 里 require 了这两个模块,而我们现在根本没有这两个模块,所以肯定会报错。前面我们提到了自动模块,所以我们可以将 jackson.core 和 jackson.annotations 这两个 jar 包添加到 module-path 下,这样在编译时就能找到了。注意:根据自动模块的命名规则,需要确保使用的 jar 包对应的模块名称符合要求。
javac -d ./mods/jackson.databind --patch-module jackson.databind=./lib/jackson-databind-2.6.7.jar `
--module-path ./lib ./mods/jackson.databind/module-info.java
这样编译仍然会有警告,警告可以先不管
➜ javac -d ./mods/jackson.databind --patch-module jackson.databind=./lib/jackson-databind-2.6.7.jar `
--module-path ./lib ./mods/jackson.databind/module-info.java
.\mods\jackson.databind\module-info.java:2: 警告: 需要自动模块的过渡指令
requires transitive jackson.annotations;
^
.\mods\jackson.databind\module-info.java:3: 警告: 需要自动模块的过渡指令
requires transitive jackson.core;
^
2 个警告
➜ tree /f
├─lib
│ jackson-annotations-2.6.7.jar
│ jackson-core-2.6.7.jar
│ jackson-databind-2.6.7.jar
├─mods
│ ├─jackson.annotations
│ │ module-info.class
│ │ module-info.java
│ ├─jackson.core
│ │ module-info.class
│ │ module-info.java
│ └─jackson.databind
│ module-info.class
│ module-info.java
├─src
│ MyApp.java
│ Tweet.java
└─target
└─classes
MyApp.class
接下来是第三步,将 module-info.class 插入到 jar 包中
jar uf ./lib/jackson-annotations-2.6.7.jar -C ./mods/jackson.annotations ./module-info.class
jar uf ./lib/jackson-core-2.6.7.jar -C ./mods/jackson.core ./module-info.class
jar uf ./lib/jackson-databind-2.6.7.jar -C ./mods/jackson.databind ./module-info.class
经过这些步骤,jackson 这 3 个普通的 jar 包变成了一个模块化的 jar 包。除上面这种方式之外,还有另一种方式:将 jar 包解压成目录,然后混合生成的 module-info.java 一起编译。
模块化普通 jar 包这一步遇到问题的话可以参考以下资料:
分包问题 Split Package
简单说就是:JPMS 不允许不同模块导出相同的包名
为了确保一致性,模块系统不允许同一个包从两个不同的模块中被读取。任何两个命名模块都不允许包含相同的包(无论是否导出这个包)。模块系统基于这个假设运行,每当需要加载类时,它会先查找包含该包的模块,然后在该模块中寻找类(这有助于提升类加载性能)。为了维护这个假设,模块系统会检查所有命名模块,确保没有包拆分现象,一旦发现就会报错。但在迁移过程中,旧的代码在类路径下,这些代码会被放入未命名模块中。为了最大限度保持兼容性,不会对这些代码严格应用模块相关的检查规则。这意味着命名模块(例如JDK中的模块)与未命名模块之间的包拆分不会被检测出来。
这种机制会给类加载带来一个问题:如果一个包在模块和类路径之间被拆分,对于该包中的类,类加载器将始终且仅会在模块中查找。当应用程序依赖的第三方库包含与JDK模块相同包名的类时,这意味着位于类路径部分的包中的类实际上是不可见的。这些类将无法被加载,或者两个第三方库包含有同名的类,其中一个库的类无法被加载,这两种情况都会引发 ClassNotFoundException 或 NoClassDefFoundError。这是Java模块化系统设计中为了保持模块封闭性而做出的权衡,可以通过 --patch-module 等机制显式解决这类包冲突问题。
遇到有这种问题的三方库,一般有以下几种解决办法:
-
使用一些maven或者gradle插件合并Jar
-
使用--path-module参数,见本文后面--patch-module参数的使用
-
放弃模块化
模块化相关命令行参数
--module-source-path
--module-source-path 是 Java 命令行工具的一部分,尤其在使用 javac 编译模块化的 Java 应用程序时非常重要。这个参数用于指定模块的源代码路径,帮助编译器找到模块的源代码。
--module-source-path 可以指定一个或多个目录,这些目录中包含模块的源代码。编译器将根据这些路径来查找和编译模块。
--module-source-path 参数使得编译模块化代码变得更容易。
假设有一个模块化的项目结构如下:
myproject/
├── moduleA/
│ ├── src/
│ │ └── module-info.java
│ │ └── com/example/moduleA/
│ │ └── MyClassA.java
└── moduleB/
├── src/
│ └── module-info.java
│ └── com/example/moduleB/
│ └── MyClassB.java
可以使用以下命令编译这两个模块:
javac --module-source-path myproject -d out $(find myproject -name "*.java")
参数说明
- --module-source-path myproject:指定根目录 myproject 作为模块源路径。
- -d out:指定输出目录为 out,编译后的文件将放在这个目录中。
- $(find myproject -name "*.java"):查找所有 Java 源文件。
--add-exports
此参数主要用于访问内部 API,可以在java 和javac中使用
一般使用格式为
java --add-exports $module/$package=$readingmodule
表示将$module这个模块下的$package这个包导出给 $readingmodule进行读取,从而使得$readingmodule模块中的代码可以读取$module 这个模块下的 $package 这个包下的所有 public 元素
注意--add-exports应该只是个临时方案,如果依赖的API有可选的替代品,并且是public的,或者随着一些库的升级,提供了一些public的相同功能的API,应该尽量修改代码进行适配
--add-opens
开放反射权限
Java 的 --add-opens $module/$package=$reflectingmodule 选项可用于将 $module 模块的 $package 包开放给 $reflectingmodule 模块进行反射操作。这样,$reflectingmodule 中的代码就能通过反射访问 $package 中的所有类型和成员,而其他模块则无法访问。
当将 $reflectingmodule 设置为 ALL-UNNAMED 时,类路径上的所有代码都可以通过反射访问该包。在迁移到 Java 9 的过程中,访问内部 API 时通常会使用这个占位符——只有当您自己的代码在模块中运行时,将导出限制到特定模块才真正有意义。
举个例子,像 Guice 这样的依赖注入库使用了类加载器的内部 API,这会导致如下错误:
Caused by: java.lang.reflect.InaccessibleObjectException:
Unable to make ClassLoader.defineClass accessible:
module java.base does not "opens java.lang" to unnamed module
请注意,错误消息指出了具体的问题,包括包含该类的模块。为了解决这个问题,我们只需要开放包含该类的包即可:
java --add-opens java.base/java.lang=ALL-UNNAMED --class-path $dependencies -jar $appjar
注意:--add-opens 是引入模块系统后,为了允许对特定模块的包进行反射访问而设计的命令行参数。它主要用于解决应用程序或第三方库(如 Guice、Hibernate 等)在尝试通过反射访问 JDK 内部 API 时遇到的 java.lang.reflect.InaccessibleObjectException 异常。虽然这个参数在迁移阶段很有用,但过度使用可能会带来安全风险和稳定性问题,因此建议仅在必要时使用,并考虑在长期通过其他方式(如使用官方API)避免对内部API的依赖。
--add-reads
编译期及运行时的 --add-reads $module=$targets 选项会在 $module 与逗号分隔列表 $targets 中的所有模块之间建立可读性连接。这使得 $module 能够访问这些模块所导出包中的所有public类型,即使 $module 没有通过 requires 子句声明对这些模块的依赖。如果将 $targets 设置为 ALL-UNNAMED,$module 就可以读取未命名模块中的内容。
--add-reads 主要应用于以下场景:
- 解决模块间非声明性依赖的需求
- 在迁移期间临时处理模块依赖问题
- 允许模块访问未命名模块中的类(通过
ALL-UNNAMED参数) - 实现某些框架所需的动态依赖解析
需要注意的是,虽然这个选项提供了灵活性,但过度使用可能会破坏模块系统的封装性设计原则,因此在生产环境中应谨慎使用,优先考虑通过正式的模块声明(requires)来建立依赖关系。
--add-modules
https://dev.java/learn/modules/add-modules-reads/
模块系统解析的根模块,从根模块解析(在编译时期,而不是运行时)模块依赖,通过--add-modules mod1,mod2可以将除 JDK 默认 root modules 外的模块添加到模块依赖解析中,可以通过扫描模块描述符把相关依赖的模块也同时解析了。
javac 和java都可以用这个选项
--module-path
module-path可以分为三类
- application module path,通过--module-path指定
- compilation module path,通过--module-source-path指定,配合javac使用
- upgrade module path,通过--upgrade-module-path指定
--module-path 会将没有模块声明的jar变为automatic module;module-path可以是class/jar目录,jar,jmod 目录
--patch-module
关于这个选项的使用参考:https://openjdk.org/projects/jigsaw/quick-start#xoverride
--patch-module $module=$artifact可以在编译期及运行时将 $artifact 中的所有类合并到 $module 模块中。
此参数可以向指定的模块添加类,用来在将目录或jar包中的class文件添加/覆盖到指定module,通常在测试环节使用
java --patch-module <模块名>=<路径1> --patch-module <模块名>=<路径2> ... <主类>
同时 patch 多个模块
java --patch-module java.base=patches/base --patch-module jdk.compiler=patches/compiler MainClass
如果你需要为多个模块指定多个修补路径,可以结合使用 --patch-module 和模块路径分隔符(: 或 ;,取决于操作系统):
# Linux/macOS(使用 : 分隔)
java --patch-module module1=patch1:patch2 --patch-module module2=patchA:patchB MainClass
# Windows(使用 ; 分隔)
java --patch-module module1=patch1;patch2 --patch-module module2=patchA;patchB MainClass
注意事项:
- 每个模块只能修补一次:不能对同一个模块多次使用
--patch-module(后面的会覆盖前面的)。 - 路径可以是目录或 JAR 文件:
- 如果是目录,会加载该目录下的所有类文件。
- 如果是 JAR 文件,会加载该 JAR 中的类。
- 模块化兼容性:修补的类必须与目标模块的包结构一致,否则可能引发
IllegalAccessError
jmod
jmod 是模块化系统推出的一种新的应用打包形式。大多数开发场景下,打包自己的应用模块,或者将其发布到 Maven 仓库,只需要打包成模块化的 jar 包就行了,不用将其打包为 jmod 模块。jmod 文件与 jar 不同,jmod 是不能单独运行的,它主要供 jlink 使用,用于构建可独立运行的运行时环境。主要适用场景:
- 模块包含本地库文件
- 模块包含其他配置文件
- 模块需要给其他模块使用,进行链接
JMOD 文件可以包含多个 class 文件、元数据和资源以外的文件。这种格式是可传输的,但不可执行,也就是说可以在编译时或链接时使用它,但不能在运行时使用。为了保证兼容性,高版本的JDK,首先保证始终支持直接运行 jar 包中的代码,然后在相应的命令行工具如 javac 和 java 中,添加对于模块化 jar 包的支持。
具体地说,就是 jlink 会将特定程序中用到的 jmod 模块抽取出来,构建出一个可独立运行的文件集合,这个文件集合仅包容它所用到的模块,移除了无关的模块,最终结果会比标准的 JRE 要小,并且不强制要求目标计算机上预先安装有特定版本的 JRE。
jmod文件
一个 .jmod 文件通常包含以下内容:
-
模块元数据module-info.class:这是模块的核心文件,定义了模块的名称、依赖关系(requires)、导出的包(exports)、提供的服务(provides)等信息。该文件是编译后的字节码文件,由 module-info.java 编译生成。
-
类文件(Class Files):模块中包含的所有 Java 类文件(.class 文件),这些文件是模块的功能实现。这些类文件通常位于模块的包路径下。
-
资源文件(Resources)
模块中使用的静态资源文件,例如配置文件、图片、文本文件等。这些文件通常位于模块的资源目录中。 -
本地库文件(Native Libraries):如果模块依赖于本地库(如 JNI 库),这些库文件(如 .dll、.so 或 .dylib)可以包含在 .jmod 文件中。
-
命令文件(Commands):模块中定义的可执行命令(如脚本或二进制文件),通常用于命令行工具。这些文件位于 bin 目录下。
-
配置文件(Configuration Files):模块可能包含一些配置文件,例如服务配置文件(META-INF/services/ 目录下的文件),用于定义服务提供者。
-
其他文件:模块可能包含其他辅助文件,例如文档(doc 目录)、头文件(include 目录)等。
一个 .jmod 文件实际上是一个类似于 .zip 文件的压缩文件,例如,可以将java.base.jmod文件直接修改为zip压缩文件,然后就可以进行解压,解压之后是下面这个样子:

也可以使用 jmod 工具创建、查看或提取其内容。其内部结构通常如下:
- classes/ :类文件
- bin/ :可执行命令
- conf/ :配置文件
- include/ :头文件
- legal/ :法律声明
- lib/ :本地库文件
- man/ :手册页
- module-info.class :模块元数据文件
jmod命令行工具
jmod工具是java9之后随jdk安装的一个命令行工具,和java命令在同一个位置,用来创建、查看jmod文件
- 查看 .jmod 文件内容:使用 jmod list 命令,该命令的作用就是列出jmod文件中每个文件的相对路径
/path/to/jmod list /path/to/<module-name>.jmod
- 提取 .jmod 文件:使用 jmod extract 命令,该命令作用就相当于解压缩文件,--dir参数指定解压后输出的位置
/path/to/jmod extract /path/to/<module-name>.jmod --dir <output-directory>
创建jmod文件
使用jmod create命令,支持3个参数
- <path>:指定模块的类文件所在路径
- <version>:模块的版本号。版本号通常遵循语义化版本规范,例如 1.0.0、2.3.4 等
- <module-name>.jmod:生成的 .jmod 文件名。
此命令必须要求module-info.class文件,一般形式如下:
jmod create --class-path <path> --module-version <version> <module-name>.jmod
我们还是使用前面的 tweet 项目,在 src 目录下创建 module-info.java 文件,内容如下:
module tweet {
requires jackson.core;
requires jackson.databind;
requires jackson.annotations;
requires java.sql;
exports org.tweet;
}
由于不允许默认包名,因此我们需要为 java 文件添加包名 org.tweet。
接下来编译项目
➜ javac -d ./target/tweet --module-path ./lib $(find src/ -name '*.java')
# Windows PowerShell 使用下面这个命令
➜ javac -d ./target/tweet --module-path ./lib (Get-ChildItem -Recurse -Path "src/" -Filter "*.java").FullName
结果如下:
├─lib
│ jackson-annotations-2.6.7.jar
│ jackson-core-2.6.7.jar
│ jackson-databind-2.6.7.jar
├─target
│ └─tweet
│ │ module-info.class
│ └─org
│ └─tweet
│ MyApp$1.class
│ MyApp.class
│ Tweet.class
└─src
│ module-info.java
└─org
└─tweet
MyApp.java
Tweet.java
下面制作 jmod :
jmod create --class-path .\target\tweet\ tweet.jmod
根目录下会生成 tweet.jmod 文件,文件内容很简单,就是将指定的编译结果进行打包而已
➜ jmod extract ./tweet.jmod --dir ./tweet
➜ tree /f
D:/Test
│ tweet.jmod
│ ...... 省略部分
└─tweet
└─classes
│ module-info.class
│
└─org
└─tweet
MyApp$1.class
MyApp.class
Tweet.class
maven-jmod-plugin
此插件可以直接在 maven 的构建过程中,将应用打包为 jmod 文件。但是此项目似乎还处于 alpha 版本。
https://github.com/apache/maven-jmod-plugin
https://maven.apache.org/plugins/maven-jmod-plugin/usage.html
jlink
https://docs.oracle.com/en/java/javase/11/tools/jlink.html
使用jlink工具可以定制化运行时,即将一组模块及其依赖关系组装并优化到自定义运行时镜像中。
- 传统方式:部署应用需要完整JRE(约200-300MB)
- JLink方式:只包含应用实际需要的模块(可小至40-50MB)
jlink 基于Java模块(module-info.java),静态分析模块依赖关系,只打包必要的模块到运行时镜像。一般使用方式如下:
jlink [options] --module-path modulepath --add-modules module[,module...] --output output_path
下面解释这些参数的含义
-
modulepath:参数指定查找模块的位置,可以是模块化的 jar 包,jmod 文件或者暴露的模块,暴露的模块就是指将 jmod 提取出来的根目录下的文件集合
-
module:指定需要加到运行时镜像中的模块,jlink 工具会添加这些模块以及这些模块的传递依赖到
-
output_path:指定运行时镜像的输出位置
主要优势
- ✅ 显著减小部署包体积:去除未使用的JDK模块
- ✅ 提升启动性能:精简的运行时加载更快
- ✅ 增强安全性:减少潜在攻击面
- ✅ 简化部署:自带JRE,无需目标机器预装Java
- ✅ 支持交叉编译:可为不同平台构建运行时
制作运行时镜像
参考:https://dev.java/learn/jlink/
还是基于我们前面的 tweet 项目,先编译项目,这次我们输出到 mods/tweet 目录下:
➜ javac -d mods/tweet --module-path ./lib $(find src/ -name '*.java')
# Windows PowerShell 使用下面这个命令
➜ javac -d mods/tweet --module-path ./lib (Get-ChildItem -Recurse -Path "src/" -Filter "*.java").FullName
由于自动模块不能参与 jlink ,所以我们使用到的 jackson 需要是模块化的,前面我们已经演示过了如何将普通的 jar 转换为模块化的 jar,下面我们就来使用这些模块化的 jackson 来进行 jlink。
# 注意:Windows 上;是表示多条命令,需要使用引号括起来
➜ jlink --module-path "./lib/;./mods/tweet" --add-modules tweet --output ./image/tweet
# 结果如下,文件比较多,因此省略了文件,只显示文件夹
➜ tree .\image\tweet\
.\IMAGE\TWEET
├─bin
│ └─server
├─conf
│ └─security
│ └─policy
│ ├─limited
│ └─unlimited
├─include
│ └─win32
├─legal
│ ├─java.base
│ ├─java.logging
│ ├─java.sql
│ ├─java.transaction.xa
│ └─java.xml
└─lib
└─security
# 查看镜像的模块,可以看到比JDK少了很多
➜ .\image\tweet\bin\java --list-modules
jackson.annotations
jackson.core
jackson.databind
java.base@17.0.7
java.logging@17.0.7
java.sql@17.0.7
java.transaction.xa@17.0.7
java.xml@17.0.7
tweet
也可以将我们的应用打成 jar 包再进行 jlink
jar --create --file ./lib/tweet.jar -C ./mods/tweet .
jlink --module-path "./lib/" --add-modules tweet --output ./image/tweet
制作的镜像包含了全部程序,我们可以直接运行它
➜ Get-Content "tweet.json" -Encoding UTF8 | .\image\tweet\bin\java --module tweet/org.tweet.MyApp
jlink 参数选项
--launcher
应用程序模块可以包括一个自定义启动器,它是镜像中bin文件夹中的可执行脚本,预先配置为使用具体模块和主类启动JVM。需要使用到 --launcher 这个参数,格式如下:
--launcher $NAME=$MODULE/$MAIN-CLASS
# $NAME:为可执行文件选择的文件名
# $MODULE:要启动的模块的名称
# $MAIN-CLASS:模块主类的名称
如果模块定义了一个主类,则可以省略$MAIN-CLASS
➜ jlink --module-path "./lib/;./mods/tweet" --add-modules tweet `
--launcher app=tweet/org.tweet.MyApp --output ./image/tweet
效果就是生成的镜像包含一个可执行的启动脚本 app.bat,将此文件添加快捷方式即可双击启动我们的程序
maven-jlink-plugin
https://maven.apache.org/plugins/maven-jlink-plugin/jlink-mojo.html
maven-jlink-plugin 是 Apache Maven 的一个插件,封装了 jlink 工具,用于创建定制的 Java 运行时镜像(JRE)。
一般配置如下,详细使用见官网:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jlink-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>create-runtime</id>
<goals><goal>jlink</goal></goals>
<phase>package</phase>
<configuration>
<jlinkImageName>custom-runtime</jlinkImageName>
<launcher>
name=myapp
module=com.myapp/com.myapp.Main
</launcher>
<compression>2</compression>
<stripDebug>true</stripDebug>
<noManPages>true</noManPages>
<addModules>
java.base,
java.logging,
jdk.httpserver
</addModules>
</configuration>
</execution>
</executions>
</plugin>
注意事项
- 模块化要求:应用及其依赖必须支持JPMS
- 反射问题:需要为反射访问的包添加
opens语句 - 服务加载:使用
bindServices选项处理ServiceLoader - 多平台构建:建议在CI中为各目标平台分别构建
jpackage
jpackage 是 Java 提供的一个跨平台的打包工具,自 JDK 14 开始引入。通过 jpackage,可以将 Java 应用程序和所需的 JRE 打包成一个独立的、可执行的安装包,方便分发和部署。
jpackage 会使用 jlink 工具创建一个包含仅应用程序所需模块的精简 JRE。这个 JRE 将被打包进最终的应用程序包中。
使用方法参考:
常用命令集合
Unix/Linux:
# 判断jar包是否模块化
jar tf /path/to/xxx.jar | grep "moudle-info.class"
# 解压jar包
jar xf /path/to/xxx.jar ./
# 将把当前目录下的所有文件和子目录压缩到 newfile.jar 中
jar cf xxx.jar *
# 压缩多个目录
jar cf xxx.jar src/ lib/
# 压缩某些特定类型的文件,可以使用通配符
jar cf xxx.jar src/*.java
# 压缩多个目录和文件
jar cf xxx.jar src/ lib/ README.md
Windows:
# 判断jar包是否模块化, 是否包含module-info.class文件
jar tf yourfile.jar | findstr "module-info.class"
执行:
java -Xdiag:resolver --module-path mlib --add-modules jackson.databind
\ -cp tweetsum.jar org.tweetsum.Main <tweet.json

浙公网安备 33010602011771号