
本文旨在指导读者如何利用 java 19 的 `jlink` 工具为 spring boot 3.0 应用创建精简的自定义运行时环境。通过详细分析 `jdeps` 输出,识别并添加 spring boot 应用程序所需的 jdk 模块,解决了因模块缺失导致的 `noclassdeffounderror` 问题,从而实现更小、更高效的部署包。
引言:jlink 与 Spring Boot 应用优化
随着 Java 平台模块系统(JPMS)的引入,jlink 工具为开发者提供了构建自定义运行时镜像的能力,这对于部署云原生应用或需要极小运行时环境的场景尤为有益。通过 jlink,我们可以将应用程序及其所需的 JDK 模块打包成一个独立的、精简的运行时环境,从而减少部署包大小、提升启动速度。
本教程将以一个基于 Java 19 和 Spring Boot 3.0 的简单应用为例,演示如何从零开始,逐步分析依赖,并最终成功构建一个可运行的自定义运行时镜像。
1. Spring Boot 应用基础设置
首先,我们创建一个基本的 Spring Boot 3.0 应用。可以通过 start.spring.io 生成一个 Maven 项目,并添加 commons-lang3 依赖。
pom.xml 示例:
4.0.0 org.springframework.boot spring-boot-starter-parent 3.0.0 com.example demo 0.0.1-SNAPSHOT demo Demo project for Spring Boot 19 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.apache.commons commons-lang3 org.springframework.boot spring-boot-maven-plugin
主应用类 DemoApplication.java:
package com.example.demo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
String mix = "MIX";
if (StringUtils.isNoneBlank(mix)) {
System.out.println(mix);
}
SpringApplication.run(DemoApplication.class, args);
}
}构建项目后,会在 target 目录下生成 demo-0.0.1-SNAPSHOT.jar 文件,可以直接通过 java -jar target/demo-0.0.1-SNAPSHOT.jar 运行。
2. 使用 jdeps 分析依赖模块
在构建自定义运行时之前,我们需要识别应用程序及其依赖项所需的 JDK 模块。jdeps 工具是实现这一目标的关键。
执行 jdeps target/demo-0.0.1-SNAPSHOT.jar 命令:
jdeps target/demo-0.0.1-SNAPSHOT.jar demo-0.0.1-SNAPSHOT.jar -> java.base demo-0.0.1-SNAPSHOT.jar -> java.logging demo-0.0.1-SNAPSHOT.jar -> not found com.example.demo -> java.io java.base com.example.demo -> java.lang java.base com.example.demo -> org.apache.commons.lang3 not found com.example.demo -> org.springframework.boot not found com.example.demo -> org.springframework.boot.autoconfigure not found com.example.demo -> org.springframework.context not found ... (其他 Spring Boot Loader 相关输出) ...
jdeps 输出解读:
- demo-0.0.1-SNAPSHOT.jar -> java.base 和 demo-0.0.1-SNAPSHOT.jar -> java.logging 表明应用程序直接或间接依赖于 java.base 和 java.logging 这两个 JDK 模块。
- not found 标识符出现在 org.apache.commons.lang3、org.springframework.boot 等依赖项旁边,这是因为 jdeps 在分析 Spring Boot 的“胖 JAR”时,默认将其内部的第三方库视为未找到的外部模块。对于 jlink 而言,我们主要关注应用程序对 JDK 模块的依赖。
- 更详细的 jdeps 输出会列出 org.springframework.boot.loader 等内部组件对 java.io, java.lang, java.util 等 java.base 模块内部包的依赖。
从初步分析来看,java.base 和 java.logging 是显而易见的必需模块。
3. 初次尝试 jlink 并分析错误
基于 jdeps 的初步结果,我们尝试构建一个包含 java.base 和 java.logging 的自定义运行时:
jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.logging --output mycustomrt
这条命令会在当前目录下创建一个名为 mycustomrt 的自定义运行时环境。
现在,尝试使用这个自定义运行时来运行 Spring Boot 应用:
mycustomrt/bin/java -jar target/demo-0.0.1-SNAPSHOT.jar
不出所料,应用程序启动失败,并抛出了 java.lang.NoClassDefFoundError: java/beans/PropertyEditorSupport 错误:
java.lang.NoClassDefFoundError: java/beans/PropertyEditorSupport
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1013)
...
at org.springframework.boot.context.properties.bind.BindConverter$TypeConverterConverter.(BindConverter.java:180)
...
Caused by: java.lang.ClassNotFoundException: java.beans.PropertyEditorSupport
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445)
... 这个错误非常关键,它表明我们的自定义运行时缺少 java.beans.PropertyEditorSupport 类。java.beans 包中的类通常与 Java 的 JavaBeans 规范相关,虽然它在较旧的 Java 版本中可能部分属于 java.base,但在现代 Java 版本中,尤其是涉及 GUI 或更广泛的工具支持时,它往往归属于 java.desktop 模块。Spring Boot 即使不直接涉及桌面 GUI,其内部的属性绑定、类型转换等机制也可能间接依赖 java.beans 中的一些工具类。
4. 修正 jlink 模块列表
为了解决 NoClassDefFoundError,我们需要将包含 java.beans 包的模块添加到自定义运行时中。经过排查,java.beans.PropertyEditorSupport 位于 java.desktop 模块。
此外,考虑到 Spring Boot 应用的常见需求,一些其他常用模块也可能被间接依赖,例如:
- java.xml: 用于 XML 处理,Spring 框架中常用。
- java.sql: 用于数据库连接和操作。
- java.prefs: 用于用户偏好设置。
因此,我们将这些常用且可能被 Spring Boot 间接依赖的模块一并加入。
修正后的 jlink 命令:
jlink --module-path $JAVA_HOME/jmods --add-modules java.base,java.logging,java.xml,java.sql,java.prefs,java.desktop --output mycustomrt
执行此命令后,一个新的 mycustomrt 运行时环境将被创建。
5. 验证自定义运行时
现在,再次使用新生成的自定义运行时来运行 Spring Boot 应用:
mycustomrt/bin/java -jar target/demo-0.0.1-SNAPSHOT.jar
此时,应用程序应该能够成功启动并正常运行,输出 Spring Boot 的启动日志和应用程序的自定义消息:
MIX . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.0.0) 2022-11-30T19:47:53.468+01:00 INFO 18179 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication v0.0.1-SNAPSHOT using Java 19.0.1 with PID 18179 (/home/me/NetBeansProjects/demo/target/demo-0.0.1-SNAPSHOT.jar started by neblaz in /home/me/NetBeansProjects/demo) ... (其他 Spring Boot 启动日志) ...
这表明我们已经成功为 Spring Boot 3.0 应用构建了一个功能完整的自定义运行时环境。
6. 注意事项与最佳实践
- 迭代式模块发现: 对于复杂的 Spring Boot 应用,仅凭 jdeps 的一次运行可能无法完全列出所有必需的 JDK 模块。通常需要一个迭代过程:先添加最基本的模块,运行应用,根据 NoClassDefFoundError 提示补充缺失的模块,直到应用正常启动。
- Spring Boot Fat Jar 的特殊性: Spring Boot 的可执行 JAR 是一个“胖 JAR”,它包含了所有应用程序代码和依赖项。jlink 创建的运行时环境只包含 JDK 模块,而应用程序的第三方依赖仍然由 Spring Boot 的内部加载器处理。
- 更全面的 jdeps 用法: 对于更深入的模块依赖分析,可以尝试 jdeps --recursive --list-deps target/demo-0.0.1-SNAPSHOT.jar 或 jdeps --list-modules --recursive target/demo-0.0.1-SNAPSHOT.jar。然而,对于胖 JAR,这些命令可能仍然无法完全自动化地识别所有间接 JDK 模块依赖,因为它们主要关注 JAR 文件本身的模块信息,而非运行时反射或动态加载的需求。
- 自动化构建: 在实际项目中,建议将 jlink 过程集成到 Maven 或 Gradle 构建生命周期中,例如使用 maven-jlink-plugin 或 gradle-jlink-plugin,以自动化自定义运行时镜像的生成。
- GraalVM Native Image: 对于追求极致启动速度和更小部署包的应用,可以考虑使用 GraalVM Native Image 技术。它能将 Spring Boot 应用编译成独立的本地可执行文件,进一步减小体积并显著提升启动速度,但配置过程相对复杂。
总结
通过本教程,我们学习了如何利用 jlink 工具为 Spring Boot 3.0 应用构建精简的自定义运行时环境。关键步骤包括:
- 使用 jdeps 分析应用程序对 JDK 模块的初步依赖。
- 尝试构建初始运行时并运行应用程序。
- 根据运行时错误(如 NoClassDefFoundError)识别并补充缺失的 JDK 模块。
- 重新构建运行时并验证应用程序的正常运行。
通过这种方式,开发者可以为 Spring Boot 应用创建更小、更高效的部署包,这对于优化资源利用和提升部署效率具有重要意义。










