歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
能獨立執行的jar檔案
在開發springboot應用時,通過java -jar命令啟動應用是常用的方式,今天就來一起了解這個簡單操作背後的技術;
開發demo
開發一個springboot應用作為本次研究的物件,對應的版本資訊如下:
- JDK:1.8.0_211
- springboot:2.3.1.RELEASE
- maven:3.6.0
接下來開發springboot應用,這個應用非常簡單:
- springboot應用名為springbootstarterdemo,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>
- 只有一個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();
}
}
- 編碼完成,在pom.xml所在目錄執行命令
mvn clean package -U -DskipTests
- 構建成功後,在target目錄下得到檔案springbootstarterdemo-0.0.1-SNAPSHOT.jar
- 就是這個springbootstarterdemo-0.0.1-SNAPSHOT.jar,此時執行java -jar springbootstarterdemo-0.0.1-SNAPSHOT.jar就能啟動應用,如下圖:
接下來就用這個springbootstarterdemo-0.0.1-SNAPSHOT.jar來分析jar檔案能夠獨立啟動的原因;
java -jar做了什麼
-
先要弄清楚java -jar命令做了什麼,在oracle官網找到了該命令的描述:
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.
-
再次秀出我蹩腳的英文翻譯:
- 使用-jar引數時,後面的引數是的jar檔名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);
- 該jar檔案中包含的是class和資原始檔;
- 在manifest檔案中有Main-Class的定義;
- Main-Class的原始碼中指定了整個應用的啟動類;(in its source code)
小結一下:
java -jar會去找jar中的manifest檔案,在那裡面找到真正的啟動類;
探查springbootstarterdemo-0.0.1-SNAPSHOT.jar
- springbootstarterdemo-0.0.1-SNAPSHOT.jar是前面的springboot工程的構建結果,是個壓縮包,用常見的壓縮工具就能解壓,我這裡的環境是MacBook Pro,用unzip即可解壓;
- 解壓後有很多內容,我們先關注manifest相關的,下圖紅框中就是manifest檔案:
- 開啟上圖紅框中的檔案,內容如下:
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
- 在上述內容可見Main-Class的值org.springframework.boot.loader.JarLauncher,這個和前面的java官方文件對應上了,正是這個JarLauncher類的程式碼中指定了真正的啟動類;
疑惑出現
- 在MANIFEST.MF檔案中有這麼一行內容:
Start-Class: com.bolingcavalry.springbootstarterdemo.Springbootstarter
demoApplication
- 前面的java官方文件中,只提到過Main-Class ,並沒有提到Start-Class;
- Start-Class的值是SpringbootstarterdemoApplication,這是我們的java程式碼中的唯一類,也只真正的應用啟動類;
- 所以問題就來了:理論上看,執行java -jar命令時JarLauncher類會被執行,但實際上是SpringbootstarterdemoApplication被執行了,這其中發生了什麼呢?
猜測
動手之前先猜一下,個人覺得原因應該如下:
- java -jar命令會啟動JarLauncher;
- Start-Class是給JarLauncher用的;
- JarLauncher根據Start-Class找到了SpringbootstarterdemoApplication,然後執行它;
分析JarLauncher
- 先下載SpringBoot原始碼,我下載的是2.3.1版本,地址:https://github.com/spring-projects/spring-boot/releases/tag/v2.3.1.RELEASE
- JarLauncher所在的工程是spring-boot-loader,先弄明白JarLauncher的繼承關係,如下圖,可見JarLauncher繼承自ExecutableArchiveLauncher,而ExecutableArchiveLauncher的父類Launcher位於最頂層,是個抽象類:
3. java -jar執行的是JarLauncher的main方法,如下,會例項化一個JarLauncher物件,然後執行其launch方法,並且將所有入參都帶入:
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
- 上面的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);
}
- 可見要重點關注的是getMainClass()方法,在看這個方法之前,我們先去關注一個重要的成員變數archive,是JarLauncher的父類ExecutableArchiveLauncher的archive,如下可見,該變數又來自方法createArchive:
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
this.classPathIndex = getClassPathIndex(this.archive);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
- 方法來自Launcher.createArchive,如下所示,可見成員變數archive實際上是個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));
}
- 現在回到getMainClass()方法,可見this.archive.getManifest方法返回的是META-INF/MANIFEST.MF檔案的內容,然後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;
}
- 從上述分析可知:getMainClass()方法返回的是META-INF/MANIFEST.MF中取得了Start-Class的屬性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);
}
- 展開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檔案中額外生成了名為Start-Class的配置,這裡面是我們編寫的應用啟動類SpringbootstarterdemoApplication:
啟動類是JarLauncher,它是如何與MANIFEST.MF檔案關聯的呢?從下圖可以看出,最終是通過JarFile類的成員變數manifestSupplier關聯上的:
再來看看關鍵程式碼的執行情況,如下圖:
至此,SpringBoot的jar獨立執行的基本原理已經清楚,探究的過程中,除了熟悉關鍵程式碼流程,還對jar中的檔案有了更多瞭解,如果您正在學習SpringBoot,希望本文能給您一些參考;
官方文件
- 最後附上SpringBoot官方文件,可以看到Start-Class描述資訊:
- 上述文件明確提到:Start-Class定義的是實際的啟動類,此時的您應該對一切都瞭然於胸,產生本該如此的感慨;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos