您的位置 首页 java

为什么SpringBoot可以直接运行 Jar 包?看完这篇文章我瞬间秒懂

小伙伴们好呀,今天咋们来探索下,为什么 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 的问题。

原文链接:

文章来源:智云一二三科技

文章标题:为什么SpringBoot可以直接运行 Jar 包?看完这篇文章我瞬间秒懂

文章地址:https://www.zhihuclub.com/177336.shtml

关于作者: 智云科技

热门文章

网站地图