关于云计算:springboot的jar为何能独立运行

33次阅读

共计 9650 个字符,预计需要花费 25 分钟才能阅读完成。

欢送拜访我的 GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,波及 Java、Docker、Kubernetes、DevOPS 等;

欢送拜访我的 GitHub

https://github.com/zq2599/blog_demos
内容:所有原创文章分类汇总及配套源码,波及 Java、Docker、Kubernetes、DevOPS 等;

能独立运行的 jar 文件

在开发 springboot 利用时,通过 <font color=”blue”>java -jar</font> 命令启动利用是罕用的形式,明天就来一起理解这个简略操作背地的技术;

开发 demo

开发一个 springboot 利用作为本次钻研的对象,对应的版本信息如下:

  • JDK:1.8.0_211
  • springboot:2.3.1.RELEASE
  • maven:3.6.0

接下来开发 springboot 利用,这个利用非常简单:

  1. springboot 利用名为 <font color=”blue”>springbootstarterdemo</font>,pom.xml 文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.bolingcavalry</groupId>
    <artifactId>springbootstarterdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springbootstarterdemo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 只有一个 java 类,外面有个 http 接口:
package com.bolingcavalry.springbootstarterdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

@SpringBootApplication
@RestController
public class SpringbootstarterdemoApplication {public static void main(String[] args) {SpringApplication.run(SpringbootstarterdemoApplication.class, args);
    }
    @RequestMapping(value = "/hello")
    public String hello(){return "hello" + new Date();
    }
}
  1. 编码实现,在 pom.xml 所在目录执行命令
mvn clean package -U -DskipTests
  1. 构建胜利后,在 target 目录下失去文件 <font color=”blue”>springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>
  2. 就是这个 <font color=”blue”>springbootstarterdemo-0.0.1-SNAPSHOT.jar</font>,此时执行 <font color=”blue”>java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar</font> 就能启动利用,如下图:


接下来就用这个 <font color=”blue”>springbootstarterdemo-0.0.1-SNAPSHOT.jar</font> 来剖析 jar 文件可能独立启动的起因;

java -jar 做了什么

  • 先要弄清楚 <font color=”blue”>java -jar</font> 命令做了什么,在 oracle 官网找到了该命令的形容:

<font color=”blue”>
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.
</font>

  • 再次秀出我糟糕的英文翻译:
  1. 应用 <font color=”blue”>-jar</font> 参数时,前面的参数是的 jar 文件名 (本例中是 springbootstarterdemo-0.0.1-SNAPSHOT.jar);
  2. 该 jar 文件中蕴含的是 class 和资源文件;
  3. 在 manifest 文件中有 <font color=”blue”>Main-Class</font> 的定义;
  4. <font color=”blue”>Main-Class</font> 的源码中指定了整个利用的启动类;(in its source code)

小结一下:
<font color=”blue”>java -jar</font> 会去找 jar 中的 manifest 文件,在那外面找到真正的启动类;

探查 springbootstarterdemo-0.0.1-SNAPSHOT.jar

  1. <font color=”blue”>springbootstarterdemo-0.0.1-SNAPSHOT.jar</font> 是后面的 springboot 工程的构建后果,是个压缩包,用常见的压缩工具就能解压,我这里的环境是 MacBook Pro,用 unzip 即可解压;
  2. 解压后有很多内容,咱们先关注 manifest 相干的,下图红框中就是 manifest 文件:

  1. 关上上图红框中的文件,内容如下:
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springbootstarterdemo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.1.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
  1. 在上述内容可见 Main-Class 的值 <font color=”blue”>org.springframework.boot.loader.JarLauncher</font>,这个和后面的 java 官网文档对应上了,正是这个 JarLauncher 类的代码中指定了真正的启动类;

纳闷呈现

  1. 在 MANIFEST.MF 文件中有这么一行内容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
 demoApplication
  1. 后面的 java 官网文档中,只提到过 <font color=”blue”>Main-Class </font>,并没有提到 <font color=”blue”>Start-Class</font>;
  2. Start-Class 的值是 SpringbootstarterdemoApplication,这是咱们的 java 代码中的惟一类,也只真正的利用启动类;
  3. 所以问题就来了:实践上看,执行 java -jar 命令时 JarLauncher 类会被执行,但实际上是 SpringbootstarterdemoApplication 被执行了,这其中产生了什么呢?

猜想

入手之前先猜一下,集体感觉起因应该如下:

  1. java -jar 命令会启动 JarLauncher;
  2. <font color=”blue”>Start-Class</font> 是给 JarLauncher 用的;
  3. JarLauncher 依据 <font color=”blue”>Start-Class</font> 找到了 SpringbootstarterdemoApplication,而后执行它;

剖析 JarLauncher

  1. 先下载 SpringBoot 源码,我下载的是 2.3.1 版本,地址:https://github.com/spring-pro…
  2. JarLauncher 所在的工程是 spring-boot-loader,先弄明确 JarLauncher 的继承关系,如下图,可见 JarLauncher 继承自 ExecutableArchiveLauncher,而 ExecutableArchiveLauncher 的父类 Launcher 位于最顶层,是个抽象类:

  1. java -jar 执行的是 JarLauncher 的 main 办法,如下,会实例化一个 JarLauncher 对象,而后执行其 launch 办法,并且将所有入参都带入:
public static void main(String[] args) throws Exception {new JarLauncher().launch(args);
}
  1. 下面的 launch 办法在父类 Launcher 中:
protected void launch(String[] args) throws Exception {
    // 将 jar 解压后运行的形式叫做 exploded mode
    // 如果是 exploded mode,就不能反对通过 URL 加载 jar
    // 如果不是 exploded mode, 就能够通过 URL 加载 jar
    if (!isExploded()) {
        // 如果容许通过 URL 加载 jar,就在此注册对应的解决类
        JarFile.registerUrlProtocolHandler();}
    // 创立 classLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
    // jarmode 是创立 docker 镜像时用到的参数,应用该参数是为了生成带有多个 layer 信息的镜像
    // 这里临时不关注 jarmode
    String jarMode = System.getProperty("jarmode");
    // 如果没有 jarmode 参数,launchClass 的值就来自 getMainClass() 返回
    String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
    launch(args, launchClass, classLoader);
}
  1. 可见要重点关注的是 <font color=”blue”>getMainClass()</font> 办法,在看这个办法之前,咱们先去关注一个重要的成员变量 <font color=”blue”>archive</font>,是 JarLauncher 的父类 ExecutableArchiveLauncher 的 archive,如下可见,该变量又来自办法 <font color=”blue”>createArchive</font>:
public ExecutableArchiveLauncher() {
        try {this.archive = createArchive();
            this.classPathIndex = getClassPathIndex(this.archive);
        }
        catch (Exception ex) {throw new IllegalStateException(ex);
        }
    }
  1. 办法来自 Launcher.createArchive,如下所示,可见成员变量 <font color=”blue”>archive</font> 实际上是个 JarFileArchive 对象:
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));
    }
  1. 当初回到 getMainClass() 办法,可见 <font color=”blue”>this.archive.getManifest</font> 办法返回的是 <font color=”red”>META-INF/MANIFEST.MF</font> 文件的内容,而后 getValue(START_CLASS_ATTRIBUTE) 办法实际上就是从 META-INF/MANIFEST.MF 中获得了 Start-Class 的属性:
@Override
    protected String getMainClass() throws Exception {
        // 对应的是 JarFileArchive.getManifest 办法,// 进去后发现对应的就是 JarFile.getManifest 办法,// JarFile.getManifest 对应的就是 META-INF/MANIFEST.MF 文件的内容
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
            // 对应的是 META-INF/MANIFEST.MF 文件中的 Start-Class 的属性
            mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
        }
        if (mainClass == null) {throw new IllegalStateException("No'Start-Class'manifest entry specified in" + this);
        }
        return mainClass;
    }
  1. 从上述剖析可知:getMainClass() 办法返回的是 META-INF/MANIFEST.MF 中获得了 <font color=”red”>Start-Class</font> 的属性 com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication,再次回到 launch 办法中,可见最终运行的代码是 launch(args, launchClass, classLoader),它的 launchClass 参数就是 com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication:
protected void launch(String[] args) throws Exception {if (!isExploded()) {JarFile.registerUrlProtocolHandler();
        }
        ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        // 这里的 launchClass 等于 "com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
        String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
        // 这里就是启动 SpringbootstarterdemoApplication 的中央
        launch(args, launchClass, classLoader);
    }
  1. 开展 launch(args, launchClass, classLoader),最终查到了 MainMethodRunner 类:
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) {
        // mainClassName 被赋值为 "com.bolingcavalry.springbootstarterdemo.SpringbootstarterdemoApplication"
        this.mainClassName = mainClass;
        this.args = (args != null) ? args.clone() : null;}

    public void run() throws Exception {
        // 失去 SpringbootstarterdemoApplication 的 Class 对象
        Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
        // 失去 SpringbootstarterdemoApplication 的 main 办法对象
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.setAccessible(true);
        // 通过反射执行 main 办法
        mainMethod.invoke(null, new Object[] {this.args});
    }
}

终于,水落石出了;

小结

最初尽可能简短做个小结,先看 jar 是如何产生的,如下图,maven 插件生成的 jar 文件中,有常见的 class、jar,也有合乎 java 标准的 MANIFEST.MF 文件,并且,还在 MANIFEST.MF 文件中额定生成了名为 <font color=”blue”>Start-Class</font> 的配置,这外面是咱们编写的利用启动类 <font color=”blue”>SpringbootstarterdemoApplication</font>:

启动类是 <font color=”blue”>JarLauncher</font>,它是如何与 MANIFEST.MF 文件关联的呢?从下图能够看出,最终是通过 JarFile 类的成员变量 <font color=”blue”>manifestSupplier</font> 关联上的:


再来看看要害代码的执行状况,如下图:

至此,SpringBoot 的 jar 独立运行的基本原理曾经分明,探索的过程中,除了相熟要害代码流程,还对 jar 中的文件有了更多理解,如果您正在学习 SpringBoot,心愿本文能给您一些参考;

官网文档

  1. 最初附上 SpringBoot 官网文档,能够看到 <font color=”blue”>Start-Class</font> 形容信息:

  1. 上述文档明确提到:Start-Class 定义的是理论的启动类,此时的您应该对所有都了然于胸,产生本该如此的感叹;

你不孤独,欣宸原创一路相伴

  1. Java 系列
  2. Spring 系列
  3. Docker 系列
  4. kubernetes 系列
  5. 数据库 + 中间件系列
  6. DevOps 系列

欢送关注公众号:程序员欣宸

微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游 Java 世界 …
https://github.com/zq2599/blog_demos

正文完
 0