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 格局详析