深入解析:Spring Boot可执行JAR为何能直接运行?

Spring Boot JAR 直接运行的技术原理‌

‌1. 可执行 JAR 的结构设计‌

Spring Boot 生成的 JAR 是‌可执行 Fat JAR‌,其结构与传统 JAR 不同:

    ‌嵌套依赖‌:所有第三方依赖 JAR 文件存储在 BOOT-INF/lib 目录下,避免外部依赖冲突‌。
    ‌自定义类加载器‌:通过 Spring Boot Loader(位于 org/springframework/boot/loader 目录)实现嵌套 JAR 的加载‌。
    ‌应用代码与配置‌:用户代码在 BOOT-INF/classes 目录,META-INF/MANIFEST.MF 定义入口类‌。

我们将SpringBoot的应用打包jar文件解压后,可以看到如下结构:

文件说明:

1、BOOT-INF:   classes 目录包括应用内所有的class;lib 目录是所有依赖的jar包。

2、META-INF:增强型清单文件

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: cz
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.zikao.exam.ZikaoExamApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.7.18
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_351
Main-Class: org.springframework.boot.loader.JarLauncher

3、org: jar包加载相关类(Loader Classes‌)

 

‌2. 核心机制:Spring Boot Loader‌

    ‌入口类 JarLauncher‌
    MANIFEST.MF 中指定 Main-Class 为 JarLauncher,而非用户的主类。JarLauncher 负责启动流程‌:

    JVM->>JarLauncher: main()
    JarLauncher->>ClassLoader: 创建LaunchedURLClassLoader
    ClassLoader->>JarLauncher: 返回加载器实例
    JarLauncher->>Application: 反射调用main()
    Application->>SpringApplication: run()

       1、JarLauncher 中main方法开启启动;

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

        2、在初始化JarLauncher时,同时会初识化父类构造方法 ,获取到jar报的文档地址,加载所有的jar文件;

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

 通过获取classpath.idx 文件,获取加载的所有jar包

 protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
        if (archive instanceof ExplodedArchive) {
            String location = this.getClassPathIndexFileLocation(archive);
            return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
        } else {
            return null;
        }
    }

    private String getClassPathIndexFileLocation(Archive archive) throws IOException {
        Manifest manifest = archive.getManifest();
        Attributes attributes = manifest != null ? manifest.getMainAttributes() : null;
        String location = attributes != null ? attributes.getValue("Spring-Boot-Classpath-Index") : null;
        return location != null ? location : this.getArchiveEntryPathPrefix() + "classpath.idx";
    }

3、  创建 LaunchedURLClassLoader,加载 BOOT-INF/lib 下的依赖 JAR。使用自定义类加载器 LaunchedURLClassLoader,支持从嵌套 JAR 中加载类资源,解决传统 JAR 无法加载嵌套依赖的问题‌。

 protected void launch(String[] args) throws Exception {
        if (!this.isExploded()) {
            JarFile.registerUrlProtocolHandler();
        }

        ClassLoader classLoader = this.createClassLoader(this.getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        String launchClass = jarMode != null && !jarMode.isEmpty() ? "org.springframework.boot.loader.jarmode.JarModeLauncher" : this.getMainClass();
        this.launch(args, launchClass, classLoader);
    }



  protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
        List<URL> urls = new ArrayList(50);

        while(archives.hasNext()) {
            urls.add(((Archive)archives.next()).getUrl());
        }

        return this.createClassLoader((URL[])urls.toArray(new URL[0]));
    }

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

4、获取应用的主方法 ,也就是在 META-INF(增强型清单文件 )文件中的Start-Class所对应的方法

 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);
        } else {
            return mainClass;
        }
    }

5、 通过反射调用用户主类(Start-Class 指定的 SpringApplication 类)的 main() 方法‌。

protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        this.createMainMethodRunner(launchClass, args, classLoader).run();
    }

    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, args);
    }

‌3. Maven 插件支持‌

通过 spring-boot-maven-plugin 插件实现可执行 JAR 的生成:

    ‌repackage 目标‌:将标准 Maven 构建的 JAR 转换为 Fat JAR,嵌入依赖和 Spring Boot Loader‌。
    ‌配置示例‌:

    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>

‌4. 嵌入式容器整合‌

Spring Boot 默认集成嵌入式 Web 服务器(如 Tomcat、Jetty),无需外部部署:

    ‌依赖内嵌‌:服务器代码作为依赖打包进 JAR 的 BOOT-INF/lib 目录‌。
    ‌自动启动‌:SpringApplication 启动时自动初始化嵌入式服务器并监听端口‌。

‌总结‌

Spring Boot JAR 可直接运行的核心原因:

    ‌Fat JAR 结构‌:整合依赖、类加载器和应用代码‌。
    ‌自定义类加载器‌:通过 JarLauncher 和 LaunchedURLClassLoader 加载嵌套 JAR‌。
    ‌插件支持‌:spring-boot-maven-plugin 生成可执行包‌。
    ‌嵌入式服务器‌:内置 Web 容器实现自包含部署‌。

这些机制共同实现了 java -jar 直接启动 Spring Boot 应用的便捷性。