# 误区
开始学习及使用 [`Spring Boot`](https://spring.io/) 的时候,通常都编写过以下代码:
```java
package com.example.myapplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```
接着通过 [`Gradle`](https://gradle.org/) 或者 [`Maven`](http://maven.apache.org/) 构建工具打包得到文件`app.jar`,然后输入以下命令启动:
```bash
java -jar app.jar
```
这里读者可能会误以为 `JVM` 是直接调用的 `Application#main` 函数,然后开始执行 `SpringApplication.run(Application.class, args);`。
接下来就开始解释这为什么会是`误解`。
# 分析
首先我们分析一下 `java -jar` 这个操作到底做了什么。
查询 `java` 命令手册之后,在描述区有这么一句:
> By default, the first argument that is not an option of the java command is the fully qualified name of the class to be called.
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.
重点在于最后一句:启动类必须指定在 `Manifest` 清单中的 `Main-Class` 头中。
当然,直接查看 `-jar` option 也能找打类似的描述:
```text
-jar filename
Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains
a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that
serves as your application's starting point.
When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are
ignored.
```
至此我们已经了解到执行 `java -jar app.jar` 的第一步是寻找 `Manifest` 清单中的 `Main-Class` 头,找到`启动类`之后才进行调用 `#main` 函数。
# 解压 `app.jar`
`app.jar` 是经过 `Gradle` 或者 `Maven` 的 `spring boot plugin` 插件特殊处理。解压结果如下:
```text
app.jar
|
+-META-INF
| +-MANIFEST.MF
+-org
| +-springframework
| +-boot
| +-loader
| +-
+-BOOT-INF
+-com
| +-example
| +-myapplication
| +-Application.class
| +-Other classes...
+-lib
+-dependency1.jar
+-dependency2.jar
```
> 可以从以下链接找到 `jar` 文件的规格要求。
https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html
打开文件 `META-INF/MANIFEST.MF`,内容如下:
```manifest
Manifest-Version: 1.0
Implementation-Title: Example Application
Implementation-Version: 1.1.0
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.myapplication.Application
Spring-Boot-Version: 2.2.1.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
```
该文件指定 `org.springframework.boot.loader.JarLauncher` 为 `Main-Class`,也就意味着通过 `java -jar app.jar` 命令运行后首先执行的应该是 `JarLauncher#main`。
> `JarLauncher` 文件可以在以下链接找到。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java
# 突破
`JarLauncher` 是 `spring boot loader` 子模块中的一个类,内容如下:
```java
package org.springframework.boot.loader;
import org.springframework.boot.loader.archive.Archive;
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
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);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
```
JarLauncher#main 中调用的 Launcher#launch(args) 实际上是调用父类 `org.springframework.boot.loader.Launcher#launch(args)` 方法。
> `org.springframework.boot.loader.Launcher` 可以在以下链接中找到。
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java
`Launcher` 的主要的方法如下:
```java
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
/**
* Launch the application given the archive file and a fully configured classloader.
* @param args the incoming arguments
* @param mainClass the main class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
```
大致的思路如下:
- 遍历 jar 包中的依赖(Nest Jar)和 `classes`,并创建一个 `ClassLoader`;
- 根据清单文件(META-INF/Manifest.MF)找到所指定的 `Start-Class`:启动类;
- 将刚刚创建的 `ClassLoader` 设置到当前线程中。
- 最后通过反射找到`启动类`的 `main` 函数,并调用该函数。
直到最后一步执行完成,才开始真正执行 `Application@main` 函数,才正式进入 `Spring Boot` 的初始化。
Spring Boot Fat Jar 启动原理