JVM CDS动态归档在Spring Boot多层JAR结构中Class-Path通配符未展开?JarLauncher与Archive.getNestedArchives()

内容分享1周前发布
0 0 0

JVM CDS 动态归档在 Spring Boot 多层 JAR 结构中 Class-Path 通配符未展开:JarLauncher 与 Archive.getNestedArchives()

大家好,今天我们来深入探讨一个在 Spring Boot 多层 JAR 结构中使用 JVM CDS (Class Data Sharing) 动态归档时可能遇到的问题:Class-Path 通配符未展开,以及这与
JarLauncher

Archive.getNestedArchives()
的行为之间的关系。这个问题会直接影响 CDS 动态归档的效率,甚至可能导致归档失败。

1. 背景知识:Spring Boot 多层 JAR 结构与 CDS

首先,我们需要了解 Spring Boot 的多层 JAR 结构以及 CDS 的基本概念。

1.1 Spring Boot 多层 JAR 结构

Spring Boot 为了方便模块化和增量更新,通常会将应用程序打包成一个可执行 JAR,其内部采用嵌套 JAR 的结构。这种结构一般包含以下几个部分:

BOOT-INF/classes: 存放应用程序自身的
.class
文件。BOOT-INF/lib: 存放应用程序依赖的第三方 JAR 包。META-INF/MANIFEST.MF: 清单文件,包含应用程序的元数据,例如 Main-Class 和 Class-Path。


MANIFEST.MF
中的
Class-Path
属性用于指定应用程序运行时需要加载的 JAR 包路径。在多层 JAR 结构中,
Class-Path
通常会使用相对路径,指向
BOOT-INF/lib
目录下的 JAR 包。

1.2 JVM CDS (Class Data Sharing)

CDS 是一种 JVM 优化技术,旨在减少应用程序的启动时间和内存占用。它通过将已经加载的类数据预先存储在一个归档文件中,在下次启动时直接从归档文件中加载类数据,从而避免了重新解析和验证类的过程。

CDS 分为静态 CDS 和动态 CDS 两种。

静态 CDS: 在应用程序构建时,通过
jlink
工具生成包含系统类和应用程序类的归档文件。动态 CDS: 在应用程序运行时,通过 JVM 参数
-XX:DumpLoadedClassList=<listfile>

-Xshare:dump
生成包含应用程序类的归档文件。然后,在下次启动时使用
-Xshare:on

-Xshare:auto
加载归档文件。

动态 CDS 的优势在于它可以只归档应用程序实际使用的类,避免了静态 CDS 可能包含大量未使用类的缺点。

2. 问题描述:Class-Path 通配符未展开

在使用动态 CDS 时,可能会遇到一个问题:
MANIFEST.MF
文件中的
Class-Path
属性使用通配符时,JVM 在进行动态归档时可能无法正确展开通配符,导致只有部分类被归档,或者根本无法进行归档。

例如,
MANIFEST.MF
文件中可能包含以下内容:



Manifest-Version: 1.0
Main-Class: com.example.Application
Class-Path: BOOT-INF/lib/*.jar

在这种情况下,JVM 在执行
-Xshare:dump
时,期望能将
BOOT-INF/lib
目录下所有的 JAR 包都加入到类路径中,并归档这些 JAR 包中的类。然而,实际情况是,JVM 可能只将
BOOT-INF/lib/*.jar
作为一个字面字符串添加到类路径中,而不是展开成
BOOT-INF/lib
目录下所有 JAR 包的列表。

这会导致以下问题:

只有部分类被归档: 只有应用程序直接依赖的类(即不在
BOOT-INF/lib
下的类)会被归档,而
BOOT-INF/lib
下的类则不会被归档。归档失败: 如果应用程序依赖的类都在
BOOT-INF/lib
下,那么 JVM 可能会因为找不到应用程序的主类而导致归档失败。后续启动速度慢: 因为没有充分利用 CDS,后续启动时仍然需要重新加载和验证大量的类,启动速度无法得到有效提升。

3. 原因分析:JarLauncher 与 Archive.getNestedArchives()

要理解这个问题的原因,我们需要了解 Spring Boot 的
JarLauncher
以及
Archive.getNestedArchives()
方法的作用。

3.1 JarLauncher


JarLauncher
是 Spring Boot 用于启动可执行 JAR 的一个类。它的主要作用是:

解析
MANIFEST.MF
文件,获取应用程序的元数据,例如 Main-Class 和 Class-Path。根据 Class-Path 构建类加载器。启动应用程序的主类。


JarLauncher
在构建类加载器时,会解析
MANIFEST.MF
中的
Class-Path
属性,并将
Class-Path
中指定的 JAR 包添加到类加载器的搜索路径中。 但是,
JarLauncher
自身并不会展开 Class-Path 中的通配符。

3.2 Archive.getNestedArchives()


Archive.getNestedArchives()
是 Spring Boot
Archive
接口的一个方法,用于获取嵌套的 JAR 包。Spring Boot 在解析可执行 JAR 时,会将
BOOT-INF/lib
目录下的 JAR 包视为嵌套的 JAR 包。



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

Archive
接口的一个实现,用于表示 JAR 文件。
JarFileArchive

getNestedArchives()
方法会遍历 JAR 文件中的所有条目,并根据指定的
EntryFilter
过滤出嵌套的 JAR 包。

3.3 问题的原因

问题的根源在于 JVM 在执行动态 CDS 时,并没有像
JarLauncher
那样解析
MANIFEST.MF
文件,并展开
Class-Path
中的通配符。而是直接使用
MANIFEST.MF
中字面字符串作为类路径。

Spring Boot 使用
JarLauncher
启动应用程序时,会调用
Archive.getNestedArchives()
方法来获取嵌套的 JAR 包,并添加到类加载器中。但这个过程发生在 JVM 动态 CDS 归档之前。 因此,JVM 动态 CDS 只能看到
MANIFEST.MF
文件中的
Class-Path: BOOT-INF/lib/*.jar
,而无法看到
BOOT-INF/lib
目录下所有 JAR 包的列表。

4. 解决方案

解决这个问题的方法主要有两种:

4.1 修改 MANIFEST.MF 文件

最直接的解决方案是修改
MANIFEST.MF
文件,将
Class-Path
中的通配符替换为
BOOT-INF/lib
目录下所有 JAR 包的完整列表。

例如,如果
BOOT-INF/lib
目录下包含
a.jar

b.jar

c.jar
三个 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
文件,当
BOOT-INF/lib
目录下的 JAR 包发生变化时,需要手动更新
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>

这个配置会自动将
BOOT-INF/lib
目录下的所有 JAR 包添加到
MANIFEST.MF
文件的
Class-Path
属性中。

4.2 使用 -cp 或 -classpath 参数

另一种解决方案是在执行
java
命令时,使用
-cp

-classpath
参数显式指定类路径。

例如:


java -cp "app.jar:BOOT-INF/lib/*" -XX:DumpLoadedClassList=classes.lst -Xshare:dump

其中,
app.jar
是 Spring Boot 可执行 JAR 的名称,
BOOT-INF/lib/*
表示
BOOT-INF/lib
目录下的所有 JAR 包。

这种方法的优点是无需修改
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 已生成."

使用方法:


your-app.jar
替换成你的 Spring Boot JAR 包的名字。保存脚本为
dump_cds.sh
,并赋予执行权限 (
chmod +x dump_cds.sh
)。运行脚本:
./dump_cds.sh

这个脚本会自动构建包含所有
BOOT-INF/lib
下 JAR 包的 Classpath,并执行
java -cp
命令来进行 CDS dump。 后续的启动命令也需要使用相同的 classpath 构建方式。

5. 代码示例:验证 Class-Path 是否正确展开

为了验证
Class-Path
是否正确展开,我们可以编写一个简单的 Java 程序,打印出类加载器的搜索路径。



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 应用程序中。然后,在应用程序启动时,运行这个程序,查看输出的类路径是否包含
BOOT-INF/lib
目录下的所有 JAR 包。

5.1 如何集成到 Spring Boot 应用?

创建 Java 类: 将上面的
ClasspathChecker.java
放到你的 Spring Boot 项目的源代码目录下,例如
src/main/java/com/example/ClasspathChecker.java
修改 Application 类 (或者创建一个 CommandLineRunner): 在你的 Spring Boot 应用的主类 (
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.");
        }
    }
}

解释:


@Component
: 将这个类注册为 Spring Bean,Spring Boot 会自动管理它的生命周期。
CommandLineRunner
: Spring Boot 应用启动后,
run
方法会被自动执行。
getClass().getClassLoader()
: 获取当前类的类加载器。剩下的代码和之前的
ClasspathChecker
是一样的,用于打印 Classpath。

重新构建 Spring Boot 应用: 使用 Maven 或 Gradle 重新构建你的 Spring Boot 应用。运行 Spring Boot 应用: 运行 Spring Boot 应用,查看控制台输出。你应该能看到
Classpath URLs:
后面跟着一系列 URL,这些 URL 代表了你的 Classpath。 检查其中是否包含你期望的
BOOT-INF/lib
下的 JAR 包。

通过这个例子,我们可以明确地看到 Spring Boot 应用运行时使用的 Classpath,从而验证我们的配置是否正确,以及 JVM 在 CDS 归档时是否能正确识别这些 Classpath。

6. 注意事项

在使用动态 CDS 时,建议始终显式指定类路径,避免依赖
MANIFEST.MF
文件中的
Class-Path
属性。在 Linux/Unix 系统上,可以使用
find
命令和
xargs
命令来生成包含所有 JAR 包的类路径。在 Windows 系统上,可以使用 PowerShell 脚本来生成包含所有 JAR 包的类路径。在测试 CDS 效果时,建议多次启动应用程序,并观察启动时间的变化。

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 动态归档的原理和使用方法,并在实际项目中避免类似的问题。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...