avatar

目录
Spring Boot应用是如何以JAR包的方式启动

通常一个Spring Boot 单体应用的package方式会是jar包的方式,而且通常情况下只要在console以 java -jar 命令加上JAR包的完整名称就能启动应用,那么这个过程是如何实现的呢,不妨让我们在此一探究竟。

通常情况下,Spring Boot 应用打包成JAR包的方式来运行,通过解压一个空白Spring Boot工程(即,由IDE引导生成且不添加任何额外的starter依赖)package之后的 JAR包,再通过我们tree 命令查看其目录结构如下:

Spring Boot Jar Directory

从父目录来看,大致可以分为以下几大目录结构:

  • BOOT-INF/classes 目录存放应用编译后的class文件;
  • BOOT-INF/lib 目录存放应用依赖的jar包;
  • META-INF/ 目录存放应用相关的元信息,比如MANIFESTR.MF
  • org/ 目录存放Spring Boot相关的class文件

其中,META-INF目录底下的MANIFESTR.MF文件描述了整个JAR包的一些元信息。打开 MANIFEST.MF 文件,看看到底提供了一些什么信息。

Code
1
2
3
4
5
6
7
8
9
10
11
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3
Built-By: Administrator
Build-Jdk: 11.0.8-internal
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Version: 2.3.7.RELEASE
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx

这么多项信息,其中值得重点关注的是 Main-ClassStart-Class 这两项。

Main-Class 属性定义了 JAR 文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过java -jar xxx.jar来运行该 JAR 文件。

Start-Class 属性定义了整个Spring Boot 应用的程序入口类, 这表明其是一个带有 main() 方法的类。

因为我们的打包方式选择的是 JAR,因此自然而然地,要深入到对应的Launcher 类 JarLauncher.class 中去看看风景。

在项目的pom.xml文件中添加以下坐标,方便我们查看 JarLauncher.class 的源码。

xml
1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.3.7.RELEASE</version> <!-- 版本自定义 -->
<scope>provided</scope>
</dependency>

由它的类结构图可以看到,JarLauncher 类 继承了 ExecutableArchiveLauncher,而ExecutableArchiveLauncher 又继承了Launcher 类,后两者都是抽象类。在 JarLauncher 中,看到它的 main() 方法如下:

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

继续看 JarLauncherlaunch(args)方法,这个方法的实现放在了抽象基类 Launcher 里边。

java
1
2
3
4
5
6
7
8
9
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler(); //1
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); //2
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader); //3
}

一共三步:

  1. 注册jar协议处理器,实际上是注册了 java.protocol.handler.pkgs 这样一个属性, 以便定位到一个确定的URLStreamHandler 去处理 jar URLs . 如何定位到一个具体的 URLStreamHandler, 则是根据具体的 protocol 去 URL::getURLStreamHandler(String protocol) 方法中搜寻了。针对 JAR 协议,调用URLStreamHandler.openConnection()方法会拿到一个JarURLConnection,通过这个类来连接上JAR文件后,便可以获取JAR文件里头的信息。 注册完成之后,会清除掉以前设置过的 URLStreamHandler.

    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * Reset any cached handlers just in case a jar protocol has already been used. We
    * reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
    * should have no effect other than clearing the handlers cache.
    */
    private static void resetCachedUrlHandlers() {
    try {
    URL.setURLStreamHandlerFactory(null);
    }
    catch (Error ex) {
    // Ignore
    }
    }
  2. 创建类加载器。

    java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(guessClassPathSize());
    while (archives.hasNext()) {
    urls.add(archives.next().getUrl());
    }
    if (this.classPathIndex != null) {
    urls.addAll(this.classPathIndex.getUrls());
    }
    return createClassLoader(urls.toArray(new URL[0]));
    }

    获得所有 archives 的 URL, 并为 所有的 URL 创建类加载器。这里的 Archive 集合主要存放的是 JAR 解压过后的位于 BOOT-INF/classes/

    BOOT-INF/lib/ 目录底下的class文件。

  3. 取得 LauncherClass 名称, 并执行 launch() 方法。注意到这里首先会通过 System.getProperty("jarmode") 尝试获取jarmode该属性,缺省条件下该值为 null. 因此 LauncherClass 名称由 getMainClass() 方法取得。

    最后执行 launch(args, launchClass, classLoader); 方法。相关源码如下:

    java
    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
    ----Launcher.class----
    protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
    Thread.currentThread().setContextClassLoader(classLoader);
    createMainMethodRunner(launchClass, args, classLoader).run();
    }

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

    ----MainMethodRunner.class---

    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 = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    mainMethod.setAccessible(true);
    mainMethod.invoke(null, new Object[] { this.args });
    }

可以看到,MainMethodRunner 对象关联 mainClass 及 main 方法参数 args,并最终通过反射的方式,执行mainClass中的main()方法来启动整个应用的。 mainClass 属性从何而来呢?上面说了,由getMainClass() 方法获取。具体源码如下:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
private static final String START_CLASS_ATTRIBUTE = "Start-Class";

protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}

可以看到,实际上, 是读取了 MANIFEST.MF 文件中的 Start-Class 属性的值,并将其赋给了 mainClass. 也就是说, 在这个示例中, mainClass 对应到的是com.example.demo.DemoApplication, 也就是整个Spring Boot 应用的入口类。

综上,我们一步步地看到了Spring Boot 应用由 java -jar 命令开始,是怎样一步步地找到应用的入口类,进而从入口类的main()方法开始执行,从而成功启动整个应用的。

文章作者: JanGin
文章链接: http://jangin.github.io/2021/06/26/how-does-a-spring-boot-application-starting-via-jar-package/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 JanGin's BLOG

评论