硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!

Posted 芋道源码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!相关的知识,希望对你有一定的参考价值。

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 8:55 更新文章,每天掉亿点点头发...

源码精品专栏

 




摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/jar/ 「芋道源码」欢迎转载,保留摘要,谢谢!

  • 1. 概述
  • 2. MANIFEST.MF
  • 3. JarLauncher
  • 4. LaunchedURLClassLoader
  • 666. 彩蛋

大家好,我是艿艿,一个熬夜退役选手~

这两周很不顺心,接连的变化,一脸的懵逼。还好我还有 150000 女粉,我又充满能量的肝完了本文。

This browser does not support music or audio playback. Please play it in WeChat or another browser. 硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!

1. 概述

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以方便的将 Spring Boot 项目打成 jar 包或者 war 包。

考虑到部署的便利性,我们绝大多数 99.99% 的场景下,我们会选择打成 jar 包。这样,我们就无需在部署项目的服务器上,配置相应的 Tomcat、Jetty 等 Servlet 容器。

那么,jar 包是如何运行,并启动 Spring Boot 项目的呢?这个就是本文的目的,一起弄懂 Spring Boot jar 包的运行原理。

下面,我们来打开一个 Spring Boot jar 包,看看其里面的结构。如下图所示,一共分成四部分:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
Spring Boot jar
  • META-INF 目录:通过 MANIFEST.MF 文件提供 jar 包的元数据,声明了 jar 的启动类。

  • org 目录:为 Spring Boot 提供的 spring-boot-loader 项目,它是 java -jar 启动 Spring Boot 项目的秘密所在,也是稍后我们将深入了解的部分。

    Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using java -jar. Generally you will not need to use spring-boot-loader directly, but instead work with the Gradle or Maven plugin.

  • BOOT-INF/lib 目录:我们 Spring Boot 项目中引入的依赖的 jar 包们。spring-boot-loader 项目很大的一个作用,就是解决 jar 包里嵌套 jar 的情况,如何加载到其中的类。

  • BOOT-INF/classes 目录:我们在 Spring Boot 项目中 Java 类所编译的 .class、配置文件等等。

先简单剧透下,spring-boot-loader 项目需要解决两个问题:

  • 第一,如何引导执行我们创建的 Spring Boot 应用的启动类,例如上述图中的 Application 类。
  • 第二,如何加载 BOOT-INF/class 目录下的类,以及 BOOT-INF/lib 目录下内嵌的 jar 包中的类。

下面,尾随艿艿,一起来抽丝剥茧!

2. MANIFEST.MF

我们来查看 META-INF/MANIFEST.MF 文件,里面的内容如下:

Manifest-Version: 1.0
Implementation-Title: lab-39-demo
Implementation-Version: 2.2.2.RELEASE
Start-Class: cn.iocoder.springboot.lab39.skywalkingdemo.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.2.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

它实际是一个 Properties 配置文件,每一行都是一个配置项目。重点来看看两个配置项:

  • Main-Class 配置项:Java 规定的 jar 包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher 类,进行 Spring Boot 应用的启动。
  • Start-Class 配置项:Spring Boot 规定的 主启动类,这里设置为我们定义的 Application 类。

小知识补充:为什么会有 Main-Class/Start-Class 配置项呢?因为我们是通过 Spring Boot 提供的 Maven 插件 spring-boot-maven-plugin 进行打包,该插件将该配置项写入到 MANIFEST.MF 中,从而能让 spring-boot-loader 能够引导启动 Spring Boot 应用。

可能胖友会有疑惑,Start-Class 对应的 Application 类自带了 #main(String[] args) 方法,为什么我们不能直接运行会如何呢?我们来简单尝试一下哈,控制台执行如下:

$ java -classpath lab-39-demo-2.2.2.RELEASE.jar cn.iocoder.springboot.lab39.skywalkingdemo.Application
错误: 找不到或无法加载主类 cn.iocoder.springboot.lab39.skywalkingdemo.Application

直接找不到 Application 类,因为它在 BOOT-INF/classes 目录下,不符合 Java 默认的 jar 包的加载规则。因此,需要通过 JarLauncher 启动加载。

当然实际还有一个更重要的原因,Java 规定可执行器的 jar 包禁止嵌套其它 jar 包。但是我们可以看到 BOOT-INF/lib 目录下,实际有 Spring Boot 应用依赖的所有 jar 包。因此,spring-boot-loader 项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载 BOOT-INF/classes 目录下的 .class 文件,以及 BOOT-INF/lib 目录下的 jar 包。

3. JarLauncher

JarLauncher 类是针对 Spring Boot jar 包的启动类,整体类图如下所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
JarLauncher 类图

友情提示:WarLauncher 类,是针对 Spring Boot war 包的启动类,后续胖友可以自己瞅瞅,差别并不大哈~

JarLauncher 的源码比较简单,如下图所示:

public class JarLauncher extends ExecutableArchiveLauncher {

 static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

 static final String BOOT_INF_LIB = "BOOT-INF/lib/";

 public JarLauncher() {
 }

 protected JarLauncher(Archive archive) {
  super(archive);
 }

 @Override
 protected boolean isNestedArchive(Archive.Entry entry) {
  if (entry.isDirectory()) {
   return entry.getName().equals(BOOT_INF_CLASSES);
  }
  return entry.getName().startsWith(BOOT_INF_LIB);
 }

 public static void main(String[] args) throws Exception {
  new JarLauncher().launch(args);
 }

}

通过 #main(String[] args) 方法,创建 JarLauncher 对象,并调用其 #launch(String[] args) 方法进行启动。整体的启动逻辑,其实是由父类 Launcher 所提供,如下图所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
Launcher 启动过程

父类 Launcher 的 #launch(String[] args) 方法,代码如下:

// Launcher.java

protected void launch(String[] args) throws Exception {
 // <1> 注册 URL 协议的处理器
 JarFile.registerUrlProtocolHandler();
 // <2> 创建类加载器
 ClassLoader classLoader = createClassLoader(getClassPathArchives());
 // <3> 执行启动类的 main 方法
 launch(args, getMainClass(), classLoader);
}
  • <1> 处,调用 JarFile 的 #registerUrlProtocolHandler() 方法,注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于 jar 包的加载读取。
  • <2> 处,调用自身的 #createClassLoader(List<Archive> archives) 方法,创建自定义的 ClassLoader 实现类,用于从 jar 包中加载类。
  • <3> 处,执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。

简单来说,就是整一个可以读取 jar 包中类的加载器,保证 BOOT-INF/lib 目录下的类和 BOOT-classes 内嵌的 jar 中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。

下面,我们逐行代码来看看噢。即将代码多多,保持淡定,嘿嘿~

3.1 registerUrlProtocolHandler

友情提示:对应 JarFile.registerUrlProtocolHandler(); 代码段,不要迷路。

JarFile 是 java.util.jar.JarFile 的子类,如下所示:

public class JarFile extends java.util.jar.JarFile {

    // ... 省略其它代码

}

JarFile 主要增强支持对内嵌的 jar 包的获取。如下图所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
读取内嵌的 jar 包的演示

OK,介绍完之后,让我们回到 JarFile 的 #registerUrlProtocolHandler() 方法,注册 Spring Boot 自定义的 URL 协议的处理器。代码如下:

// JarFile.java

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";

private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";

/**
 * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
 * {@link URLStreamHandler} will be located to deal with jar URLs.
 */

public static void registerUrlProtocolHandler() {
    // 获得 URLStreamHandler 的路径
 String handlers = System.getProperty(PROTOCOL_HANDLER, "");
 // 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去
 System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
   : handlers + "|" + HANDLERS_PACKAGE));
 // 重置已缓存的 URLStreamHandler 处理器们
 resetCachedUrlHandlers();
}

/**
 * Reset any cached handlers just in case a jar protocol has already been used.
 * We reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
 * should have no effect other than clearing the handlers cache.
 *
 * 重置 URL 中的 URLStreamHandler 的缓存,防止 `jar://` 协议对应的 URLStreamHandler 已经创建
 * 我们通过设置 URLStreamHandlerFactory 为 null 的方式,清空 URL 中的该缓存。
 */

private static void resetCachedUrlHandlers() {
 try {
  URL.setURLStreamHandlerFactory(null);
 } catch (Error ex) {
  // Ignore
 }
}
  • 胖友先跟着注释,自己阅读下如上的代码~

目的很明确,通过将 org.springframework.boot.loader 包设置到 "java.protocol.handler.pkgs" 环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 jar: 协议的 URL。

友情提示:这里我们暂时不深入 Handler 的源码,避免直接走的太深,丢失了主干。后续胖友可结合《Java URL 协议扩展实现》文章,进行 Handler 的实现理解。

另外,HandlerTests 提供的单元测试,也是非常有帮助的~

3.2 createClassLoader

友情提示:对应 ClassLoader classLoader = createClassLoader(getClassPathArchives()) 代码段,不要迷路。

3.2.1 getClassPathArchives

首先,我们先来看看 #getClassPathArchives() 方法,它是由 ExecutableArchiveLauncher 所实现,代码如下:

// ExecutableArchiveLauncher.java

private final Archive archive;

@Override
protected List<Archive> getClassPathArchives() throws Exception {
 // <1> 获得所有 Archive
 List<Archive> archives = new ArrayList<>(
   this.archive.getNestedArchives(this::isNestedArchive));
 // <2> 后续处理
 postProcessClassPathArchives(archives);
 return archives;
}

protected abstract boolean isNestedArchive(Archive.Entry entry);

protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}

友情提示:这里我们会看到一个 Archive 对象,先可以暂时理解成一个一个的档案,稍后会清晰认识的~

<1> 处,this::isNestedArchive 代码段,创建了 EntryFilter 匿名实现类,用于过滤 jar 包不需要的目录。

// Archive.java

/**
 * Represents a single entry in the archive.
 */

interface Entry {

 /**
  * Returns {@code true} if the entry represents a directory.
  * @return if the entry is a directory
  */

 boolean isDirectory();

 /**
  * Returns the name of the entry.
  * @return the name of the entry
  */

 String getName();

}

/**
 * Strategy interface to filter {@link Entry Entries}.
 */

interface EntryFilter {

 /**
  * Apply the jar entry filter.
  * @param entry the entry to filter
  * @return {@code true} if the filter matches
  */

 boolean matches(Entry entry);

}

这里在它的内部,调用了 #isNestedArchive(Archive.Entry entry) 方法,它是由 JarLauncher 所实现,代码如下:

// JarLauncher.java

static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

static final String BOOT_INF_LIB = "BOOT-INF/lib/";

@Override
protected boolean isNestedArchive(Archive.Entry entry) {
    // 如果是目录的情况,只要 BOOT-INF/classes/ 目录
 if (entry.isDirectory()) {
  return entry.getName().equals(BOOT_INF_CLASSES);
 }
 // 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包
 return entry.getName().startsWith(BOOT_INF_LIB);
}
  • 目的就是过滤获得, BOOT-INF/classes/ 目录下的类,以及 BOOT-INF/lib/ 的内嵌 jar 包。

<1> 处,this.archive.getNestedArchives 代码段,调用 Archive 的 #getNestedArchives(EntryFilter filter) 方法,获得 archive 内嵌的 Archive 集合。代码如下:

// Archive.java

/**
 * Returns nested {@link Archive}s for entries that match the specified filter.
 * @param filter the filter used to limit entries
 * @return nested archives
 * @throws IOException if nested archives cannot be read
 */

List<Archive> getNestedArchives(EntryFilter filter) throws IOException;

Archive 接口,是 spring-boot-loader 项目定义的档案抽象,其子类如下图所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
Archive 类图
  • ExplodedArchive 是针对 目录的 Archive 实现类。
  • JarFileArchive 是针对 jar 包的 Archive 实现类。

友情提示:这块可能有一丢丢复杂,胖友吃耐心哈~

那么,我们在 ExecutableArchiveLauncher 的 archive 属性是怎么来的呢?答案在 ExecutableArchiveLauncher 的构造方法中,代码如下:

// ExecutableArchiveLauncher.java

public abstract class ExecutableArchiveLauncher extends Launcher {

 private final Archive archive;

 public ExecutableArchiveLauncher() {
  try {
   this.archive = createArchive();
  } catch (Exception ex) {
   throw new IllegalStateException(ex);
  }
 }

 protected ExecutableArchiveLauncher(Archive archive) {
  this.archive = archive;
 }

 // ... 省略其它
}

// Launcher.java
public abstract class Launcher {

 protected final Archive createArchive() throws Exception {
     // 获得 jar 所在的绝对路径
  ProtectionDomain protectionDomain = getClass().getProtectionDomain();
  CodeSource codeSource = protectionDomain.getCodeSource();
  URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
  String path = (location != null) ? location.getSchemeSpecificPart() : null;
  if (path == null) {
   throw new IllegalStateException("Unable to determine code source archive");
  }
  File root = new File(path);
  if (!root.exists()) {
   throw new IllegalStateException(
     "Unable to determine code source archive from " + root);
  }
  // 如果是目录,则使用 ExplodedArchive 进行展开
        // 如果不是目录,则使用 JarFileArchive
  return (root.isDirectory() ? new ExplodedArchive(root)
    : new JarFileArchive(root));
 }

}

根据根路径是否为目录的情况,创建 ExplodedArchive 或 JarFileArchive 对象。那么问题就来了,这里的 root 是什么呢?艿艿一波骚操作,终于输出了答案,如下图所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
root 是什么!

以上是关于硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!的主要内容,如果未能解决你的问题,请参考以下文章

入门kafka,2021新鲜出炉阿里巴巴面试真题

Java面试题,这一次带你搞懂Spring代理创建过程,一招彻底弄懂!

艿艿的 Spring Cloud Alibaba!开整~

5个性能测试工具哪个更好用?对比结果新鲜出炉!

2021 备战秋招新鲜出炉的美团字节阿里腾讯等大厂综合 Java 岗面试题

2022下半年软件设计师上午真题题目+答案 (新鲜出炉持续更新)