小伙伴们好呀,今天咋们来探索下,为什么 SpringBoot 的 jar 包可以直接运行? 以及 4ye 踩到的坑
目录如下
开始之前,先简单介绍下这个 jar
什么是 Jar
JAR 文件格式以流行的 ZIP 文件格式为基础。与 ZIP 文件不同的是,JAR 文件不仅用于压缩和发布,而且还用于部署和封装库、组件和插件程序,并可被像编译器和 JVM 这样的工具直接使用。在 JAR 中包含特殊的文件,如 manifests 和部署描述符,用来指示工具如何处理特定的 JAR。——《百度百科》
jar包结构图
这里小伙伴们可以自行查找下 jar文件规范
例如
规范中最重要的一点,就是 MATE-INF 文件夹中的 MANIFEST.MF 清单文件了。
文件内容如下
一眼看过去,这个 Main-Class 配置就特别突出了。 它指明了这个启动类的位置。
当我们用 java -jar xx.jar 命令运行一个 jar 包时,无外乎,它肯定是帮我们找到这个 main 方法,然后启动它。
(ps:使用 -jar 时,会忽略 classpath 环境的配置。)
这里我将上期 Map 专题的代码进行打包,运行效果如下
可以看到第一次运行时出现没有主清单属性的提示 。
在 pom 文件中添加 Main-class 配置(如下),即可解决问题。
复制<build>
<finalName>Map</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>cn.java4ye.HashMapMain</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-a-jar</id>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.
那么,到这里,基本的秘密已经被我们知道了。
执行 java -jar xx.jar 命令时,会去:
解析 MATE-INF 文件夹中的MANIFEST.MF 清单文件,
然后找到 Main-class ,运行其中的 main方法。
接着我们再反过来看看这个 Springboot 的 jar 包有啥不同。
官方文档参考
建议大家先去读读这个文档~
地址:#executable-jar
没看文档前,我想得很简单,直接就打开 Springboot 打包好的 jar 包去找 META-INF 文件夹下的 MANIFEST.MF
看到后,我的理解如下图
其他配置应该是表明这个 classes ,lib 去哪里找。
实际上呢,这个理解也没有错,但是里面多了很多细节~
比如:
- 嵌套的 jar 包要怎么解决
- classpath.idx 和 layers.idx 是用来干嘛的
- jarmode 又是什么
下面就让我们来了解探索下它的奥秘叭。(也顺便看看 4ye 掉到怎样的牛角尖去了叭)
META-INF
我们可以看到,Springboot 插件打包后生成的 jar 包和原来是有很大的不同的。
除了 META-INF 文件夹结构没变化之外。
BOOT-INF
结构如下
这里的重点在 classpath.idx 和 layers.idx 这两个索引文件。
classpath.idx 可以被 jar 和 war 包使用,它配置了哪些 jar 包要被加载到 classpath 中。
layers.idx 只能被 jar 包使用,在 创建镜像 的时候被使用,如 Docker /OCI (OCI是一种容器标准)
原话如下:
The layers.idx file can be used only for jars, and it allows a jar to be split into logical layers for Docker/OCI image creation.
官方文档中详细说明了这个 layers.idx 的规范。
比如 “dependencies” 是这部分 layer 的名称,下面的都属于这个 layer。
org
这里就是 Springboot 运行 jar 包的秘密了。
org文件夹
这里先介绍下一些背景,然后再简单看看 源码 ~
背景一
Fat Jar 指的就是这种 jar in jar , 或者说嵌套的 Jar 包。但是 Java 中并没有能加载嵌套 Jar 的方式,所以 Spring boot 自己写了这套代码,来解决这个问题。
当然,这句话是从 Springboot 官方文档中发现的
Java does not provide any standard way to load nested jar files (that is, jar File s that are themselves contained within a jar).
到了这里,我就掉入一个坑了。。 因为对这块不熟悉,我一直以为它说的就是 URLClassLoader 无法加载到这里面的class,可是我测试了好多遍,发现嵌套在里面的 class 可以被加载到呀(自己挖的坑,文末解答)
我翻了很多资料,还去查看 GitHub 的 issue,发现都没有人提过相关的问题
到了这里,我已经非常非常无奈了!这痛苦的感觉,就像刚开始学习编程时,装环境被各种奇奇怪怪的 bug 搞到渐渐没脾气。
被折磨了 N 久之后,我又找了另外一个角度,难道它说的是这个 JarFile 。我的天,我尝试了一下后,发现确实没有办法加载嵌套的 Jar
证明如下
背景二
URLStreamHandler ,它是用来处理各种协议的(比如 http,file,jar 等等),配合 URL,URLConnection 可以加载相应的资源。
比如 上面的例子中用到这个协议 jar:file:/xx.jar ,那么加载资源时,它就会使用这个 jdk jar 包下的 handler 来进行处理
关于协议的扩展可以看这里
而Springboot 也是重写了这个 handler 来处理嵌套的 jar 资源。
源码
核心代码便是这个 JarFile 以及下面要说到的Launcher 了。
根据 MANIFEST.MF 中指定的 main-class,我们可以发现如下代码
launch 方法如下
第一步,注册协议,registerUrl Protocol Handler
这里就涉及到这个 URLStreamHandler 机制协议了
我们可以通过 JVM 启动参数 -D java.protocol.handler.pkgs 来设置 URLStreamHandler 实现类的包路径
这里的代码也是通过这个系统参数将 URLStreamHandler 实现类的包路径 设置为 loader 包下的。
第二步是 创建 classLoader 。
这里可以看到这些 url 的格式如下
第三步是 判断是否有 jarmode 参数 。这个是和 docker 镜像相关的。
官方文档
#boot-features-container-images
用来简化提取 layer 的操作。
比如以前要写很多 copy ,而使用 jarmode 就会自动去 layers.idx 中提取了。(下期再写~)
由于我们没有添加该参数,所以这里是执行 getMainClass() 方法,来获取到这个启动类的。
第四步,运行 run 方法。
这里就创建 MainMethodRunner 类,并执行其中的 run 方法,去反射运行这个 main 方法了
小结一下
除了 JarLauncher 外,源码中的 WarLauncher 和 PropertiesLauncher 也拥有 main 方法,小伙伴们可以自己看看~
JarLauncher和 WarLauncher 都是继承这个ExecutableArchiveLauncher 的。
大坑
到了这里,我陷入了沉思
想到了之前写这个 AOP 插件( 《AOP 插件就这?上手不用两分钟!!》)时,好像也遇到了一点小困难和 jar 包相关的。再次翻看后,我发现我那会写了这么一个注释在 pom 文件中,去掉后打包确实报了这么一个错误。
“用 Springboot 插件打包,但是我们没有 main 方法,会报错,这里跳过就好了”
随后我又在之前的文章中看到了这么一段记录,这里也提到了 jar 包结构的变化 BOOT-INF/classes/ ,还有引出一个问题 ——“URLClassLoader 无法正确加载类,一直出现 ClassNotFoundException ”
到这里,我已经无限好奇了。然后就不自觉地掉进这个牛角尖里了……
为啥 URLClassLoader 就无法加载到这些类了呢?
我尝试用 URLClassLoader 去读取 Springboot 插件打包出来的 jar 包,发现 BOOT-INF/classes/ 下面的类一直无法读取到。但是这个 org 文件夹中的就可以正常加载。
同时我也发现一点不对劲! 没错,在最开始的证明这里,明明可以加载到 BOOT-INF/classes/ 下面的类呀!
于是我做了 n 遍验证,发现在插件篇中,这个 BOOT-INF/classes/ 下面的类一直无法读取到,会一直报错。这我就很纳闷了,直到我发现现在做 demo 的这个项目里,在 pom 文件中就引入了这个 jar 包!我的天~ 去掉之后它也一直报错了,坑死自己了
所以之前在 《AOP 插件就这?上手不用两分钟!!》 一文中提到的结论是没错的。而这也更加验证了 Java 中并没有能加载嵌套 Jar 的方式 ,所以 Springboot 才重写了它的。
而重写后,资源的路径文件夹路径变成下面这种方式了!!
上面这个图是 直到,我发现在 IDEA 中可以直接 debug jar 包 才得来的…… 不然现在还在那里卡着呢
总结
本期 思维导图 可以在这里获取
那么 Springboot jar 包为啥可以运行呢?
答:
执行 java -jar xx.jar 命令时,会去 解析MATE-INF 文件夹中的 MANIFEST.MF 清单文件,然后找到 Main-class ,反射运行其中的 main 方法。这个是最根本的原因。
而 Springboot maven 插件打包后的 jar 包结构有所变动,新增 org loader 代码目录和 BOOT-INF 目录,META-INF 目录不变,但是其中的 MANIFEST.MF 发生改变,其中新增 Start-Class 表示真正的启动类,而原本的 Main-Class 则指向JarLauncher , JarLauncher 启动时会去 注册协议,创建 ClassLoader,加载并反射运行 Start-Class 中的 main 方法,来启动程序。重写 Jar 协议是在 Spring boot loader源码中的 JarFile 中进行的,同时重新实现 URLStreamHandler 来解决 嵌套Jar 的问题。
原文链接: