JVM CDS动态归档在Spring Boot多层JAR结构中Class-Path通配符未展开?JarLauncher与Archive.getNestedArchives()
JVM CDS 动态归档在 Spring Boot 多层 JAR 结构中 Class-Path 通配符未展开:JarLauncher 与 Archive.getNestedArchives()
大家好,今天我们来深入探讨一个在 Spring Boot 多层 JAR 结构中使用 JVM CDS (Class Data Sharing) 动态归档时可能遇到的问题:Class-Path 通配符未展开,以及这与 和
JarLauncher 的行为之间的关系。这个问题会直接影响 CDS 动态归档的效率,甚至可能导致归档失败。
Archive.getNestedArchives()
1. 背景知识:Spring Boot 多层 JAR 结构与 CDS
首先,我们需要了解 Spring Boot 的多层 JAR 结构以及 CDS 的基本概念。
1.1 Spring Boot 多层 JAR 结构
Spring Boot 为了方便模块化和增量更新,通常会将应用程序打包成一个可执行 JAR,其内部采用嵌套 JAR 的结构。这种结构一般包含以下几个部分:
BOOT-INF/classes: 存放应用程序自身的 文件。BOOT-INF/lib: 存放应用程序依赖的第三方 JAR 包。META-INF/MANIFEST.MF: 清单文件,包含应用程序的元数据,例如 Main-Class 和 Class-Path。
.class
中的
MANIFEST.MF 属性用于指定应用程序运行时需要加载的 JAR 包路径。在多层 JAR 结构中,
Class-Path 通常会使用相对路径,指向
Class-Path 目录下的 JAR 包。
BOOT-INF/lib
1.2 JVM CDS (Class Data Sharing)
CDS 是一种 JVM 优化技术,旨在减少应用程序的启动时间和内存占用。它通过将已经加载的类数据预先存储在一个归档文件中,在下次启动时直接从归档文件中加载类数据,从而避免了重新解析和验证类的过程。
CDS 分为静态 CDS 和动态 CDS 两种。
静态 CDS: 在应用程序构建时,通过 工具生成包含系统类和应用程序类的归档文件。动态 CDS: 在应用程序运行时,通过 JVM 参数
jlink 和
-XX:DumpLoadedClassList=<listfile> 生成包含应用程序类的归档文件。然后,在下次启动时使用
-Xshare:dump 或
-Xshare:on 加载归档文件。
-Xshare:auto
动态 CDS 的优势在于它可以只归档应用程序实际使用的类,避免了静态 CDS 可能包含大量未使用类的缺点。
2. 问题描述:Class-Path 通配符未展开
在使用动态 CDS 时,可能会遇到一个问题: 文件中的
MANIFEST.MF 属性使用通配符时,JVM 在进行动态归档时可能无法正确展开通配符,导致只有部分类被归档,或者根本无法进行归档。
Class-Path
例如, 文件中可能包含以下内容:
MANIFEST.MF
Manifest-Version: 1.0
Main-Class: com.example.Application
Class-Path: BOOT-INF/lib/*.jar
在这种情况下,JVM 在执行 时,期望能将
-Xshare:dump 目录下所有的 JAR 包都加入到类路径中,并归档这些 JAR 包中的类。然而,实际情况是,JVM 可能只将
BOOT-INF/lib 作为一个字面字符串添加到类路径中,而不是展开成
BOOT-INF/lib/*.jar 目录下所有 JAR 包的列表。
BOOT-INF/lib
这会导致以下问题:
只有部分类被归档: 只有应用程序直接依赖的类(即不在 下的类)会被归档,而
BOOT-INF/lib 下的类则不会被归档。归档失败: 如果应用程序依赖的类都在
BOOT-INF/lib 下,那么 JVM 可能会因为找不到应用程序的主类而导致归档失败。后续启动速度慢: 因为没有充分利用 CDS,后续启动时仍然需要重新加载和验证大量的类,启动速度无法得到有效提升。
BOOT-INF/lib
3. 原因分析:JarLauncher 与 Archive.getNestedArchives()
要理解这个问题的原因,我们需要了解 Spring Boot 的 以及
JarLauncher 方法的作用。
Archive.getNestedArchives()
3.1 JarLauncher
是 Spring Boot 用于启动可执行 JAR 的一个类。它的主要作用是:
JarLauncher
解析 文件,获取应用程序的元数据,例如 Main-Class 和 Class-Path。根据 Class-Path 构建类加载器。启动应用程序的主类。
MANIFEST.MF
在构建类加载器时,会解析
JarLauncher 中的
MANIFEST.MF 属性,并将
Class-Path 中指定的 JAR 包添加到类加载器的搜索路径中。 但是,
Class-Path 自身并不会展开 Class-Path 中的通配符。
JarLauncher
3.2 Archive.getNestedArchives()
是 Spring Boot
Archive.getNestedArchives() 接口的一个方法,用于获取嵌套的 JAR 包。Spring Boot 在解析可执行 JAR 时,会将
Archive 目录下的 JAR 包视为嵌套的 JAR 包。
BOOT-INF/lib
public interface Archive extends Iterable<Entry> {
String getUrl();
String getName();
Iterator<Entry> iterator();
// 获取嵌套的 Archives
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
interface Entry {
boolean isDirectory();
String getName();
}
interface EntryFilter {
boolean matches(Entry entry);
}
}
是
JarFileArchive 接口的一个实现,用于表示 JAR 文件。
Archive 的
JarFileArchive 方法会遍历 JAR 文件中的所有条目,并根据指定的
getNestedArchives() 过滤出嵌套的 JAR 包。
EntryFilter
3.3 问题的原因
问题的根源在于 JVM 在执行动态 CDS 时,并没有像 那样解析
JarLauncher 文件,并展开
MANIFEST.MF 中的通配符。而是直接使用
Class-Path 中字面字符串作为类路径。
MANIFEST.MF
Spring Boot 使用 启动应用程序时,会调用
JarLauncher 方法来获取嵌套的 JAR 包,并添加到类加载器中。但这个过程发生在 JVM 动态 CDS 归档之前。 因此,JVM 动态 CDS 只能看到
Archive.getNestedArchives() 文件中的
MANIFEST.MF,而无法看到
Class-Path: BOOT-INF/lib/*.jar 目录下所有 JAR 包的列表。
BOOT-INF/lib
4. 解决方案
解决这个问题的方法主要有两种:
4.1 修改 MANIFEST.MF 文件
最直接的解决方案是修改 文件,将
MANIFEST.MF 中的通配符替换为
Class-Path 目录下所有 JAR 包的完整列表。
BOOT-INF/lib
例如,如果 目录下包含
BOOT-INF/lib、
a.jar 和
b.jar 三个 JAR 包,那么可以将
c.jar 文件修改为:
MANIFEST.MF
Manifest-Version: 1.0
Main-Class: com.example.Application
Class-Path: BOOT-INF/lib/a.jar BOOT-INF/lib/b.jar BOOT-INF/lib/c.jar
这种方法的优点是简单直接,但缺点是需要手动维护 文件,当
MANIFEST.MF 目录下的 JAR 包发生变化时,需要手动更新
BOOT-INF/lib 文件。
MANIFEST.MF
可以使用 Maven 或 Gradle 插件来自动生成包含完整 JAR 包列表的 文件。
MANIFEST.MF
例如,使用 Maven 的 插件可以实现这个功能:
maven-jar-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>BOOT-INF/lib/</classpathPrefix>
<mainClass>com.example.Application</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
这个配置会自动将 目录下的所有 JAR 包添加到
BOOT-INF/lib 文件的
MANIFEST.MF 属性中。
Class-Path
4.2 使用 -cp 或 -classpath 参数
另一种解决方案是在执行 命令时,使用
java 或
-cp 参数显式指定类路径。
-classpath
例如:
java -cp "app.jar:BOOT-INF/lib/*" -XX:DumpLoadedClassList=classes.lst -Xshare:dump
其中, 是 Spring Boot 可执行 JAR 的名称,
app.jar 表示
BOOT-INF/lib/* 目录下的所有 JAR 包。
BOOT-INF/lib
这种方法的优点是无需修改 文件,但缺点是需要在每次执行
MANIFEST.MF 命令时都显式指定类路径。 此外,需要注意的是,在某些操作系统上,
java 通配符可能无法正确展开,需要使用更具体的通配符或者手动列出所有 JAR 包。
*
更进一步的优化:编写脚本自动化
可以将这个过程封装成一个脚本,例如 Bash 脚本:
#!/bin/bash
APP_JAR="your-app.jar" # 你的 Spring Boot 可执行 JAR 名称
LIB_DIR="BOOT-INF/lib" # BOOT-INF/lib 目录
# 构建 classpath
CLASSPATH="$APP_JAR"
for jar in "$LIB_DIR"/*.jar; do
CLASSPATH="$CLASSPATH:$jar"
done
# 执行 dump
java -cp "$CLASSPATH" -XX:DumpLoadedClassList=classes.lst -Xshare:dump
echo "CDS dump 完成,classes.lst 已生成."
使用方法:
将 替换成你的 Spring Boot JAR 包的名字。保存脚本为
your-app.jar,并赋予执行权限 (
dump_cds.sh)。运行脚本:
chmod +x dump_cds.sh
./dump_cds.sh
这个脚本会自动构建包含所有 下 JAR 包的 Classpath,并执行
BOOT-INF/lib 命令来进行 CDS dump。 后续的启动命令也需要使用相同的 classpath 构建方式。
java -cp
5. 代码示例:验证 Class-Path 是否正确展开
为了验证 是否正确展开,我们可以编写一个简单的 Java 程序,打印出类加载器的搜索路径。
Class-Path
import java.net.URL;
import java.net.URLClassLoader;
public class ClasspathChecker {
public static void main(String[] args) {
ClassLoader cl = ClasspathChecker.class.getClassLoader();
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
System.out.println("Classpath URLs:");
for (URL url : urls) {
System.out.println(url.toString());
}
} else {
System.out.println("ClassLoader is not a URLClassLoader.");
}
}
}
将这个程序打包成一个 JAR 包,并添加到 Spring Boot 应用程序中。然后,在应用程序启动时,运行这个程序,查看输出的类路径是否包含 目录下的所有 JAR 包。
BOOT-INF/lib
5.1 如何集成到 Spring Boot 应用?
创建 Java 类: 将上面的 放到你的 Spring Boot 项目的源代码目录下,例如
ClasspathChecker.java。修改 Application 类 (或者创建一个 CommandLineRunner): 在你的 Spring Boot 应用的主类 (
src/main/java/com/example/ClasspathChecker.java) 中,或者创建一个实现了
Application.java 接口的类,加入以下代码来运行
CommandLineRunner。
ClasspathChecker
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.net.URL;
import java.net.URLClassLoader;
@Component
public class ClasspathCheckerRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
ClassLoader cl = getClass().getClassLoader();
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
System.out.println("Classpath URLs:");
for (URL url : urls) {
System.out.println(url.toString());
}
} else {
System.out.println("ClassLoader is not a URLClassLoader.");
}
}
}
解释:
: 将这个类注册为 Spring Bean,Spring Boot 会自动管理它的生命周期。
@Component: Spring Boot 应用启动后,
CommandLineRunner 方法会被自动执行。
run: 获取当前类的类加载器。剩下的代码和之前的
getClass().getClassLoader() 是一样的,用于打印 Classpath。
ClasspathChecker
重新构建 Spring Boot 应用: 使用 Maven 或 Gradle 重新构建你的 Spring Boot 应用。运行 Spring Boot 应用: 运行 Spring Boot 应用,查看控制台输出。你应该能看到 后面跟着一系列 URL,这些 URL 代表了你的 Classpath。 检查其中是否包含你期望的
Classpath URLs: 下的 JAR 包。
BOOT-INF/lib
通过这个例子,我们可以明确地看到 Spring Boot 应用运行时使用的 Classpath,从而验证我们的配置是否正确,以及 JVM 在 CDS 归档时是否能正确识别这些 Classpath。
6. 注意事项
在使用动态 CDS 时,建议始终显式指定类路径,避免依赖 文件中的
MANIFEST.MF 属性。在 Linux/Unix 系统上,可以使用
Class-Path 命令和
find 命令来生成包含所有 JAR 包的类路径。在 Windows 系统上,可以使用 PowerShell 脚本来生成包含所有 JAR 包的类路径。在测试 CDS 效果时,建议多次启动应用程序,并观察启动时间的变化。
xargs
7. CDS 归档之外的考量
除了 Classpath 问题,成功进行 CDS 归档还需要考虑其他因素,例如:
JVM 版本: 不同版本的 JVM 对 CDS 的支持程度可能不同。 确保你使用的 JVM 版本支持动态 CDS。内存设置: CDS 归档需要一定的内存空间。 如果内存不足,可能会导致归档失败。类加载器: 自定义类加载器可能会影响 CDS 的效果。 尽量使用默认的类加载器。AOT 编译: 如果使用了 GraalVM Native Image 等 AOT 编译技术,CDS 的效果可能会受到影响。
概括一下
本文深入探讨了 Spring Boot 多层 JAR 结构中使用 JVM CDS 动态归档时可能遇到的 Class-Path 通配符未展开的问题,分析了问题的原因,并提供了两种解决方案。 显式指定类路径或者使用Maven插件更新MANIFEST.MF是可行的办法。
希望本文能够帮助大家更好地理解 JVM CDS 动态归档的原理和使用方法,并在实际项目中避免类似的问题。