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.0Implementation-Title: SpringBootDemoSpring-Boot-Version: 2.0.6.RELEASEMain-Class: org.springframework.boot.loader.JarLauncherStart-Class: springboot2.DemoAppSpring-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.classentry time: 1490962798000class 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格局详析