Spring Boot 解析(二):FatJar 启动原理

Spring boot 系列,主要通过解读 Spring Boot 源码, 分析其内部实现机制。本文该系列的第二篇,主要带你解析 Spring Boot 可运行 jar 启动背后的原理。

Spring Boot 应用使用 spring-boot-maven-plugin 快速打包,构建一个可执行 jar。Spring Boot 内嵌容器,通过 java -jar 命令便可以直接启动应用,可以部署到生产环境。

虽然是一个简单的启动命令,背后却藏着很多知识,所谓 一花一世界,一叶一森林 。今天带着大家探索 FAT JAR 启动的背后原理。本文主要包含以下几个部分:

  • JAR 是什么。首先需要了解 jar 是什么,才知道 java -jar 做了什么事情。

  • FatJar 有什么不同。   Spring Boot 提供的可执行 jar 与普通的 jar 有什么区别。

  • 启动时的类加载原理。 启动过程中类加载器做了什么?Spring Boot 又如何通过自定义类加载器解决内嵌包的加载问题。

  • 启动的整个流程。最后整合前面三部分的内容,解析源码看如何完成启动。

JAR 是什么 

看过同事写了一篇关于 JAR 的介绍,里面的一句话深有感触。“ 在 JAVA 语言这个圈子里摸爬滚打,除了对这个语言层面和框架层面的学习之外,有一些东西一直存在,但确实没有对它们有足够的重视,因为都觉得是理所应当的,比如JAR 是什么?

JAR文件(Java归档,英语: J ava AR chive)是一种软件包文件格式,通常用于聚合大量的Java类文件、相关的元数据和资源(文本、图片等)文件到一个文件,以便分发Java平台应用软件或库。简单点理解其实就是一个压缩包。既然是压缩包那么为了提取JAR文件的内容,可以使用任何标准的unzip解压缩软件提取内容。或者使用Java虚拟机自带命令 jar -xf foo.jar来解压相应的jar文件。

JAR 可以简单分为两类:

  • 非可执行 JAR。这类就是平时依赖的类库。

  • 可执行 JAR。这类 JAR 可以通过 java -jar 启动。如 Spring Boot 编译的 JAR 就属于这一类。

不管是非可行 JAR 和 可执行 JAR 解压后都包含两部分:META-INF 目录(元数据)和 package 目录(编译后的class)。这种普通的 jar 不包含第三方依赖包,只包含应用自身的配置文件、class 等。

不管是怎么类型的jar,都会有 MANIFEST.MF文件定义了包信息。首先看下非可执行 JAR guava 的 MANINFEST.MF 内容:

自己构建一个可执行 jar 解压后的 MANINFEST.MF 的内容是:

可执行JAR 的 MANINFEST.MF 文件比非执行jar多了一个 Main-Class, 这个配置表示jar 执行的路口类,当执行 java -jar xxx.jar 时会加载 Main-Class 指定的类执行。

FatJar 有什么不同

一般普通的 jar 只包含当前 jar的信息,不含有第三方 jar。当内部依赖第三方 jar 时,直接运行则会报错,这时候需要将第三方 jar 内嵌到 可执行 jar 里。将一个jar及其依赖的三方jar全部打到一个包中,这个包即为 FatJar

Spring Boot 为了解决内嵌 jar 问题,提供了一套 FatJar 解决方案,分别定义了 jar目录结构和 MANIFEST.MF。在编译生成可执行 jar 的基础上,使用spring-boot-maven-plugin 按Spring Boot 的可执行包标准 repackage,得到可执行的Spring Boot jar。根据可执行 jar 类型,分为两种: 可执行 Jar可执行 war

Spring Boot 项目被编译以后,在 targert 目录下存在两个jar文件:一个是 xxx.jar  和 xxx.jar.original,例如:

xxx.jar.original 是 maven 编译后的原始jar 文件,即标准的java jar。该文件仅包含应用本地资源。 如果单纯使用这个jar,无法正常运行,因为缺少依赖的第三方资源。因此 spring-boot-maven-plugin 插件对这个 xxx.jar.original 再做一层加工,引入第三方依赖的 jar 包等资源,将其 ” repackage ” 为 xxx.jar。可执行 Jar 的文件结构如下图所示:

  • META-INF: 存放元数据。MANIFEST.MF 是 jar 规范,Spring Boot 为了便于加载第三方 jar 对内容做了修改。

  • org: 存放Spring Boot 相关类,比如启动时所需的 Launcher 等。

  • BOOT-INF/class:存放应用编译后的 class 文件。

  • BOOT-INF/lib:存放应用依赖的 JAR 包。

Spring Boot 的 MANIFEST.MF 和普通jar 有些不同。

Main-Class: 是java -jar 启动引导类,但这里不是项目中的类,而是Spring Boot 内部的 JarLauncher。

Start-Class:这个才是正在要执行的应用内部主类

所以 java -jar 启动的时候,加载运行的是 JarLauncher。Spring Boot 内部如何通过 JarLauncher 加载 Start-Class 执行呢?为了更清楚加载流程,我们先介绍下 java -jar 是如何完成类加载逻辑的。

启动时的类加载原理

后面会有专门文章详细介绍类加载机制。 这里简单说下 java -jar 启动时是如何完成记载类加载的。 java 采用了双亲委派机制,Java语言系统自带有三个类加载器: 

  1. Bootstrap CLassloder:   最顶层的加载类,主要加载核心类库

  2. Extention ClassLoader: 扩展的类加载器,加载目录%JRE_HOME%/lib/ext目录下的jar包和class文件。 还可以加载-D java.ext.dirs选项指定的目录。

  3. AppClassLoader: 是应用加载器。

默认情况下通过 java -classpath,  java -cp, java -jar 使用的类加载器 都是 AppClassLoader。 普通可执行 jar 通过 java -jar 启动后,使用 AppClassLoader 加载 Main-class 类。 如果第三方jar 不在 AppClassLoader 里,会导致启动时候会报 ClassNotFoundException。

例如在 Spring Boot 可执行jar 的解压目录下,执行应用的主函数,就直接报该错误:

因为 java 命令未指明 classpath, 依赖的第三方jar 无法被加载。Spring Boot JarLauncher 启动的时,会将所有依赖的内嵌 jar (BOOT-INF/lib 目录下) 和 class (BOOT-INF/classes 目录)都加入到自定义的类加载器 LaunchedURLClassLoader中,并用这个 ClassLoder 去加载MANIFEST.MF 配置 Start-Class,则不会出现类找不到的错误。

LaunchedURLClassLoader 是 URLClassLoader 的子类, URLClassLoader  会通过URL[] 来搜索类所在的位置。Spring Boot 则将所需要的内嵌文档组装成 URL[],最终构建 LaunchedURLClassLoader 类。

启动的整个流程 

有了以上知识的铺垫,我们看下整个 FatJar 启动的过程会是怎样。 为了看源码可以在 pom.xml 引入下面的配置:

简单概括起来可以分为几步:

  • java -jar 启动,AppClassLoader 则会加载  MANIFEST.MF 配置的 Main-Class,  JarLauncher。

  • JarLauncher 启动时,注册 URL 关联协议。

  • 获取所有内嵌的存档(内嵌 jar 和  class)

  • 根据存档的 URL[] 构建 类加载器。

  • 然后用这个类加载器加载 Start-Class 。 保证这些类都在同一个 ClassLoader 中。

注册URL关联协议

因为FatJar 中的jar 都是内嵌的jar, 和普通 jar 有些不同。LaunchedURLClassLoader 是 URLClassLoader 类型的加载器,通过覆盖 jar 协议,使用自定义的jar  Handler。具体这块细节会放在类加载器里,敬请关注。

Spring Boot 解析系列文章