关于java:SpringBoot内嵌JAR免解压加载原理

30次阅读

共计 4282 个字符,预计需要花费 11 分钟才能阅读完成。

SpringBoot 内嵌 JAR 免解压加载原理

起因

因为我的项目环境基于 Felix(一个 apache 的开源 OSGi 实现框架),Felix 框架在解决蕴含内嵌 JAR 包的构件时,须要将内嵌 JAR 解压缩到本地 cache 目录,能力拜访这些内嵌 JAR 资源。因为内嵌 JAR 数量宏大,会额定占用约 500M 左右的反复磁盘空间。

一个蕴含内嵌 JAR 的 OSGi 构件大略长这个样子:

回想到 Springboot 在打包时,能够同样将我的项目所有依赖打包到独自的一个 JAR 中,应用 java -jar test.jar 这种模式间接运行。这为我的项目的散发带来了极大的不便。
一个 SpringBoot 繁多 JAR 文件的目录构造和 OSGi 内嵌 JAR 构造相仿:

这其中,BOOT-INF/classes、BOOT-INF/lib/*.jar 是咱们利用的 classs-path 的内容。咱们察看到 SpringBoot 在运行期间,并不需要将 lib 下的 jar 包解压缩到本地磁盘目录,就能够间接拜访内嵌 JAR 中的 classs,这是怎么做到的呢?

探索

因为 Java 对 class 的加载是随需加载的,即:应用程序启动期间,不会也没有必要把 JAR 包内的所有 class 字节内容读取到内存中;而是随着程序的运行,当须要用到某一个 class 的性能时,才从 class-path 上搜查该 class 的字节码,再加载到内存中。

咱们晓得,从文件存储格局上看,JAR 文件实质上就是 Zip 文件格式(只是 JAR 约定 META-INF/MANIFEST.MF 文件必须是压缩包的第一个 entry)。

那么,Zip 文件为什么能够实现对 Entry 的随机读取呢?这取决于 Zip 文件的存储构造。Zip 文件是由一个个 Entry 数据程序重叠起来的,在 Zip 文件最初,存储了所有 Entry 的目录信息,其中标识了每一个 Entry 在 Zip 文件中的偏移地位。因为 Zip 文件是对每一个 Entry 独自压缩(每一个 Entry 能够抉择是否压缩、以及压缩模式),而不是对所有 Entry 一起压缩 – 这很要害!所以,通过 Entry 目录能够定位到任何一个 Entry,从而实现 Entry 数据的随机读取。

咱们再看 JDK 的 Zip 相干 API。JDK 是通过本身 java.util.zip.ZipFile 这个类来实现对 JAR 中的 class 或资源的随机读取的。这个类须要一个本地 File 做结构函数参数,同时不反对内嵌 JAR 包中 class 资源的读取。

SpringBoot 的实现

spring-boot-loader是 SpringBoot 的疏导程序,当你应用 Maven-Install 打包一个 SpringBoot 繁多 JAR 包时,SpringBootLoader 的代码被拷贝到 JAR 中。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
    <version>2.6.6</version>
</dependency>

SpringBoot 繁多 JAR 包 MANIFEST 内容:

Manifest-Version: 1.0
Implementation-Title: SpringBootDemo
Spring-Boot-Version: 2.0.6.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: springboot2.DemoApp
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/

咱们看到,SpringBootLoader:

  • 接管了 SpringBoot 程序的启动入口(Main-Class)
  • 对 JDK 中 java.util.zip.ZipFile 实现了扩大,以便反对内嵌 JAR 中 class 的随机拜访
  • 提供了 SpringBoot-ClassLoader 负责 BOOT-INF 下 classes、lib 的资源加载(Spring-Boot-Classes、Spring-Boot-Lib 内容形成了利用真正的 class-path)

应用 SpringBootLoader 的 Zip 扩大来随机读取内嵌 JAR 的资源

咱们打包一个 SpringBootJar,如果名字叫:SpringBootDemo-1.0-boot.jar,其中,在 BOOT-INF/lib 目录下存在一个内嵌 JAR:logback-core-1.2.3.jar,咱们来尝试读取这个内嵌 JAR 中的 class 文件:ch.qos.logback.core.Appender.class

示例代码如下:

import org.springframework.boot.loader.jar.JarFile;

public class Demo {public static void main(String[] args) throws Exception {JarFile rootJar = new JarFile(new File("SpringBootDemo-1.0-boot.jar"));
        String innerName = "BOOT-INF/lib/logback-core-1.2.3.jar";
        String className = "ch.qos.logback.core.Appender";

        ZipEntry entry = rootJar.getEntry(innerName);
        JarFile innerJar = rootJar.getNestedJarFile(entry);

        String classEntryName = className.replace('.', '/') + ".class";
        entry = innerJar.getEntry(classEntryName);
        InputStream in = innerJar.getInputStream(entry);
        byte[] bs = new byte[(int) entry.getSize()];
        for (int i = 0; i < bs.length;) {i += in.read(bs, i, bs.length - i);
        }
        in.close();
        System.out.println("entry name:" + entry.getName());
        System.out.println("entry time:" + entry.getTime());
        System.out.println("class File first byte: 0x" + Integer.toHexString(bs[0] & 0xFF));

        rootJar.close();}
}

执行后果如下:咱们看到已胜利读取 Appender.class 文件内容。

entry name: ch/qos/logback/core/Appender.class
entry time: 1490962798000
class File first byte: ca

OSGi 构件内嵌 JAR 的读取尝试

然而,当咱们尝试用同样的办法,去读取一个 OSGi 构件内嵌 JAR 资源时候,呈现了谬误:

Unable to open nested entry 'lib/cdi-api-1.0.jar'. It has been compressed and nested jar files must be stored without compression. Please check the mechanism used to create your executable jar file
    at org.springframework.boot.loader.jar.JarFile.createJarFileFromFileEntry(JarFile.java:332)

至此,SpringBootLoader 在不解压内嵌 JAR 包时就能够读取其内容的要害就在于:内嵌 JAR 包必须以 STORED 形式存储!

其实,想一下也容易明确,只有内嵌 JAR 应用 STORED 模式存储时(即:非压缩,另一个模式是:DEFLATED),其内嵌 JAR 的子资源能力通过地址偏移在不应用压缩算法的状况下进行定位。一旦应用了压缩算法,就必须残缺的将内嵌 JAR 包进行解压缩能力获取内嵌子资源的地位,这须要在内存中持有整个解压缩的数据,显然是不可承受的。

解决方案

明确了 SpringBootLoader 读取内嵌 JAR 资源的要害(内嵌 JAR 以 STORED 模式存储),那么对应的 OSGi 构件内嵌 JAR 资源的读取计划也就有了。

OSGi 构件是通过 maven-bundle-plugin 来进行打包的,须要对其进行批改、或者在其后再减少一个新的插件,实现对内嵌 JAR 包的存储模式的批改。要害代码示例如下;

public static void main(String[] args) throws Exception {File innerjar = new File("test.jar");
    byte[] bs = Files.readAllBytes(innerjar.toPath());

    ZipOutputStream jarout = new ZipOutputStream(new FileOutputStream("fat.jar"));
    Manifest manifest = new Manifest();
    manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
    manifest.getMainAttributes().putValue("Bundle-ClassPath", ".,lib/test.jar");
    ZipEntry entry = new ZipEntry(JarFile.MANIFEST_NAME);
    jarout.putNextEntry(entry);
    manifest.write(jarout);
    jarout.closeEntry();

    entry = new ZipEntry("lib/test.jar");
    entry.setMethod(ZipEntry.STORED);
    entry.setSize(bs.length);
    entry.setCrc(0x6baaa6bL);
    jarout.putNextEntry(entry);
    jarout.write(bs);
    jarout.closeEntry();
    jarout.close();}

参考资料

  • 压缩包 Zip 格局详析

正文完
 0