Spring Boot Loader 源码分析

Spring Boot jar

我们之前就发现 Spring Boot 打包出来的 jar 解压后,有三个目录。

BOOT-INF

classes

当前工程编译好的结果文件,包含 src/main/java 和 src/main/resources 目录下的所有文件。

lib

当前工程依赖的所有 jar 文件(第三方 jar 包)。

META-INF

只有一个文件:MANIFEST.MF。

org

由 Spring Boot 提供的一堆字节码文件,入口类就在这里。

org.springframework.boot.loader.JarLauncher

org package 是怎么来的

BOOT-INF 是通过编译工程文件,META-INF 是打包时自动生成,那么 org package 是怎么来的?

plugins {
   id 'org.springframework.boot' version '2.1.4.RELEASE'
   id 'java'
}

还记得我们之间配置的 gradle plugin 么?既然打包是由 org.springframework.boot 这个插件完成,那么答案肯定在这里。

根据 org 的目录结构,尝试增加一个依赖到工程里。

org.springframework.boot:spring-boot-loader (在开发阶段,因为有 gradle 插件的存在,原则上这个依赖是不引入的,这里是研究需要

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter-web'
   implementation 'org.springframework.boot:spring-boot-loader'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

再观察 spring-boot-loader.jar,发现和上面的 org package 一模一样。

结论:说明 Spring Boot Gradle Plugin 在用 bootJar 任务对 Spring Boot 工程进行打包的时候,是把 spring-boot-loader.jar 这个 jar 解压,随后把解压内容放置在 Spring Boot 的应用 jar 里。而 spring-boot-loader.jar 这个依赖,肯定是在 Spring Boot Gradle Plugin 里被依赖的。

疑问:为什么不直接把这个依赖传递给实际的 Spring Boot 应用,而是采用这么麻烦的方式,把这个依赖的内容打包进实际的 Spring Boot 应用。

JarLauncher 启动类

根据 MANIFEST.MF 文件的描述。

Spring Boot 的启动类就是

Main-Class: org.springframework.boot.loader.JarLauncher

而 Spring Boot 的应用主类是

Start-Class: com.lichee.microservices.MicroservicesApplication

源码

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
 */
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);
   }

}

类关系图

从图中可以看出来,最顶层的启动器是 Launcher,其次是 ExecutableArchiveLauncher。最终的实现分别两种模式,一个是 JarLauncher(针对于 jar 归档文件),另一个是 WarLauncher(针对于 war 归档文件)。从这里也就说明了为什么 Spring Boot 的应用可以通过 jar 和 war 两种方式运行。

源码分析

通过代码可以发现,实际程序的入口是在:

new JarLauncher().launch(args);

实际调用的 launch 方法是 Launcher.java 里的 launch 方法。

/**
 * 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);
}

这里一共 3 个步骤,最重要的就是第 2 步 ClassLoader 类加载器。

registerUrlProtocolHandler

注册URL协议处理器,这个不重要,可以忽略。

JarFile.registerUrlProtocolHandler();

classLoader

最重要的就是自定义类加载器这个阶段。

ClassLoader classLoader = createClassLoader(getClassPathArchives());

getClassPathArchives

返回所有符合条件的 jar 或 工程文件,并包装成一个类型为 Archive 的 List 对象。这里的符合条件是指在 BOOT-INF/lib/ 下的 jar 文件,和在 BOOT-INF/classes/ 下的所有工程文件,用来构建 classpath。

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

createArchive

定位当前执行的具体 jar 文件或者文件目录,通过磁盘上的绝对路径来定位,返回一个 Archive 对象。

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

getNestedArchives

返回与指定过滤器(EntryFilter)所匹配的嵌套归档文件。

@Override
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);
}

isNestedArchive

EntryFilter 过滤器,判断 entry 所指定的文件(具体 jar 文件或者文件目录)是否满足条件 ,满足条件的应该添加到 classpath 里,每个指定的文件都会调用一次。

条件:在 BOOT-INF/classes/ 目录下的工程文件或者在 BOOT-INF/lib/ 目录下的第三方 jar 包。

注意:其实这里就是在判断,要执行的 jar 文件是否是按照 Spring Boot 特有的目录结构来放置工程文件,以及所依赖的第三方 jar 包。只有满足条件的工程文件或所依赖的第三方 jar 包才会进入下一步,也就是通过自定义类加载器来加载这些满足条件的文件,这里就需要 jar 文件规范相关知识。

static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";

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

postProcessClassPathArchives

是一个空方法,事后处理方法,回调方法。

/**
 * Called to post-process archive entries before they are used. Implementations can
 * add and remove entries.
 * @param archives the archives
 * @throws Exception if the post processing fails
 */
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}

createClassLoader

上面的所有方法,都是为了准备 List 对象,所有符合条件的 jar(BOOT-INF/lib/ )和工程文件(BOOT-INF/classes/ ),并包装成一个类型为 Archive 的 List 对象。

创建一个针对指定归档文件(Archive)的自定义类加载器,也就是用来加载 getClassPathArchives 所返回的集合(jar 或者 工程文件),应用类加载器(也可以叫做系统类加载器)是加载不了这些文件的,就必须自己创建一个新的类加载器,用来加载这些存在于自定义目录内的文件。

这个方法是把传入的 List 对象转撑一个 List 对象,URL 表示文件在磁盘上的绝对路径。

**
 * Create a classloader for the specified archives.
 * @param archives the archives
 * @return the classloader
 * @throws Exception if the classloader cannot be created
 */
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]));
}

LaunchedURLClassLoader

Spring Boot 提供的自定义类加载器,urls 表示所有需要加载文件的 url(jar 文件的绝对路径),getClass().getClassLoader() 表示父加载器(也就是应用类加载器)。

注意:在创建一个类加载器的时候,一定要指定它的父加载器 getClass().getClassLoader(),这个父加载器其实就是应用类加载器。

这个方法创建一个针对指定归档文件(URL)的类加载器。

/**
 * Create a classloader for the specified URLs.
 * @param urls the URLs
 * @return the classloader
 * @throws Exception if the classloader cannot be created
 */
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
   return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

launch

最后阶段,通过反射来完成工程应用启动。

launch(args, getMainClass(), classLoader);

getMainClass

返回应该被加载的主类。从 Manifest 对象种,获取 Start-Class 属性,这个 Start-Class 是什么呢?

MANIFEST.MF 文件中定义 Start-Class: com.lichee.microservices.MicroservicesApplication

@Override
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;
}

launch

通过给定的归档文件和全新的 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();
}

第一行代码把自定义的 classloader 设置到当前线程上下文类加载器,在默认情况下,当前线程上下文类加载器就是 AppClassLoader。通过这种方式就把当前线程上下文的默认类加载器换成了 Spring Boot 自定义的类加载器。

转换:AppClassLoader --> LaunchedURLClassLoader

现在是把类加载器放置进去,在未来某处肯定会从当前线程中取出这个上下文类加载器,然后进行类加载。

createMainMethodRunner

创建 MainMethodRunner,用于启动和加载应用。

其实这里的 classLoader 并没有用到。

/**
 * Create the {@code MainMethodRunner} used to launch the application.
 * @param mainClass the main class
 * @param args the incoming arguments
 * @param classLoader the classloader
 * @return the main method runner
 */
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
      ClassLoader classLoader) {
   return new MainMethodRunner(mainClass, args);
}

MainMethodRunner

使用当前线程上下文类加载器,加载一个包含了 main 方法的主类,然后调用这个主类的 main 方法。主要看这个 run 方法。

/**
 * Utility class that is used by {@link Launcher}s to call a main method. The class
 * containing the main method is loaded using the thread context class loader.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 */
public class MainMethodRunner {

   private final String mainClassName;

   private final String[] args;

   /**
    * Create a new {@link MainMethodRunner} instance.
    * @param mainClass the main class
    * @param args incoming arguments
    */
   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

当我们使用下面这个指令的时候,就会启动 Spring Boot 的应用,原理就在这个 run 方法。

java -jar microservices-0.0.1-SNAPSHOT.jar

Thread.currentThread().getContextClassLoader() 获取当前线程上下文类加载器,其实也就是获取我们之前已经设置好的 LaunchedURLClassLoader。之前从 MANIFEST.MF 文件中把 Start-Class 取出来,也就是我们的主类:com.lichee.microservices.MicroservicesApplication,通过 loadClass 方法就把主类加载到虚拟机中,于是得到主类的一个 Class 对象。

通过反射主类 Class 对象的方式,拿到主类的 main 方法。

Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);

通过反射方式直接调用 main 方法。

mainMethod.invoke(null, new Object[] { this.args });

总结下来三个步骤:

  1. 通过当前线程上下文类加载器加载 Start-Class 定义的主类到虚拟机中,拿到 Class 对象

  2. 通过 Class 对象反射拿到 main 方法

  3. 通过反射执行 main 方法

思考

为什么 Spring Boot 规定入口类(被 @SpringBootApplication 注释的类)的启动方法是 main 方法?

通过上面的源码分析,不需要非要 main 方法才可以,在反射阶段,完全可以换成另外一个普通的实例方法,只要在反射的时候,更换方法名就可以。

Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);

原因在于为了方便 Spring Boot 应用开发阶段的运行、调试,但是类加载器不一样。

两种运行方式

Spring Boot 可以通过以下两种方式运行,好处就是便利了日常开发、调试、测试阶段。

但是类加载器会有明显的区别,修改项目代码,把类加载器打印出来。

@SpringBootApplication
public class MicroservicesApplication {

    public static void main(String[] args) {

        System.out.println(MicroservicesApplication.class.getClassLoader());
        SpringApplication.run(MicroservicesApplication.class, args);
    }

}

jar

通过指令运行。从结果可以看出来是采用的 LaunchedURLClassLoader,也就是采用 Spring Boot 提供的全新类加载器。

java -jar microservices-0.0.1-SNAPSHOT.jar

main

直接在开发阶段的 ide(eclipse、idea)里右键 run 方式运行。从结果可以看出来是采用的 AppClassLoader,也就是采用应用类加载器。

总结

打包机制

Spring Boot 打包机制,通过 gradle plugin 的 org.springframework.boot 插件对应用打成 jar 包,在 jar 包的根目录放置 spring-boot-loader.jar 这个 jar 包解压缩后的所有文件。自定义 BOOT-INF 和 META-INF 两个目录,BOOT-INF/classes 放置工程文件,BOOT-INF/lib 放置工程三方依赖,MANIFEST.MF 文件中定义 Start-Class 和 Main-Class 属性。

运行机制

Spring Boot 运行机制,首先应用加载器(系统加载器)加载 org.springframework.boot.loader.JarLauncher。在加载 JarLauncher 的同时,创建一个 Spring Boot 特有的类加载器 LaunchedURLClassLoader,用这个特有的类加载器加载 BOOT-INF 下的工程文件和三方依赖。最后通过反射调用 Start-Class 应用入口类的 main 方法启动应用程序。

上面所讲的是 jar 包形式运行,开发阶段直接在 ide 里右键 run 运行工程,则是直接调用系统的 AppClassLoader 类加载器。

优雅方式

Spring Boot 通过自定义类加载器这种方式,优雅的解决了 jar 文件规范问题。至于把 spring-boot-loader.jar 这个 jar 包里的文件原封不动的拷贝过来,是为了给应用类加载器(系统加载器)一个程序入口。先加载 JarLauncher 到内容中,再通过自定义类加载器加载自己的应用。

这也回答了之前的问题,为什么不把 spring-boot-loader.jar 这个依赖传递给工程应用,而是在打包的时候,直接打包进工程应用的最顶层。

Last updated