# 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 方法，引导启动即可。
