SpringBoot 程序是如何通过java -jar 启动的

SpringBoot打包方式主要有 jar 和 war 两种,以下仅针对 jar 进行分析

MANIFEST.MF

当我们将程序打成 Jar,我们总会发现在 META-INF 文件夹中有一个 MANIFEST.MF 文件,该文件包含了该 Jar 包的主要信息。如果算可执行的 Jar 包,该文件中还会包含一个 Main-Class 属性,表明方法入口。以下是一个 SpringBoot 程序的 MANIFEST.MF 内容

1
2
3
4
5
6
7
8
9
10
11
12
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.yxd.es.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher

Archive

  • archive即归档文件,这个概念在linux下比较常见
  • 通常就是一个tar/zip格式的压缩包
  • jar是zip格式

在spring boot里,抽象出了Archive的概念。

一个archive可以是一个jar(JarFileArchive),也可以是一个文件目录(ExplodedArchive)。可以理解为Spring boot抽象出来的统一访问资源的层。

上面的demo-0.0.1-SNAPSHOT.jar 是一个Archive,然后demo-0.0.1-SNAPSHOT.jar里的/lib目录下面的每一个Jar包,也是一个Archive。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
/**获取当前归档的URL*/
URL getUrl();
/**归档的信息*/
Manifest getManifest();
/**获取嵌套的子归档*/
Iterator<Archive> getNestedArchives(EntryFilter searchFilter, EntryFilter includeFilter);
boolean isExploded()

interface Entry {
boolean isDirectory();
String getName();
}

@FunctionalInterface
interface EntryFilter {
boolean matches(Entry entry);
}
}

Launcher

值得注意的是,上面的 MANIFEST 文件中的 Main-Class 属性并不是我们 SpringBoot 程序中的启动类,而 Start-Class 属性才是。

在打成Jar包的时候,SpringBoot 插件会在org/springframework/boot/loader 目录中载入以下几个文件

其中就有 Main-Class 属性中的 JarLauncher 类,也就是说 java -jar 之后会调用 JarLauncher#main来启动程序。类似的,如果我们打成的是 war 包,则会调用 WarLauncher#main

主要类继承关系如下:

主要流程如下:

1
2
3
4
5
6
public class JarLauncher extends ExecutableArchiveLauncher {
...
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public abstract class Launcher {
...
protected void launch(String[] args) throws Exception {
// 注册JarUrl协议. 这一步的主要作用是向系统注册新的协议,使boot可以使用自己的方式获取资源信息
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
// 创建ClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
// 获取 Start-Class,也就是SpringBoot程序的启动类
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}

protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
// 设置类加载器
Thread.currentThread().setContextClassLoader(classLoader);
// 通过反射执行启动类的 main 方法
createMainMethodRunner(launchClass, args, classLoader).run();
}

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
// 返回main方法的执行类
return new MainMethodRunner(mainClass, args);
}
...
}

上述过程中有一个获取类加载器的方法,大致流程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public abstract class ExecutableArchiveLauncher extends Launcher {
...
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
// 获取过滤器,在Jar环境中要求归档路径必须以 BOOT-INF/ 开头
Archive.EntryFilter searchFilter = this::isSearchCandidate;
// 获取过滤后的Archive
// 除了要求满足 searchFilter,还要是 BOOT-INF/classes/ 或者 BOOT-INF/lib/ 路径下的 Archive
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives;
}
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Launcher {
...
/**
* 通过上述getClassPathArchivesIterator()方法中获取的Archive信息获取ClassLoader能够加载的路径,并创建ClassLoader
*/
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(50);
while (archives.hasNext()) {
Archive archive = archives.next();
urls.add(archive.getUrl());
archive.close();
}
return createClassLoader(urls.toArray(new URL[0]));
}
...
}

启动程序调用类

1
2
3
4
5
6
7
8
9
10
11
public class MainMethodRunner {
...
public void run() throws Exception {
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
// 反射获取 main 方法
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
...
}

之后便是执行SpringApplication#run 方法启动真正的程序,然后就是加载配置、加载Bean等等。