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

Spring Boot 应用打包为独立安装包:JPackage 实战指南 Java 应用打包本质上是解决运行时环境一致性问题,其核心原理在于将应用代码、依赖库与指定版本 JRE 绑定封装,避免因 JDK 版本冲突、模块缺失或系统配置差异导致启动失败。JPackage 作为 JDK 原生工具,提供 JRE 裁剪、多平台原生安装包(MSI/PKG/DEB)生成及元数据注入能力,显著提升交付可靠性与终端用户体验。相比 fat jar,它支持服务注册、防火墙配置、图标版权信息嵌入等工程化特性,适用于政企级 Spring Boot 项目交付场景。本文聚焦真实落地中的环境绑定、参数固化、中文 阅读详情

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

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

源码精品专栏

 
“

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

  • 1. 概述

  • 2. MANIFEST.MF

  • 3. JarLauncher

  • 4. LaunchedURLClassLoader

  • 666. 彩蛋


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

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

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 包
  • ① 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 包的启动类,整体类图如下所示:

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 所提供,如下图所示:

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 包的获取。如下图所示:

读取内嵌的 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 项目定义的档案抽象,其子类如下图所示:

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 是什么呢?艿艿一波骚操作,终于输出了答案,如下图所示:

root 是什么!

root 路径为 jar 包的绝对地址,也就是说创建 JarFileArchive 对象。原因是,Launcher 所在包为 org 下,它的根目录当然是 jar 包的绝对路径哈!

???? 现在是不是对 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) 方法的源码,如下图所示:

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(null, new 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 包中加载类的。核心如下图所示:

  • <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 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

兄弟,艿一口,点个赞!????

Spring Boot生存指南:自动装配、Starter契约与实战排坑 Spring Boot并非简单的Spring简化版,而是一套以‘条件化自动装配’和‘能力交付契约’为核心的工程化开发框架。其核心原理在于基于@Conditional的启动时条件匹配、Starter封装的可覆盖配置协议,以及外部化配置的宪法式层级管理。技术价值体现在显著降低Java Web项目启动失败率、配置漂移风险和多环境部署复杂度。典型应用场景包括数据库连接池故障排查、PDF中文渲染、MyBatis Plus分页冲突、Excel像素级转图等高频生产问题。本文聚焦真实世界中的启动契约、配置校验与热词场景(如 阅读详情

相关推荐

VS Code 直接编辑 JAR 文件:Java 热修复新范式

JAR 文件是 Java 应用分发与依赖管理的核心载体,其不可修改性常导致线上紧急故障响应迟滞。理解 JAR 本质需回归 ZIP 封装+字节码结构原理,而真实工程价值在于绕过源码缺失、构建链断裂等约束,实现低侵入热修复。关键技术路径依赖 JDK 原生工具链(如 javap/javac/jar),保障跨版本兼容与安全可控;典型应用场景涵盖第三方 SDK 参数调优、外包遗留系统补丁、金融级灰度环境快速验证等。本文聚焦 VS Code 插件化方案,将 JAR 编辑无缝融入主流 Java 开发流,以‘反编译-编辑-

weixin_33755557的博客 435

Spring Boot Shiro 入门

1. 概述 艿艿:本文是《芋道 Spring Boot 安全框架 Spring Security 入门》的姊妹篇,所以开头就“重复”再来一遍,嘿嘿。 基本上,在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。???? 考虑到很多胖友对认证和授权有点分不清楚,艿艿这里引用一个网上有趣的例子: FROM《认证 (aut...

weixin_42073629的博客 927

Lithe-IDEA:面向Spring Boot的开源轻量Java IDE重构实践

IDE(集成开发环境)是Java开发者的核心生产力工具,其本质是代码解析、语义分析与人机交互的工程综合体。现代Java开发高度依赖Spring Boot等约定式框架,对配置感知、Bean推演和生态协同提出实时化、原生化要求。传统IDE常因闭源模块耦合、插件机制脆弱、启动与内存开销过大,难以满足企业级标准化与开发者自主可控需求。Lithe-IDEA以IntelliJ Platform为基座,通过JavaFX重构UI、能力契约式插件管理、Spring Boot语义内建等关键技术,实现冷启动6.3秒、常驻内存42

weixin_33946605的博客 356

芋道 Spring Boot 安全框架 Spring Security 入门

本文在提供完整代码示例,可见GitHub - YunaiV/SpringBoot-Labs: 一个涵盖六个专栏:Spring Boot 2.X、Spring Cloud、Spring Cloud Alibaba、Dubbo、分布式消息队列、分布式事务的仓库。希望胖友小手一抖,右上角来个 Star,感恩 1024的 对应lab-01-spring-security目录。 原创不易,给点个Star嘿,一起冲鸭! 1. 概述 基本上,在所有的开发的系统中,都必须做认证(authenticat...

chen1133的博客 9576

Spring Boot配置文件敏感数据加密实战:基于Jasypt的完整解决方案

在软件开发和数据安全领域,敏感信息保护是核心议题。其基本原理在于通过加密算法将明文数据转换为不可读的密文,确保即使数据存储或传输介质被非法获取,原始信息也不会泄露。这项技术的核心价值在于为应用程序的配置数据,尤其是数据库密码、API密钥等敏感凭证,提供了一层关键的防护屏障,是实现安全开发生命周期(SDLC)和满足合规性要求的重要实践。在Spring Boot这一主流Java企业级开发框架中,配置文件(如application.yml)的管理尤为关键。针对配置文件中敏感数据泄露的风险,一种广泛应用的工程实践是

cuanku6549的博客 416

Lithe-IDEA:面向Spring Boot的轻量级IntelliJ定制方案

IDE(集成开发环境)作为软件开发的核心工具,其性能与架构适配性直接影响工程效率。IntelliJ IDEA 作为主流Java IDE,虽功能强大,但默认配置常因插件冗余、启动臃肿和框架感知不足,导致Spring Boot开发者在中低配设备、教学场景及CI/CD流水线中面临启动慢、内存高、注解识别失灵等典型问题。Lithe-IDEA并非新IDE,而是基于IntelliJ Platform SDK的深度定制发行版,通过插件白名单管控、JVM参数硬约束、Spring Boot元数据预加载及四层架构语义校验等机制

weixin_33895657的博客 332

芋道 Spring Boot 快速入门

点击上方“芋道源码”,选择“设为星标”做积极的人,而不是积极废人!源码精品专栏原创 | Java 2019 超神之路,很肝~中文详细注释的开源项目RPC 框架 Dubbo 源码解析网络...

芋艿V 1403

Spring Boot多数据源密码加密:SM4国密算法与EnvironmentPostProcessor实战

在微服务架构中,配置安全是防线的重要一环。数据库密码等敏感信息若以明文存储在配置文件或配置中心,一旦泄露极易引发拖库风险。本文从配置安全的基础概念出发,阐述如何在 Spring Boot 应用启动早期,利用 EnvironmentPostProcessor 扩展点对数据源密码进行无感解密。通过引入国密 SM4 算法,结合 dynamic-datasource 多数据源框架,实现配置文件中仅保留密文,且业务代码零侵入。该方案不仅解决多环境密钥管理混乱问题,还满足金融政务场景的国产化合规要求。文章中详细拆解了

weixin_33834137的博客 585

Spring Boot 项目瘦身指南,瘦到不可思议!

https://blog.youkuaiyun.com/yjgithub/article/details/80475521目录:前言瘦身前的 Jar 包解决方案一、前言SpringBoot 部署起来...

JAVA葵花宝典 192

Lithe-IDEA:专为Java/Spring Boot优化的轻量级开源IDE

轻量级IDE是Java开发者长期关注的基础技术概念,其核心在于通过架构精简与服务裁剪实现启动快、内存低、响应敏。原理上,它并非简单禁用插件,而是对IntelliJ Platform进行编译期依赖剥离、运行时服务收敛与UI渲染重构,聚焦Java+Maven+Spring Boot最小可行生态。技术价值体现在冷启动<1.5秒、常驻内存<400MB、50模块项目索引提速13倍等硬指标。典型应用场景包括Spring Boot快速迭代、资源受限设备开发、CI/CD本地验证及专注编码的极简工作流。本文深入解析Lithe

weixin_34126557的博客 350

【SpringBoot 3.x 第64节】Spring Boot 3.x 玩出花了?CRaC 毫秒级启动听起来像科幻,结果我真试成了!

本文介绍了CRaC(Coordinated Restore at Checkpoint)技术在Spring Boot 3.x中的应用,该技术通过"冻结"JVM进程状态实现毫秒级恢复启动。文章详细讲解了CRaC原理、Spring Boot 3.3对主流组件(如DataSource、Redis、Kafka等)的原生支持,并提供了环境配置指南,包括Linux系统要求、CRaC JDK选择和项目配置方法。作者实测显示,该技术能将中型Spring Boot应用的启动时间从9秒降至300-500毫秒

**My Coding Family** 1197

Spring Boot项目创建失败的三大隐性依赖链与修复指南

Spring Boot项目初始化失败,本质是Maven依赖解析、JDK版本兼容性、IDE与构建工具协同这三重技术契约未对齐所致。其核心原理在于Spring Boot通过BOM(Bill of Materials)统一约束生态组件版本,而JDK字节码版本(如class file version 61.0对应JDK 17)、Maven插件API兼容性、以及仓库元数据完整性共同构成启动前提。该机制显著提升工程一致性,但一旦任一环节错配——如Initializr返回SNAPSHOT版BOM、本地Maven缓存损坏、

weixin_34122810的博客 594

Spring Boot毕业设计避坑指南:从启动到部署的硬核实战

Spring Boot是Java Web开发的主流框架,其自动配置与约定优于配置特性大幅降低入门门槛,但真实工程落地涉及依赖管理、启动流程、事务控制、API契约和生产部署等多层技术逻辑。理解Spring Boot生命周期、MyBatis动态SQL边界、事务传播机制及Tomcat/JDBC/MyBatis/Spring四层超时协同,是保障系统稳定的核心能力。结合MySQL建表规范、Swagger接口契约、Linux服务化部署等实践要点,本文聚焦毕业设计高频故障场景,覆盖pom.xml依赖冲突、applicat

weixin_32325225的博客 286

Spring Boot配置文件加密实战:Jasypt与Spring Cloud Config方案详解

在软件工程实践中,配置管理是应用安全与部署的核心环节。其原理在于将敏感信息(如数据库密码、API密钥)从明文存储转为密文,通过加解密机制在运行时动态还原,从而避免信息泄露。这项技术的核心价值在于实现安全性与便利性的平衡,既能防止代码仓库、配置中心泄露风险,又能适应持续集成与容器化部署流程。常见的应用场景包括微服务架构中的集中配置管理、金融支付系统的密钥保护以及符合安全合规要求的项目部署。本文聚焦于Spring Boot生态,深入解析Jasypt对称加密与Spring Cloud Config非对称加密两种主

weixin_33847182的博客 467

SpringBoot-Labs:一个涵盖六个专栏:Spring Boot 2.X,Spring Cloud,Spring Cloud Alibaba,Dubbo,分散消息类别,各种事务的仓库。希望胖友小手一抖,右上角来个星星,感恩1024

友情提示:因为提供了50000+行示例代码,所以艿艿替换注释了所有Maven模块。 胖友可以根据自己的需要,修改立即。 一个涵盖六个主流技术栈的正经仓库: 作为一个热爱深夜撸码的18岁头发茂密的可爱小男孩,希望大佬能够一键三连。 亲,一键三连啊 交流群 扫码如下二维码,回复“艿艿”关键字。 可以添加18岁的我为好友,并拉你进一个交流装逼群。 :smiling_face_with_horns:等后面,艿艿头发重新长一点出来,给旁友们录制点视频哈! Spring Boot专栏 基于Spring Boot 2.X版本的深度入门教程。 市面上的春天引导基础入门文章很多,但是深度入门文章却很少。对于很多开发者来说,入门即是其对某个技术栈的最终理解,

你以为你懂 Java 反射?看了这篇源码分析,我发现我以前都错了

摘要:本文讲述了一次生产环境Metaspace内存溢出的故障排查经历。事故源于一个通用数据导出功能中频繁使用反射调用method.invoke(),触发了JVM的"膨胀机制":当同一Method对象调用超过15次时,JVM会动态生成GeneratedMethodAccessor类,导致Metaspace被撑爆。文章分析了问题根源在于循环中反复调用getMethods()创建新Method对象,并提出了三种解决方案:缓存Method对象、改用MapStruct/MethodHandle工具

BUG猿的博客 770

从Spring Boot到裸机:一位软件工程师的硬核梦想之旅

摘要:本文记录了一场关于软件工程效率的深度技术探讨,从构建工具、依赖管理到裸机开发和存算一体架构。文章对比了现代软件抽象带来的效率损失与裸机开发的极致性能,探讨了存算一体技术突破冯·诺依曼瓶颈的可能性,并提出从FPGA实践开始的技术进阶路径。最后强调技术人应建立系统化知识体系,追求"卷质"而非"卷量",在软硬件融合中寻找创新突破。这场对话展现了技术人对效率本质的不懈追求。

qq_36710118的博客 344

借助AI高效写出结构严谨、内容专业的课题申报书:10大核心模块拆解(附实用提示词)

在很多老师和科研人员眼中,写一份高质量的课题申报,确实是一件非常困难的事。难就难在不只是要说清楚写了什么研究内容,还要体现申报书的可行性和创新性。七哥除了自己写课题,也经常指导会员同仁使用高级学术版搞定课题的申报工作,摸索出了一套行之有效的借助AI申报课题的技巧,在这篇文章中,我将给大家点明课题申报书的10大核心模块的写作技巧,还会给大家一些提示词模板供大家参考,如果你也正准备申报课题,那么这篇文章一定不要错过!特别说明的是,提示词的使用并不是一蹴而就的,是专业思考的延伸和AI化专业表达,过程中需要多次的调

智能AI让学术科研更容易 445

Spring Boot性能调优实战:从启动慢到高并发的全链路优化

Spring Boot作为主流Java开发框架,其默认配置在生产环境中常成为性能瓶颈的隐性根源。理解自动装配机制、Web容器线程模型、ORM执行路径及JVM内存分代原理,是实现高效服务的基础。本文聚焦Spring Boot性能优化核心场景——启动耗时过高、接口响应延迟、连接池争抢与GC频繁等典型问题,结合Tomcat/Jetty/Undertow容器选型对比、HikariCP连接池参数调优、G1 GC分代策略调整及MyBatis-Plus字段投影等真实工程实践,系统揭示配置项背后的操作系统、JVM和数据库物

weixin_29284201的博客 221

30分钟上手YOLO26 nano:INT8量化部署到智能相机,推理速度提升43%

微调代码(仅需5行,数据集遵循COCO格式)data="custom_data.yaml", # 数据集配置文件epochs=20, # 少量轮数即可收敛batch=8,imgsz=640model.save("yolov26n_custom.pt") # 保存微调后的模型本文在30分钟内完成了YOLO26 nano的INT8量化与智能相机部署,核心步骤为环境准备→模型获取→ONNX导出→INT8量化→部署推理。

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 987

新系统申请软件著作权详细操作说明

今年八月底版权中心系统升级,新系统相对好用一些,至少不用再拿IE访问了,但是多了一项实名认证。下面介绍新系统的操作流程。 目录注册、登录和实名认证注册登录实名认证软件登记申请申请表填写鉴别材料身份证明文件权利归属证明文件其他材料提交材料变更登记 注册、登录和实名认证 首先访问版权中心系统:https://register.ccopyright.com.cn/login.html,如果老系统有账号直接登录即可,没有的话需要注册。 注册 点击注册按钮进入注册。 根据实际情况选择个人或单位,每个个人或单位只能在

Programming Talk 1万+
上一篇: Spring Boot 整合Netty(附源码)
下一篇: Nginx 为什么快到根本停不下来?
公众号-芋道源码
博客等级 码龄10年 1631粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值