SpringBoot JAR 启动原理

发布于:2025-05-22 ⋅ 阅读:(17) ⋅ 点赞:(0)

版本

  • 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条目,应用过滤器筛选后转换为URL
    public 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 退出            |                         |                           |
         |<--------------------|                         |                           |

参考


网站公告

今日签到

点亮在社区的每一天
去签到