vlambda博客
学习文章列表

硬核艿艿,新鲜出炉,直接带你弄懂 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 是什么!

😈 现在是不是对 Archive 稍微有点感觉落?继续附加如下代码,打印 JarFileArchive 的 #getNestedArchives(EntryFilter filter) 方法的执行结果。

// ========== 附加代码:
// 创建 Archive 对象
Archive archive = new JarFileArchive(root);
// 创建 EntryFilter 对象
Archive.EntryFilter filter = new Archive.EntryFilter() {

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

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

    @Override
    public boolean matches(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);
    }

};
// 执行读取
for (Archive item : archive.getNestedArchives(filter)) {
    System.out.println(item.getUrl());
}

// ========== 执行结果:
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/classes!/
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.2.RELEASE.jar!/
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-starter-2.2.2.RELEASE.jar!/
jar:file:/Users/yunai/Java/SpringBoot-Labs/lab-39/lab-39-demo/target/lab-39-demo-2.2.2.RELEASE.jar!/BOOT-INF/lib/spring-boot-2.2.2.RELEASE.jar!/
... 省略其他 jar 包

从执行结果可以看出,BOOT-INF/classes/ 目录被归类为一个 Archive 对象,而 BOOT-INF/lib/ 目录下的每个内嵌 jar 包都对应一个 Archive 对象。

来来来,回过头来看看 JarFileArchive 的 #getNestedArchives(EntryFilter filter) 方法的源码,如下图所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
JarFileArchive 的 #getNestedArchives(EntryFilter filter) 方法

现在是不是明白了噢!良心如我,哈哈哈!

友情提示:上述的测试代码,可以访问 lab-39-demo 项目查看。

3.2.2 createClassLoader

然后,我再来看看 #createClassLoader(List<Archive> archives) 方法,它是由 ExecutableArchiveLauncher 所实现,代码如下:

// ExecutableArchiveLauncher.java

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
 // 获得所有 Archive 的 URL 地址
    List<URL> urls = new ArrayList<>(archives.size());
 for (Archive archive : archives) {
  urls.add(archive.getUrl());
 }
 // 创建加载这些 URL 的 ClassLoader
 return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
 return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

基于获得的 Archive 数组,创建自定义 ClassLoader 实现类 LaunchedURLClassLoader,通过它来加载 BOOT-INF/classes 目录下的类,以及 BOOT-INF/lib 目录下的 jar 包中的类。

进一步的解析,我们在「5. LaunchedURLClassLoader」小节中,进行分享哈!

3.3 launch

友情提示:对应 launch(args, getMainClass(), classLoader) 代码段,不要迷路。

3.3.1 getMainClass

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

// ExecutableArchiveLauncher.java

@Override
protected String getMainClass() throws Exception {
    // 获得启动的类的全名
 Manifest manifest = this.archive.getManifest();
 String mainClass = null;
 if (manifest != null) {
  mainClass = manifest.getMainAttributes().getValue("Start-Class");
 }
 if (mainClass == null) {
  throw new IllegalStateException(
    "No 'Start-Class' manifest entry specified in " + this);
 }
 return mainClass;
}

jar 包的 MANIFEST.MF 文件的 Start-Class 配置项,,获得我们设置的 Spring Boot 的主启动类。

3.3.2 createMainMethodRunner

然后,我们再来看看 #launch() 方法,它是由 Launcher 所实现,代码如下:

protected void launch(String[] args, String mainClass, ClassLoader classLoader)
  throws Exception 
{
    // <1> 设置 LaunchedURLClassLoader 作为类加载器
 Thread.currentThread().setContextClassLoader(classLoader);
 // <2> 创建 MainMethodRunner 对象,并执行 run 方法,启动 Spring Boot 应用
 createMainMethodRunner(mainClass, args, classLoader).run();
}

该方法负责最终的 Spring Boot 应用真正的启动。

  • <1> 处:设置「3.2.2 createClassLoader」创建的 LaunchedURLClassLoader 作为类加载器,从而保证能够从 jar 加载到相应的类。
  • <2> 处,调用 #createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) 方法,创建 MainMethodRunner 对象,并执行其 #run() 方法来启动 Spring Boot 应用。

下面,我们来看看 MainMethodRunner 类,负责 Spring Boot 应用的启动。代码如下:

public class MainMethodRunner {

 private final String mainClassName;

 private final String[] args;

 /**
  * Create a new {@link MainMethodRunner} instance.
  * @param mainClass the main class
  * @param args incoming arguments
  */

 public MainMethodRunner(String mainClass, String[] args) {
  this.mainClassName = mainClass;
  this.args = (args != null) ? args.clone() : null;
 }

 public void run() throws Exception {
     // <1> 加载 Spring Boot
  Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
  // <2> 反射调用 main 方法
  Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
  mainMethod.invoke(nullnew Object[] { this.args });
 }

}
  • <1> 处:通过 LaunchedURLClassLoader 类加载器,加载到我们设置的 Spring Boot 的主启动类。
  • <2> 处:通过 反射调用主启动类的 #main(String[] args) 方法,启动 Spring Boot 应用。这里也告诉了我们答案,为什么我们通过编写一个带有 #main(String[] args) 方法的类,就能够启动 Spring Boot 应用。

4. LaunchedURLClassLoader

LaunchedURLClassLoader 是 spring-boot-loader 项目自定义的类加载器,实现对 jar 包中 META-INF/classes 目录下的类和 META-INF/lib 内嵌的 jar 包中的类的加载。

FROM 《维基百科 —— Java 类加载器》

Java 类加载器是 Java 运行时环境的一个部件,负责动态加载 Java 类到 Java 虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。

由于有了类加载器,Java 运行时系统不需要知道文件与文件系统。对学习类加载器而言,掌握 Java 的委派概念是很重要的。每个 Java 类必须由某个类加载器装入到内存。

在「3.2.2 createClassLoader」小节中,我们可以看到 LaunchedURLClassLoader 的创建代码如下:

// ExecutableArchiveLauncher.java

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    // 获得所有 Archive 的 URL 地址
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    // 创建加载这些 URL 的 ClassLoader
    return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
 return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

// LaunchedURLClassLoader.java

public class LaunchedURLClassLoader extends URLClassLoader {

 public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
  super(urls, parent);
 }
 
}
  • 第一个参数 urls,使用的是 Archive 集合对应的 URL 地址们,从而告诉 LaunchedURLClassLoader 读取 jar地址。
  • 第二个参数 parent,设置 LaunchedURLClassLoader 的 父加载器。这里后续胖友可以理解下,类加载器的 双亲委派模型,这里就拓展开了。

LaunchedURLClassLoader 的实现代码并不多,我们主要来看看它是如何从 jar 包中加载类的。核心如下图所示:

硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!
  • <1> 处,在通过 父类的 #getPackage(String name) 方法获取不到指定类所在的包时, 会通过遍历 urls 数组,从 jar 包中加载类所在的包。当找到包时,会调用 #definePackage(String name, Manifest man, URL url) 方法,设置包所在的 Archive 对应的 url
  • <2> 处,调用 父类的 #loadClass(String name, boolean resolve) 方法,加载对应的类。

如此,我们就实现了通过 LaunchedURLClassLoader 加载 jar 包中内嵌的类。

666. 彩蛋

总体来说,Spring Boot jar 启动的原理是非常清晰的,整体如下图所示:

Spring Boot jar 启动原理

红色部分,解决 jar 包中的类加载问题:

  • 通过 Archive,实现 jar 包的 遍历,将 META-INF/classes 目录和 META-INF/lib 的每一个内嵌的 jar 解析成一个 Archive 对象。
  • 通过 Handler,处理 jar: 协议的 URL 的资源 读取,也就是读取了每个 Archive 里的内容。
  • 通过 LaunchedURLClassLoader,实现 META-INF/classes 目录下的类和 META-INF/classes 目录下内嵌的 jar 包中的类的加载。具体的 URL 来源,是通过 Archive 提供;具体 URL 的读取,是通过 Handler 提供。

橘色部分,解决 Spring Boot 应用的启动问题:

  • 通过 MainMethodRunner ,实现 Spring Boot 应用的启动类的执行。

当然,上述的一切都是通过 Launcher 来完成引导和启动,通过 MANIFEST.MF 进行具体配置。

😈 生活如此美好,本文就此结束!


另外,本文有两个部分,胖友可以自己再去撸一撸,玩一玩:

  • WarLauncher 类:实现 Spring Boot war 包的启动。
  • org.springframework.boot.loader.jar 包:具体读取 jar 的逻辑。




已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

兄弟,一口,点个👇