当前位置: 首页 > news >正文

SpringBoot JAR 启动原理

文章目录

    • 版本
    • 概述
    • JAR 包结构
    • `MANIFEST.MF` 描述文件
    • JarLauncher
      • `Archive` 接口
      • `launch` 方法
      • `Handlers.register()` 方法
      • `getClassPathUrls` 方法
      • `createClassLoader` 方法
    • 时序图
    • 参考

版本

  • Java 17
  • SpringBoot 3.2.4

概述

JAR 启动原理可以简单理解为“java -jar的启动原理”

SpringBoot 提供了 Maven 插件 spring-boot-maven-plugin,可以将 SpringBoot 项目打包成 JAR 包,这个跟普通 JAR 包有所不同

  • 普通 JAR 包:可以被其他项目引用,解压后就是包名,包里就是代码
  • SpringBoot 打包的 JAR 包:只能运行,不能被其他项目依赖,包里 \BOOT-INF\classes 目录才是代码

使用 maven package 指令执行打包命令,将项目打包成 JAR 包,根据 pom.xml 文件中的 nameversion 标签作为 JAR 包名称,比如项目的
pom.xml 配置文件有 <name>springboot-demo</name><version>0.0.1-SNAPSHOT</version>,执行 maven package 命令之后打包出来之后为 springboot-demo-0.0.1-SNAPSHOT.jarspringboot-demo-0.0.1-SNAPSHOT.jar.original

  • springboot-demo-0.0.1-SNAPSHOT.jar 之类的 JAR 包:spring-boot-maven-plugin 生成的 JAR 包。包含了应用的第三方依赖,SpringBoot 相关的类,存在嵌套的 JAR 包,称之为 executable jar 或 fat jar。也就是最终可运行的 SpringBoot 的 JAR 包。可以直接执行 java -jar 指令启动
  • springboot-demo-0.0.1-SNAPSHOT.jar.original 之类的 JAR 包:默认 maven-jar-plugin 生成的 JAR 包,仅包含编译用的本地文件。也就是打包之前生成的原始 JAR 包,仅包含你项目本身的 class 文件和资源文件,不包含依赖项,也不具备 Spring Boot 的启动结构。通常由 Spring Boot Maven 插件在打包过程中中间步骤生成,Spring Boot 会在这个基础上重打包(repackage)为可运行的 JAR 文件。

JAR 包结构

springboot-demo-0.0.1-SNAPSHOT.jar 之类的 JAR 包中通常包括 BOOT-INFMETA-INForg 三个文件夹

  • META-INF:通过 MANIFEST.MF 文件提供 jar 包的元数据,声明了 JAR 的启动类
  • org:为 SpringBoot 提供的 spring-boot-loader 项目,它是 java -jar 启动 Spring Boot 项目的秘密所在
  • BOOT-INF/lib:SpringBoot 项目中引入的依赖的 JAR 包,目的是解决 JAR 包里嵌套 JAR 的情况,如何加载到其中的类
  • BOOT-INF/classes:Java 类所编译的 .class、配置文件等等

应用程序类应该放在嵌套的BOOT-INF/classes目录中。依赖项应该放在嵌套的BOOT-INF/lib目录中。

├── BOOT-INF // 文件目录存放业务相关的,包括业务开发的类和配置文件,以及依赖的 JAR
│   ├── classes
│   │   ├── application.yaml
│   │   └── com
│   │       └── example
│   │           └── springbootdemo
│   │               ├── OrderProperties.class
│   │               ├── SpringbootDemoApplication.class // 启动类
│   │               ├── SpringbootDemoApplication$OrderPropertiesCommandLineRunner.class
│   │               ├── SpringbootDemoApplication$ValueCommandLineRunner.class
│   │               ├── SpringMVCConfiguration.class
│   │               └── vo
│   │                   └── UserVO.class
│   ├── classpath.idx
│   ├── layers.idx
│   └── lib
│       ├── spring-aop-6.1.6.jar
│       ├── spring-beans-6.1.6.jar
│       ├── spring-boot-3.2.5.jar
│       ├── spring-boot-autoconfigure-3.2.5.jar
│       ├── spring-boot-jarmode-layertools-3.2.5.jar
├── META-INF // MANIFEST.MF 描述文件和 maven 的构建信息
│   ├── MANIFEST.MF
│   ├── maven
│   │   └── com.example
│   │       └── springboot-demo
│   │           ├── pom.properties // 配置文件
│   │           └── pom.xml
│   ├── services
│   │   └── java.nio.file.spi.FileSystemProvider
│   └── spring-configuration-metadata.json
└── org└── springframework└── boot└── loader // SpringBoot loader 相关类├── jar│   ├── ManifestInfo.class│   ├── MetaInfVersionsInfo.class├── jarmode│   └── JarMode.class├── launch│   ├── Archive.class│   ├── Archive$Entry.class├── log│   ├── DebugLogger.class│   ├── DebugLogger$DisabledDebugLogger.class│   └── DebugLogger$SystemErrDebugLogger.class├── net│   ├── protocol│   │   ├── Handlers.class│   │   ├── jar│   │   │   ├── Canonicalizer.class│   │   │   ├── Handler.class│   │   └── nested│   │       ├── Handler.class│   │       ├── NestedLocation.class│   └── util│       └── UrlDecoder.class├── nio│   └── file│       ├── NestedByteChannel.class│       ├── NestedByteChannel$Resources.class├── ref│   ├── Cleaner.class│   └── DefaultCleaner.class└── zip├── ByteArrayDataBlock.class├── CloseableDataBlock.class

MANIFEST.MF 描述文件

MANIFEST.MF 是 Java JAR(Java Archive)文件中的一个核心元数据文件,用于描述 JAR 包的配置信息和依赖关系。它位于 JAR 文件内部的 META-INF/ 目录下,是 JVM 启动可执行 JAR 或加载依赖的关键依据。

java -jar 命令引导的具体启动类必须配置在 MANIFEST.MF 描述文件中的 Main-Class 属性中,该命令用来引导标准执行的 JAR 文件,读取的就是 MANIFEST.MF 文件中的 Main-Class 属性值,Main-Class 属性就是定义包含了 main 方法的类代表了应用程序执行入口类

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.3.0
Build-Jdk-Spec: 19
Implementation-Title: springboot-demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.springbootdemo.SpringbootDemoApplication
Spring-Boot-Version: 3.2.5
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
  • Main-Class:定义程序入口类,格式为完全限定类名(含包名),JVM 通过此属性找到 public static void main(String[] args) 方法启动程序。设置为 spring-boot-loader 项目的 JarLauncher 类,进行 SpringBoot 应用的启动。
  • Start-Class:SpringBoot 规定的主启动类
  • Class-Path:指定依赖的 JAR 文件或目录,路径用空格分隔
    • 路径相对于 JAR 文件的位置(非当前工作目录)
    • 依赖需放在 JAR 同级目录的指定路径下
  • Manifest-Version:指定清单文件版本
  • Created-By:生成 JAR 的工具信息(如 JDK 版本或构建工具)
  • Implementation-Version:JAR 的版本号(用于版本管理)

虽然 Start-Class 已经指向了主启动类路径,但是不能直接启动

  • 原因一:因为在 JAR 包中,主启动类并不在这个路径上,而是在在 BOOT-INF/classes 目录下,不符合 Java 默认的 JAR 包的加载规则。因此,需要通过 JarLauncher 启动加载。
  • 原因二:Java 规定可执行器的 JAR 包禁止嵌套其它 JAR 包。但是可以看到 BOOT-INF/lib 目录下,实际有 SpringBoot 应用依赖的所有 JAR 包。因此,spring-boot-loader 项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载 BOOT-INF/classes 目录下的 .class 文件,以及 BOOT-INF/lib 目录下的 jar 包。

JarLauncher

JarLauncherSpring Boot 框架中用于启动可执行 JAR 文件的核心类,属于 org.springframework.boot.loader 包。它的核心作用是为 Spring Boot 的“胖 JAR”(Fat JAR)提供自定义的启动机制,解决传统 JAR 无法直接加载嵌套依赖的问题。

位于 JAR 包中的 org.springframework.boot.loader.launch.JarLauncher

继承类:Launcher -> ExecutableArchiveLauncher -> JarLauncher

public class JarLauncher extends ExecutableArchiveLauncher {public JarLauncher() throws Exception {}protected JarLauncher(Archive archive) throws Exception {super(archive);}protected boolean isIncludedOnClassPath(Archive.Entry entry) {return isLibraryFileOrClassesDirectory(entry);}protected String getEntryPathPrefix() {return "BOOT-INF/";}static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {String name = entry.name();return entry.isDirectory() ? name.equals("BOOT-INF/classes/") : name.startsWith("BOOT-INF/lib/");}public static void main(String[] args) throws Exception {(new JarLauncher()).launch(args);}
}

通过 (new JarLauncher()).launch(args) 创建 JarLauncher 对象,调用 launch 方法进行启动,整体逻辑还是通过父类的父类 Laucher 所提供。

Archive 接口

根据类图可知,JarLauncher 继承于 ExecutableArchiveLauncher 类,在 ExecutableArchiveLauncher 类源码中有对 Archive 对象的构造

public abstract class ExecutableArchiveLauncher extends Launcher {public ExecutableArchiveLauncher() throws Exception {this(Archive.create(Launcher.class));}
}

Archive 接口,是 spring-boot-loader 项目抽象出来的用来统一访问资源的接口,ExplodedArchive 是针对目录的 Archive 实现类,JarFileArchive 是针对 JAR 的 Archive 实现类,所以根据 isDirectory 方法进行判断。

  • Archive 概念即归档文档概念,在 Linux 下比较常见
  • 通常就是一个 tar/zip 格式的压缩包
  • JAR 是 zip 格式
public interface Archive extends AutoCloseable {static Archive create(Class<?> target) throws Exception {return create(target.getProtectionDomain());}static Archive create(ProtectionDomain protectionDomain) throws Exception {CodeSource codeSource = protectionDomain.getCodeSource();URI location = codeSource != null ? codeSource.getLocation().toURI() : null;// 拿到当前 classpath 的绝对路径String path = location != null ? location.getSchemeSpecificPart() : null;if (path == null) {throw new IllegalStateException("Unable to determine code source archive");} else {return create(new File(path));}}static Archive create(File target) throws Exception {if (!target.exists()) {throw new IllegalStateException("Unable to determine code source archive from " + target);} else {return (Archive)(target.isDirectory() ? new ExplodedArchive(target) : new JarFileArchive(target));}}
}

launch 方法

在其父类 Laucher 中可以看出,launcher 方法可以读取 JAR 包中的类加载器,保证 BOOT-INF/lib 目录下的类和 BOOT-classes 内嵌的 jar 中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。

public abstract class Launcher {protected void launch(String[] args) throws Exception {// 如果当前不是解压模式(!this.isExploded()),则注册处理器(Handlers.register())if (!this.isExploded()) {Handlers.register();}try {// 创建类加载器(ClassLoader)用于加载类路径上的类ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());// 根据系统属性 "jarmode" 判断是否使用特定的 JAR 模式运行器类名String jarMode = System.getProperty("jarmode");String mainClassName = this.hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : this.getMainClass();// 使用创建的类加载器和主类名调用 launch 方法启动应用this.launch(classLoader, mainClassName, args);} catch (UncheckedIOException var5) {UncheckedIOException ex = var5;throw ex.getCause();}}protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception {Thread.currentThread().setContextClassLoader(classLoader);Class<?> mainClass = Class.forName(mainClassName, false, classLoader);Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);mainMethod.setAccessible(true);mainMethod.invoke((Object)null, args);}
}

Handlers.register() 方法

逐步分析 launcher 方法,首先方法中调用的 Handlers.register() 方法,用于动态注册自定义协议处理器包,并确保 URL 流处理器缓存被正确刷新。

public final class Handlers {private static final String PROTOCOL_HANDLER_PACKAGES = "java.protocol.handler.pkgs";private static final String PACKAGE = Handlers.class.getPackageName();private Handlers() {}public static void register() {// 获取系统属性java.protocol.handler.pkgs,该属性用于指定协议处理器的包名String packages = System.getProperty("java.protocol.handler.pkgs", "");// 如果当前包名未包含在属性中,则将其追加到属性值中(以|分隔)packages = !packages.isEmpty() && !packages.contains(PACKAGE) ? packages + "|" + PACKAGE : PACKAGE;System.setProperty("java.protocol.handler.pkgs", packages);// 清除URL流处理器缓存resetCachedUrlHandlers();}private static void resetCachedUrlHandlers() {try {// 强制JVM重新加载URL流处理器URL.setURLStreamHandlerFactory((URLStreamHandlerFactory)null);} catch (Error var1) {}}
}

getClassPathUrls 方法

分析 ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls()); 中的 (Collection)this.getClassPathUrls() 方法,调用 getClassPathUrls 方法返回值作为参数,该方法为抽象方法,具体实现在 ExecutableArchiveLauncher

public abstract class ExecutableArchiveLauncher extends Launcher {protected Set<URL> getClassPathUrls() throws Exception {return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, this::isSearchedDirectory);}
}

ExecutableArchiveLaunchergetClassPathUrls 方法执行 Archive 接口定义的 getClassPathUrls 方法返回的是包含所有匹配 URL 的有序集合

class JarFileArchive implements Archive {// 通过流处理遍历JAR条目,应用过滤器筛选后转换为URLpublic Set<URL> getClassPathUrls(Predicate<Archive.Entry> includeFilter, Predicate<Archive.Entry> directorySearchFilter) throws IOException {return (Set)this.jarFile.stream().map(JarArchiveEntry::new).filter(includeFilter).map(this::getNestedJarUrl).collect(Collectors.toCollection(LinkedHashSet::new));}// 根据条目注释判断是否为解压存储的嵌套JAR,若是则调用特殊处理方法,否则直接创建标准URL// archiveEntry:BOOT-INF/classes/private URL getNestedJarUrl(JarArchiveEntry archiveEntry) {try {JarEntry jarEntry = archiveEntry.jarEntry();String comment = jarEntry.getComment();return comment != null && comment.startsWith("UNPACK:") ? this.getUnpackedNestedJarUrl(jarEntry) : JarUrl.create(this.file, jarEntry);} catch (IOException var4) {IOException ex = var4;throw new UncheckedIOException(ex);}}
}

createClassLoader 方法

分析 ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls()); 中的 createClassLoader 方法

LaunchedClassLoader 是 SpringBoot 自定义的类加载器,位于 org.springframework.boot.loader.LaunchedURLClassLoader, 专门用于加载 Spring Boot 可执行 JAR(即“胖 JAR”)中嵌套的依赖和资源。它的核心作用是解决传统 Java 类加载器无法直接加载 JAR 内嵌 JAR(如 BOOT-INF/lib/ 中的依赖)的问题。且LaunchedClassLoader 在加载类时,会先尝试自己加载(从嵌套 JAR 或用户代码),若找不到再委派父类加载器。这是对传统双亲委派机制的扩展,确保优先加载应用自身的类。

public abstract class Launcher {protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {return this.createClassLoader((URL[])urls.toArray(new URL[0]));}private ClassLoader createClassLoader(URL[] urls) {ClassLoader parent = this.getClass().getClassLoader();return new LaunchedClassLoader(this.isExploded(), this.getArchive(), urls, parent);}
}

时序图

+-----------------+     +--------------+     +----------------------+     +-------------------+
|      JVM        |     | JarLauncher  |     | LaunchedURLClassLoader|     | MainMethodRunner  |
+-----------------+     +--------------+     +----------------------+     +-------------------+|                     |                         |                           || 执行 java -jar app.jar |                       |                           ||--------------------->|                         |                           ||                     | 创建 Archive 对象        |                           ||                     |------------------------>|                           ||                     | 解析 MANIFEST.MF         |                           ||                     |<------------------------|                           ||                     | 调用 launch()            |                           ||                     |------------------------>|                           ||                     |                         | 创建类加载器              ||                     |                         |<--------------------------||                     |                         | 加载 BOOT-INF/classes/lib||                     |                         |-------------------------->||                     |                         |                           | 反射加载 Start-Class|                     |                         |                           |<------------------||                     |                         |                           | 调用 main()|                     |                         |                           |------------------>||                     |                         |                           | 执行用户代码       ||<------------------------------------------------------------(结果或异常)|| JVM 退出            |                         |                           ||<--------------------|                         |                           |

参考

  • 一文搞懂 Spring Boot 中 java -jar 的启动 jar 包的原理_springboot java -jar-CSDN 博客
  • 芋道 Spring Boot Jar 启动原理 | 芋道源码 —— 纯源码解析博客
  • Site Unreachable

相关文章:

  • 【Linux高级全栈开发】2.2.1 Linux服务器百万并发实现2.2.2 Posix API与网络协议栈
  • Mysql差异备份与恢复
  • 小黑黑prompt表述短语积累1
  • YOLO训练输入尺寸代表什么 --input_width 和 --input_height 参数
  • QGIS3.40.X使用OSM获取数据
  • 实践大模型提示工程(Prompt Engineering)
  • 民锋视角下的多因子金融分析模型实践
  • 电商项目-商品微服务-规格参数管理,分类与品牌管理需求分析
  • Spring AOP拦截失败
  • Spring IOCDI————(2)
  • 如何提灯验车
  • Android13 wifi设置国家码详解
  • 全球机电气缸市场深度洞察:技术驱动与产业升级下的增长机遇(2025-2031)
  • 面试相关的知识点
  • mmaction2——tools文件夹下
  • 从零基础到最佳实践:Vue.js 系列(4/10):《Vue Router 路由管理:深入探索与实战应用》
  • 深入浅出理解时间复杂度和空间复杂度
  • 学习黑客了解密码学
  • UML 活动图 (Activity Diagram) 使用案例
  • 【Java高阶面经:微服务篇】8.高可用全链路治理:第三方接口不稳定的全场景解决方案
  • 网站建设服务合同书/软文营销文章300字
  • 武汉网站建设网站/seo深圳优化
  • 自己做的网站套dedecms教程/网络营销策划与创意
  • 中国在菲律宾做网站/网站设计公司上海