打包后的SpringBoot项目为什么可以直接运行

前言

SpringBoot框架已经成为很多公司的标配,得益于其快速配置和容易上手,将程序员从繁杂的项目配置工作中解脱出来,将精力更多的集中在业务中,而深受猿媛们的喜爱。通过使用java -jar命令直接运行打包后的SpringBoot项目,不用再搭建额外的tomcat等web容器便可以启动一个web项目。不知你有没有思考过,打包后的SpringBoot项目为什么可以直接运行呢?

运行命令做了什么

要想搞清楚这个问题,就要看看java -jar命令到底做了哪些事情。通过阅读Oracle官方文档可以找到该命令的描述。
对于java [options] -jar filename [args]有:

If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. 
The startup class must be indicated by the Main-Class manifest header in its source code.

翻译过来大概就是:
如果指定了 -jar 选项,则其参数是包含应用程序的类和资源文件的 JAR 文件的名称。
启动类必须由其源代码中 manifest 的 Main-Class 指示。

说白了就是这个命令会去找 jar 文件中的 MANIFEST.MF 文件,MANIFEST.MF 文件中指定了真正的启动类。

打包命令做了什么

当我们将 spring-boot-maven-plugin 打包插件添加到项目中,运行打包命令后,打包时插件会把依赖的 Jar 文件一起打包进去,并在META-INF目录下生成一个MANIFEST.MF文件,这个文件里面包含了Start-Class和Main-Class。
打包文件结构如下:

BOOT-INF
│   ├── classes
│   │   ├── 项目文件
│   └── lib
│       ├── 第三方依赖的 jar
├── META-INF
│   ├── MANIFEST.MF
│   ├── app.properties
│   ├── maven
│   └── spring-configuration-metadata.json
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── MainMethodRunner.class
                ├── PropertiesLauncher$1.class
                ├── PropertiesLauncher$ArchiveEntryFilter.class
                ├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
                ├── PropertiesLauncher.class
                ├── WarLauncher.class
                ├── archive
                │   ├── Archive$Entry.class
                │   ├── Archive$EntryFilter.class
                │   ├── Archive.class
                │   ├── ExplodedArchive$1.class
                │   ├── ExplodedArchive$FileEntry.class
                │   ├── ExplodedArchive$FileEntryIterator$EntryComparator.class
                │   ├── ExplodedArchive$FileEntryIterator.class
                │   ├── ExplodedArchive.class
                │   ├── JarFileArchive$EntryIterator.class
                │   ├── JarFileArchive$JarFileEntry.class
                │   └── JarFileArchive.class
                ├── data
                │   ├── RandomAccessData.class
                │   ├── RandomAccessDataFile$1.class
                │   ├── RandomAccessDataFile$DataInputStream.class
                │   ├── RandomAccessDataFile$FileAccess.class
                │   └── RandomAccessDataFile.class
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── Bytes.class
                │   ├── CentralDirectoryEndRecord$1.class
                │   ├── CentralDirectoryEndRecord$Zip64End.class
                │   ├── CentralDirectoryEndRecord$Zip64Locator.class
                │   ├── CentralDirectoryEndRecord.class
                │   ├── CentralDirectoryFileHeader.class
                │   ├── CentralDirectoryParser.class
                │   ├── CentralDirectoryVisitor.class
                │   ├── FileHeader.class
                │   ├── Handler.class
                │   ├── JarEntry.class
                │   ├── JarEntryFilter.class
                │   ├── JarFile$1.class
                │   ├── JarFile$2.class
                │   ├── JarFile$JarFileType.class
                │   ├── JarFile.class
                │   ├── JarFileEntries$1.class
                │   ├── JarFileEntries$EntryIterator.class
                │   ├── JarFileEntries.class
                │   ├── JarURLConnection$1.class
                │   ├── JarURLConnection$2.class
                │   ├── JarURLConnection$CloseAction.class
                │   ├── JarURLConnection$JarEntryName.class
                │   ├── JarURLConnection.class
                │   ├── StringSequence.class
                │   └── ZipInflaterInputStream.class
                └── util
                    └── SystemPropertyUtils.class

MANIFEST.MF 文件内容如下:

Manifest-Version: 1.0
Implementation-Title: app-operation-service
Implementation-Version: 1.0.0-RELEASE
Archiver-Version: Plexus Archiver
Built-By: ambition
Implementation-Vendor-Id: com.ambition
Class-Path: lib/spring-boot-starter-2.2.5.RELEASE.jar
Spring-Boot-Version: 2.2.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.ambition.operation.AppOperationApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_211
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/app-operation/app-operation-service

可以看到里面有一个 Main-Class 和一个 Start-Class,其中 Start-Class 是我们项目应用的启动类,而 Main-Class 应该就是上面官方文档中提到的真正的启动类。那么问题又来了,我现在知道了java -jar命令会执行org.springframework.boot.loader.JarLauncher这个启动类,但是从结果来看好像是项目的启动类com.ambition.operation.AppOperationApplication被执行了,这个过程是怎样的呢?

看看代码吧

前因后果都知道了,现在来看看运行时代码层面做的事情,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);
    }

    protected boolean isNestedArchive(Entry entry) {
        return entry.isDirectory() ? entry.getName().equals("BOOT-INF/classes/") : entry.getName().startsWith("BOOT-INF/lib/");
    }

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

ExecutableArchiveLauncher的源码:

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;

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

    protected ExecutableArchiveLauncher(Archive archive) {
        this.archive = archive;
    }

    protected final Archive getArchive() {
        return this.archive;
    }

    protected String getMainClass() throws Exception {
    	// 获取文件
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
        	// 找到文件中指定的 Start-Class,也就是我们项目的启动类
            mainClass = manifest.getMainAttributes().getValue("Start-Class");
        }

        if (mainClass == null) {
            throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
        } else {
            return mainClass;
        }
    }

    protected List<Archive> getClassPathArchives() throws Exception {
        List<Archive> archives = new ArrayList(this.archive.getNestedArchives(this::isNestedArchive));
        this.postProcessClassPathArchives(archives);
        return archives;
    }

    protected abstract boolean isNestedArchive(Entry entry);

    protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
    }
}

Launcher的源码:

public abstract class Launcher {
    public Launcher() {
    }

    protected void launch(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        ClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());
        /**
         * 第一个参数是启动参数
         * 第二个参数是我们之前获取的项目启动类
         * 第三个参数是类加载器
         */
        this.launch(args, this.getMainClass(), classLoader);
    }

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

        while(var3.hasNext()) {
            Archive archive = (Archive)var3.next();
            urls.add(archive.getUrl());
        }

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

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

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

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

    protected abstract String getMainClass() throws Exception;

    protected abstract List<Archive> getClassPathArchives() throws Exception;

    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = this.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");
        } else {
            File root = new File(path);
            if (!root.exists()) {
                throw new IllegalStateException("Unable to determine code source archive from " + root);
            } else {
                return (Archive)(root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
            }
        }
    }
}

熟悉的两个常量,这不就是我们打包后的文件目录吗,classes 目录里面是我们项目编译后的文件,lib 目录里面是我们项目依赖的第三方文件。
跟一下JarLaunchermain方法的代码,最后调用的是父类Launcherlaunch方法,构造MainMethodRunner对象后调用它的run方法启动。

public class MainMethodRunner {
    private final String mainClassName;
    private final String[] args;

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

    public void run() throws Exception {
    	// 获取我们项目的启动类
        Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
        // 获取主方法
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        // 反射调用
        mainMethod.invoke((Object)null, this.args);
    }
}

就很直观了,加载文件中指定的 Start-Class,也就是我们项目的启动类,获取主方法并反射调用。
当然这只是很简单的看了一下代码,里面还有很多细节大家感兴趣的可以自己去探索,包括如何获取文件,如何处理 jar 里面的 jar 等等,因为 Java 没有提供任何标准的方式来加载嵌套的 jar 文件,可以为我们日常的工作提供一些思路。

结论

当我们在项目中加了 spring-boot-maven-plugin 打包插件后,打包时插件会把依赖的 jar 文件一起打包进去,并在 META-INF 目录下生成一个 MANIFEST.MF 文件,这个文件里面包含了 Start-Class 和 Main-Class。
因为 Java 没有提供任何标准的方式来加载嵌套的 jar 文件,所以就无法加载一起打包进去的依赖,而 Main-Class 就是帮我们加载嵌套的 jar 文件和 Class 文件的。
当我们运行java -jar命令时,就会去 MANIFEST.MF 文件中找 Main-Class,通过 Main-Class 中的 JarLauncher 去加载 BOOT-INF\classes 目录下的 Class 文件和 BOOT-INF\lib 目录下的 jar 文件,用反射去执行 Start-Class 类也就是我们项目的启动类,完成内嵌 Tomcat 的启动。


上海米哈游内推,福利好待遇高,五险二金,早晚餐零食水果下午茶烧烤,吃货天堂,还有奶茶咖啡券,旅游基金,内推奖励,看我这么卖力打广告就知道奖励力度有多大了,更有周年礼物年会抽奖等你来拿,欢迎大家自荐和推荐:https://app.mokahr.com/recommendation-apply/mihoyo/26460?recommendCode=NTAKBmA#/jobs?from=genPoster
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

从入门到脱发

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值