# Spring Boot JarLauncher

前面在 WebMvc 的部分咱有解析过，打war包运行，使用外部Servlet容器启动 SpringBoot 应用时，需要一个 `ServletInitializer` 来引导启动 SpringBoot 应用。那在使用jar包启动时，咱只是知道会走主启动类的 main 方法，但那是在开发时直接指定走主启动类的 main 方法，在jar包启动时是另一种方式。咱这最后一篇就来看看jar包启动 SpringBoot 应用的原理。

翻开打好的jar包，会发现3个文件夹：

* `BOOT-INF`：存放自己编写并编译好的 .class 文件和静态资源文件、配置文件等
* `META-INF`：有一个 `MANIFEST.MF` 的文件
* `org`：`spring-boot-loader` 的一些 .class 文件

其中，`org.springframework.boot.loader` 里开始能找到 .class 文件了。

翻看 `META-INF` 下面的 `MANIFEST.MF` 文件，发现里面的内容如下：

```properties
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.9.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class:org.springframework.boot.loader.JarLauncher
```

这个文件中有两个值得关注的：

* 在 `Start-Class` 中注明了 SpringBoot 的主启动类
* 在 `Main-Class` 中注明了一个类： `JarLauncher`

如果能靠 SpringBoot 的主启动类完成应用的启动，那为什么还要标注下面的那个 `JarLauncher` 呢？

## 1. JarLauncher是什么东西

```java
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);
    }

}
```

发现有个 **main** 方法！而且上面定义了两个常量，恰好就是在jar包中 `BOOT-INF` 里面的两个部分：自己的源码，和第三方jar包。

## 2. 测试直接启动两个带main方法的类

### 2.1 SpringBootApplication

**错误: 找不到或无法加载主类** `**com.example.demo.DemoApplication**`

发现启动失败，根本就找不到这个类。

### 2.2 JarLauncher

能正常启动，打印 Banner 等。

## 3. 【拓展】主启动类无法正常引导启动的原理

用正常的指令启动时，java 指令没有指定 classpath，而当前 SpringBoot 应用依赖的jar包均放在 `BOOT-INF/lib` 下，这部分无法被识别。

### 3.1 标准jar包的启动规范

在可执行jar包中，有一个规范：**被标记为Main-Class的类必须连同自己的包，直接放在jar包的最外层（没有额外的文件夹包含）。**

* `SpringBootApplication` 的位置：`"BOOT-INF/classes/com.example.demo.DemoApplication.class"`
* `JarLauncher` 的位置：`"org.springframework.boot.loader.JarLauncher"`

所以 `JarLauncher` 能引导成功，而直接运行主启动类却无法成功启动。

这也解释了另外一个现象：

SpringBoot 在打jar包时，没有直接将 `spring-boot-loader` 包直接依赖到lib目录，而是将这个包下面的所有 .class 文件都复制到要打的jar包中。

### 3.2 标准jar包的内嵌jar规范

可执行jar包中还有一个规范：**jar包中原则上不允许嵌套jar包。**

传统的打jar包的方式是将所有依赖的jar包都复制到一个新的jar包中。这样会出现一个致命问题：

如果两个不同的jar包中有一个**全限定类名相同的文件**，会出现**覆盖**现象。

SpringBoot 使用自定义的 `ClassLoader`，可以解决这个问题，具体的部分要剖析源码才能看到实现机制。

## 4. JarLauncher的main方法都做了什么

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

只有这一句，而且从上面的源码中可以发现，调用的空参数构造方法没有任何实际作用，也没有调父类的构造方法。

那一切的功能都在 `launch` 方法中。`launch` 方法不在 `JarLauncher` 里，在父类的 `Launcher` 内有定义：

```java
// 这个方法是一个入口点，且应该被一个public static void main(String[] args)调用
protected void launch(String[] args) throws Exception {
    //注册URL协议并清除应用缓存
    JarFile.registerUrlProtocolHandler();
    //设置类加载路径
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    //执行main方法
    launch(args, getMainClass(), classLoader);
}
```

从文档注释可以发现，这个方法必须被 main 方法调用，这跟上面的 `JarLauncher` 中 main 方法直接调用一致。

### 4.1 registerUrlProtocolHandler

```java
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";

private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";

public static void registerUrlProtocolHandler() {
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    System.setProperty(PROTOCOL_HANDLER,
           ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
    resetCachedUrlHandlers();
}

// 重置任何缓存的处理程序，以防万一已经使用了jar协议。
// 我们通过尝试设置null URLStreamHandlerFactory来重置处理程序，除了清除处理程序缓存之外，它应该没有任何效果。
private static void resetCachedUrlHandlers() {
    try {
        URL.setURLStreamHandlerFactory(null);
    }
    catch (Error ex) {
        // Ignore
    }
}
```

先设置当前系统的一个变量 `**java.protocol.handler.pkgs**`，而这个变量的作用，是设置 `URLStreamHandler` 实现类的包路径。

之后要重置缓存，目的是清除之前启动的残留（文档注释已标明）。

### 4.2 createClassLoader

它要来创建 `ClassLoader`，而创建之前先调了 `getClassPathArchives` 方法来取一些 `Archive` 对象。

```java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}
```

#### 4.2.1 getClassPathArchives

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

从最后看起，`isNestedArchive` 方法在调用时要传入 `Archive.Entry`，而这个参数的来源尚不明确，先搁置一边。

往前看，有一个 `this.archive`，而这个 `archive` 的成员属性是在这个类创建时被调用的。

```java
private final Archive archive;

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

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

`Archive` 对象最终创建在下面的 `createArchive` 方法。

到 `File root = new File` 之前的部分，这段代码都是在找当前类的所在jar包的绝对路径

之后下面把这个文件创建出来，并以此创建一个 `JarFileArchive` 对象。

而这个 `JarFileArchive` 是 `Archive` 的子类，这个 `Archive` 就可以被 `Launcher` 启动。文档注释和类定义：

```java
/**
* An archive that can be launched by the Launcher
*/
public interface Archive extends Iterable<Archive.Entry>
```

恰巧从 `Archive` 中得到一个意外收获：`Archive` 里的 `Archive.Entry` 可以被迭代！

跟前面的那个方法引用刚好能对应上了。

`isNestedArchive` 方法传入的参数就是 `archive` 对象中的那一组 `Entry` 对象（一个 `Entry` 相当于一个 `"File"`）。

#### 4.2.2 getNestedArchives

```java
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
    List<Archive> nestedArchives = new ArrayList<>();
    for (Entry entry : this) {
        if (filter.matches(entry)) {
            nestedArchives.add(getNestedArchive(entry));
        }
    }
    return Collections.unmodifiableList(nestedArchives);
}
```

`archive` 对象要执行 `getNestedArchives` 时，会传入一个 `EntryFilter`，以此来获取一组被嵌套的 `Archive` 。

而这个 `EntryFilter` 的工作机制就是上面的 `isNestedArchive` 方法，在 `JarLauncher` 中也有定义：

```java
protected boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
        return entry.getName().equals(BOOT_INF_CLASSES);
    }
    return entry.getName().startsWith(BOOT_INF_LIB);
}
```

很明显，看看是不是 `BOOT-INF/lib` 开头的jar包，如果不是，看看是不是 `BOOT-INF/classes` 文件夹。

这部分的意义正好跟前面测试主启动类与 `JarLauncher` 的启动相呼应：

位于 `BOOT-INF/classes` 的启动类**需要后续被扫描到，才能被处理**。

由此可见，得到的 `archives` 集合就是 `**BOOT-INF/classes**` 与 `**BOOT-INF/lib**` 下面的所有文件。

#### 4.2.3 postProcessClassPathArchives

```java
// 在使用之前调用后处理存档条目。实现可以添加和删除Entry。
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
```

这个后置处理的方法是空的，且没有子类重写，说明默认就是拿 `**BOOT-INF/classes**` 与 `**BOOT-INF/lib**` 下面的文件了。

### 4.3 createClassLoader

```java
// 为指定的归档文件创建一个类加载器
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[0]));
}

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

上面部分的源码很容易可以看出是将每个 `Archive` 的绝对路径保存到一个 `List` 中，之后调用下面的 `createClassLoader` 方法。

下面直接创建了一个 `LaunchedURLClassLoader`，传入的 `ClassLoader` 很明显是默认的，也就是 `AppClassLoader` 。

#### 4.3.1 构造方法

```java
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
    super(urls, parent);
}
```

很简单，直接调用父类的构造方法（指定 `ClassLoader` 是双亲委托机制）

而 `LaunchedURLClassLoader` 的父类是： `java.net.URLClassLoader` ，是jdk内部的 `ClassLoader`，不再深入描述。

### 4.4 launch

```java
launch(args, getMainClass(), classLoader);
```

调用 `launch` 之前会先调用 `getMainClass` 方法获取主启动类。

```java
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);
    }
    return mainClass;
}
```

它要从 `Launcher` 类的成员 `archive` 中获取 `Manifest` 文件，这个文件就是之前在 `META-INF` 下面的 `MANIFEST.MF` 文件。

之后从这个文件中取出 `Start-Class` 的值，这个值就是主启动类的全限定类名。

之后调用 `launch` 方法：

```java
// 根据一个Archive文件和一个完全配置好的ClassLoader启动应用。
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
    Thread.currentThread().setContextClassLoader(classLoader);
    createMainMethodRunner(mainClass, args, classLoader).run();
}
```

先设置当前线程的上下文类加载器为新的类加载器，也就是 `LaunchedURLClassLoader` （默认为 `AppClassLoader`）。

之后要开始创建 main 方法的运行器，并运行。

### 4.5 mainMethodRunner.run

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

简单的创建了 `MainMethodRunner` 的对象，之后上面会调用 run 方法。

`MainMethodRunner` 的结构：

```java
// 用于Launcher调用main方法的辅助类。使用当前线程上下文类加载器加载包含main方法的类。
public class MainMethodRunner {

    private final String mainClassName;

    private final String[] args;

    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args != null) ? 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(null, new Object[] { this.args });
    }

}
```

核心是 **run** 方法。

先拿到当前线程的上下文类加载器，就是 `LaunchedURLClassLoader` 。

之后用这个 `ClassLoader` 加载主启动类，之后运行 main 方法。

这也解释了为什么 `SpringBoot` 应用在开发期间只需要写 main 方法，引导启动即可。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ldbmcs.gitbook.io/java/java-frameworks-51/spring/spring-boot/spring-boot-jarlauncher.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
