溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

簡單分析SpringBoot java-jar命令行啟動原理

發(fā)布時間:2020-07-01 15:52:18 來源:億速云 閱讀:221 作者:清晨 欄目:開發(fā)技術

小編給大家分享一下簡單分析SpringBoot java-jar命令行啟動原理,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討方法吧!

在spring boot里,很吸引人的一個特性是可以直接把應用打包成為一個jar/war,然后這個jar/war是可以直接啟動的,而不需要另外配置一個Web Server。那么spring boot如何啟動的呢?今天我們就來一起探究一下它的原理。首先我們來創(chuàng)建一個基本的spring boot工程來幫助我們分析,本次spring boot版本為 2.2.5.RELEASE。

// SpringBootDemo.java
@SpringBootApplication
public class SpringBootDemo {
 public static void main(String[] args) {
  SpringApplication.run(SpringBootDemo.class);
 }

}

下面是pom依賴:

<dependencies>
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
</dependencies>

<build>
 <finalName>springboot-demo</finalName>
 <plugins>
  <plugin>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin>
 </plugins>
</build>

創(chuàng)建完工程后,執(zhí)行maven的打包命令,會生成兩個jar文件:

springboot-demo.jar
springboot-demo.jar.original

其中springboot-demo.jar.original是默認的maven-jar-plugin生成的包。springboot-demo.jar是spring boot maven插件生成的jar包,里面包含了應用的依賴,以及spring boot相關的類。下面稱之為executable jar或者fat jar。后者僅包含應用編譯后的本地資源,而前者引入了相關的第三方依賴,這點從文件大小也能看出。

簡單分析SpringBoot java-jar命令行啟動原理

圖1

關于executable jar,spring boot官方文檔中是這樣解釋的。

Executable jars (sometimes called “fat jars”) are archives containing your compiled classes along with all of the jar dependencies that your code needs to run.

Executable jar(有時稱為“fat jars”)是包含您的已編譯類以及代碼需要運行的所有jar依賴項的歸檔文件。

Java does not provide any standard way to load nested jar files (that is, jar files that are themselves contained within a jar). This can be problematic if you need to distribute a self-contained application that can be run from the command line without unpacking.

Java沒有提供任何標準的方式來加載嵌套的jar文件(即,它們本身包含在jar中的jar文件)。如果您需要分發(fā)一個自包含的應用程序,而該應用程序可以從命令行運行而無需解壓縮,則可能會出現(xiàn)問題。

To solve this problem, many developers use “shaded” jars. A shaded jar packages all classes, from all jars, into a single “uber jar”. The problem with shaded jars is that it becomes hard to see which libraries are actually in your application. It can also be problematic if the same filename is used (but with different content) in multiple jars.

為了解決這個問題,許多開發(fā)人員使用 shaded jars。 一個 shaded jar 將來自所有jar的所有類打包到一個 uber(超級)jar 中。 shaded jars的問題在于,很難查看應用程序中實際包含哪些庫。 如果在多個jar中使用相同的文件名(但具有不同的內(nèi)容),也可能會產(chǎn)生問題。

Spring Boot takes a different approach and lets you actually nest jars directly.

Spring Boot采用了另一種方法,實際上允許您直接嵌套jar。

簡單來說,Java標準中是沒有來加載嵌套的jar文件,就是jar中的jar的方式的,為了解決這一問題,很多開發(fā)人員采用shaded jars,但是這種方式會有一些問題,而spring boot采用了不同于shaded jars的另一種方式。

Executable Jar 文件結(jié)構

那么spring boot具體是如何實現(xiàn)的呢?帶著這個疑問,先來查看spring boot打好的包的目錄結(jié)構(不重要的省略掉):

簡單分析SpringBoot java-jar命令行啟動原理

圖6

可以發(fā)現(xiàn),文件目錄遵循了下面的規(guī)范:

Application classes should be placed in a nestedBOOT-INF/classesdirectory. Dependencies should be placed in a nested BOOT-INF/libdirectory.

應用程序類應該放在嵌套的BOOT-INF/classes目錄中。依賴項應該放在嵌套的BOOT-INF/lib目錄中。

我們通常在服務器中使用java -jar命令啟動我們的應用程序,在Java官方文檔是這樣描述的:

Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application's starting point.

執(zhí)行封裝在JAR文件中的程序。filename參數(shù)是具有清單的JAR文件的名稱,該清單包含Main-Class:classname形式的行,該行使用公共靜態(tài)void main(String [] args)方法定義該類,該方法充當應用程序的起點。

When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are ignored.

使用-jar選項時,指定的JAR文件是所有用戶類的源,而其他類路徑設置將被忽略。

簡單說就是,java -jar 命令引導的具體啟動類必須配置在清單文件 MANIFEST.MF 的 Main-Class 屬性中,該命令用來引導標準可執(zhí)行的jar文件,讀取的是 MANIFEST.MF文件的Main-Class 屬性值,Main-Class 也就是定義包含了main方法的類代表了應用程序執(zhí)行入口類。

那么回過頭再去看下之前打包好、解壓之后的文件目錄,找到 /META-INF/MANIFEST.MF 文件,看下元數(shù)據(jù):

Manifest-Version: 1.0 Implementation-Title: spring-boot-demo Implementation-Version: 1.0-SNAPSHOT Start-Class: com.example.spring.boot.demo.SpringBootDemo Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.2.5.RELEASE Created-By: Maven Archiver 3.4.0 Main-Class: org.springframework.boot.loader.JarLauncher

可以看到Main-Class是org.springframework.boot.loader.JarLauncher,說明項目的啟動入口并不是我們自己定義的啟動類,而是JarLauncher。而我們自己的項目引導類com.example.spring.boot.demo.SpringBootDemo,定義在了Start-Class屬性中,這個屬性并不是Java標準的MANIFEST.MF文件屬性。

spring-boot-maven-plugin 打包過程

我們并沒有添加org.springframework.boot.loader下的這些類的依賴,那么它們是如何被打包在 FatJar 里面的呢?這就必須要提到spring-boot-maven-plugin插件的工作機制了 。對于每個新建的 spring boot工程,可以在其 pom.xml 文件中看到如下插件:

<build>
 <plugins>
  <plugin>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin>
 </plugins>
</build>

這個是 SpringBoot 官方提供的用于打包 FatJar 的插件,org.springframework.boot.loader 下的類其實就是通過這個插件打進去的;

當我們執(zhí)行package命令的時候會看到下面這樣的日志:

[INFO] --- spring-boot-maven-plugin:2.2.5.RELEASE:repackage (repackage) @ spring-boot-demo ---
[INFO] Replacing main artifact with repackaged archive

repackage目標對應的將執(zhí)行到org.springframework.boot.maven.RepackageMojo#execute,該方法的主要邏輯是調(diào)用了org.springframework.boot.maven.RepackageMojo#repackage

// RepackageMojo.java
private void repackage() throws MojoExecutionException {
 // 獲取使用maven-jar-plugin生成的jar,最終的命名將加上.orignal后綴
 Artifact source = getSourceArtifact();
 // 最終文件,即Fat jar
 File target = getTargetFile();
 // 獲取重新打包器,將重新打包成可執(zhí)行jar文件
 Repackager repackager = getRepackager(source.getFile()); 
 // 查找并過濾項目運行時依賴的jar
 Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
 // 將artifacts轉(zhuǎn)換成libraries
 Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
 try {
  // 提供Spring Boot啟動腳本
  LaunchScript launchScript = getLaunchScript();
  // 執(zhí)行重新打包邏輯,生成最后fat jar
  repackager.repackage(target, libraries, launchScript);
 }
 catch (IOException ex) {
  throw new MojoExecutionException(ex.getMessage(), ex);
 }
 // 將source更新成 xxx.jar.orignal文件
 updateArtifact(source, target, repackager.getBackupFile());
}

// 繼續(xù)跟蹤getRepackager這個方法,知道Repackager是如何生成的,也就大致能夠推測出內(nèi)在的打包邏輯。
private Repackager getRepackager(File source) {
 Repackager repackager = new Repackager(source, this.layoutFactory);
 repackager.addMainClassTimeoutWarningListener(new LoggingMainClassTimeoutWarningListener());
 // 設置main class的名稱,如果不指定的話則會查找第一個包含main方法的類,
 // repacke最后將會設置org.springframework.boot.loader.JarLauncher
 repackager.setMainClass(this.mainClass);
 if (this.layout != null) {
  getLog().info("Layout: " + this.layout);
  repackager.setLayout(this.layout.layout());
 }
 return repackager;
}

repackager設置了 layout方法的返回對象,也就是org.springframework.boot.loader.tools.Layouts.Jar

/**
 * Executable JAR layout.
 */
public static class Jar implements RepackagingLayout {

 @Override
 public String getLauncherClassName() {
  return "org.springframework.boot.loader.JarLauncher";
 }

 @Override
 public String getLibraryDestination(String libraryName, LibraryScope scope) {
  return "BOOT-INF/lib/";
 }

 @Override
 public String getClassesLocation() {
  return "";
 }

 @Override
 public String getRepackagedClassesLocation() {
  return "BOOT-INF/classes/";
 }

 @Override
 public boolean isExecutable() {
  return true;
 }

}

layout我們可以將之翻譯為文件布局,或者目錄布局,代碼一看清晰明了,同時我們又發(fā)現(xiàn)了定義在MANIFEST.MF 文件的Main-Class屬性org.springframework.boot.loader.JarLauncher了,看來我們的下面的重點就是研究一下這個JarLauncher了。

JarLauncher構造過程

因為org.springframework.boot.loader.JarLauncher的類是在spring-boot-loader中的,關于spring-boot-loader,spring boot的github上是這樣介紹的:

Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched usingjava -jar. Generally you will not need to use spring-boot-loaderdirectly, but instead work with the Gradle or  Maven plugin.

Spring Boot Loader提供了秘密工具,可讓您構建可以使用java -jar啟動的單個jar文件。通常,您不需要直接使用spring-boot-loader,而可以使用Gradle或Maven插件。

但是若想在IDEA中來看源碼,需要在pom文件中引入如下配置:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-loader</artifactId>
 <scope>provided</scope>
</dependency>

找到org.springframework.boot.loader.JarLauncher類

// JarLauncher.java
public class JarLauncher extends ExecutableArchiveLauncher {

 // BOOT-INF/classes/
 static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
 // BOOT-INF/lib/
 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);
 }
 // main方法
 public static void main(String[] args) throws Exception {
 new JarLauncher().launch(args);
 }

}

可以發(fā)現(xiàn),JarLauncher定義了BOOT_INF_CLASSES和BOOT_INF_LIB兩個常量,正好就是前面我們解壓之后的兩個文件目錄。JarLauncher包含了一個main方法,作為應用的啟動入口。

從 main 來看,只是構造了一個 JarLauncher對象,然后執(zhí)行其 launch 方法 。再來看一下JarLauncher的繼承結(jié)構:

簡單分析SpringBoot java-jar命令行啟動原理

圖2

構造JarLauncherd對象時會調(diào)用父類ExecutableArchiveLauncher的構造方法:

// ExecutableArchiveLauncher.java
public ExecutableArchiveLauncher() {
 try {
  // 構造 archive 對象
  this.archive = createArchive();
 }
 catch (Exception ex) {
  throw new IllegalStateException(ex);
 }
}
// 構造 archive 對象
protected final Archive createArchive() throws Exception {
 ProtectionDomain protectionDomain = getClass().getProtectionDomain();
 CodeSource codeSource = protectionDomain.getCodeSource();
 URI location = (codeSource != null) &#63; codeSource.getLocation().toURI() : null;
 // 這里就是拿到當前的 classpath 的絕對路徑
 String path = (location != null) &#63; 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);
 }
 // 將構造的archive 對象返回
 return (root.isDirectory() &#63; new ExplodedArchive(root) : new JarFileArchive(root));
}

Archive

這里又需要我們先來了解一下Archive相關的概念。

  • archive即歸檔文件,這個概念在linux下比較常見
  • 通常就是一個tar/zip格式的壓縮包
  • jar是zip格式
public abstract class Archive {
 public abstract URL getUrl();
 public String getMainClass();
 public abstract Collection<Entry> getEntries();
 public abstract List<Archive> getNestedArchives(EntryFilter filter);
}

Archive是在spring boot里抽象出來的用來統(tǒng)一訪問資源的接口。該接口有兩個實現(xiàn),分別是ExplodedArchive和JarFileArchive。前者是一個文件目錄,后者是一個jar,都是用來在文件目錄和jar中尋找資源的,這里看到JarLauncher既支持jar啟動,也支持文件系統(tǒng)啟動,實際上我們在解壓后的文件目錄里執(zhí)行 java org.springframework.boot.loader.JarLauncher 命令也是可以正常啟動的。

簡單分析SpringBoot java-jar命令行啟動原理

圖3

在FatJar中,使用的是后者。Archive都有一個自己的URL,比如

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!

Archive類還有一個getNestedArchives方法,下面還會用到這個方法,這個方法實際返回的是springboot-demo.jar/lib下面的jar的Archive列表。它們的URL是:

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.5.RELEASE.jar!

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-starter-2.2.5.RELEASE.jar!

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-2.2.5.RELEASE.jar!

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-autoconfigure-2.2.5.RELEASE.jar!/

省略......

launch()執(zhí)行流程

archive構造完成后就該執(zhí)行JarLauncher的launch方法了,這個方法定義在了父類的Launcher里:

// Launcher.java
protected void launch(String[] args) throws Exception {
 /*
  * 利用 java.net.URLStreamHandler 的擴展機制注冊了SpringBoot的自定義的可以解析嵌套jar的協(xié)議。
  * 因為SpringBoot FatJar除包含傳統(tǒng)Java Jar中的資源外還包含依賴的第三方Jar文件
  * 當SpringBoot FatJar被java -jar命令引導時,其內(nèi)部的Jar文件是無法被JDK的默認實現(xiàn)
  * sun.net.www.protocol.jar.Handler當做classpath的,這就是SpringBoot的自定義協(xié)議的原因。
 */
 JarFile.registerUrlProtocolHandler();
 // 通過 classpath 來構建一個 ClassLoader
 ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 1
 launch(args, getMainClass(), classLoader); // 2
}

重點關注下createClassLoader(getClassPathArchives()) 構建ClassLoader的邏輯,首先調(diào)用getClassPathArchives()方法返回值作為參數(shù),該方法為抽象方法,具體實現(xiàn)在子類ExecutableArchiveLauncher中:

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

該方法會執(zhí)行Archive接口定義的getNestedArchives方法返回的與指定過濾器匹配的條目的嵌套存檔列表。從上文可以發(fā)現(xiàn),這里的archive其實就是JarFileArchive ,傳入的過濾器是JarLauncher#isNestedArchive方法引用

// JarLauncher.java
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
 // entry是文件目錄時,必須是我們自己的業(yè)務類所在的目錄 BOOT-INF/classes/
 if (entry.isDirectory()) {
  return entry.getName().equals(BOOT_INF_CLASSES);
 }
 // entry是Jar文件時,需要在依賴的文件目錄 BOOT-INF/lib/下面
 return entry.getName().startsWith(BOOT_INF_LIB);
}

getClassPathArchives方法通過過濾器將BOOT-INF/classes/和BOOT-INF/lib/下的嵌套存檔作為List<Archive>返回參數(shù)傳入createClassLoader方法中。

// Launcher.java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
 List<URL> urls = new ArrayList<>(archives.size());
 for (Archive archive : archives) {
  // 前面說到,archive有一個自己的URL的,獲得archive的URL放到list中
  urls.add(archive.getUrl());
 }
 // 調(diào)用下面的重載方法
 return createClassLoader(urls.toArray(new URL[0]));
}

// Launcher.java
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
 return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

createClassLoader()方法目的是為得到的URL們創(chuàng)建一個類加載器 LaunchedURLClassLoader,構造時傳入了當前Launcher的類加載器作為其父加載器,通常是系統(tǒng)類加載器。下面重點看一下LaunchedURLClassLoader的構造過程:

// LaunchedURLClassLoader.java
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
 super(urls, parent);
}

LaunchedURLClassLoader是spring boot自己定義的類加載器,繼承了JDK的URLClassLoader并重寫了loadClass方法,也就是說它修改了默認的類加載方式,定義了自己的類加載規(guī)則,可以從前面得到的 List<Archive>中加載依賴包的class文件了 。

LaunchedURLClassLoader創(chuàng)建完成后,我們回到Launcher中,下一步就是執(zhí)行l(wèi)aunch的重載方法了。

// Launcher.java
launch(args, getMainClass(), classLoader);

在此之前,會調(diào)用getMainClass方法并將其返回值作為參數(shù)。

getMainClass的實現(xiàn)在Launcher的子類ExecutableArchiveLauncher中:

// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
 // 從 archive 中拿到 Manifest文件
 Manifest manifest = this.archive.getManifest();
 String mainClass = null;
 if (manifest != null) {
  // 就是MANIFEST.MF 文件中定義的Start-Class屬性,也就是我們自己寫的com.example.spring.boot.demo.SpringBootDemo這個類
  mainClass = manifest.getMainAttributes().getValue("Start-Class");
 }
 if (mainClass == null) {
  throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
 }
 // 返回mainClass
 return mainClass;
}

得到mainClass后,執(zhí)行l(wèi)aunch的重載方法:

// Launcher.java
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
 // 將自定義的LaunchedURLClassLoader設置為當前線程上下文類加載器
 Thread.currentThread().setContextClassLoader(classLoader);
 // 構建一個 MainMethodRunner 實例對象來啟動應用
 createMainMethodRunner(mainClass, args, classLoader).run();
}

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

MainMethodRunner對象構建完成后,調(diào)用它的run方法:

// MainMethodRunner.java
public void run() throws Exception {
 // 使用當前線程上下文類加載器也就是自定義的LaunchedURLClassLoader來加載我們自己寫的com.example.spring.boot.demo.SpringBootDemo這個類
 Class<&#63;> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
 // 找到SpringBootDemo的main方法
 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
 // 最后,通過反射的方式調(diào)用main方法
 mainMethod.invoke(null, new Object[] { this.args });
}

至此,我們自己的main方法開始被調(diào)用,所有我們自己的應用程序類文件均可通過/BOOT-INF/classes加載,所有依賴的第三方jar均可通過/BOOT-INF/lib加載,然后就開始了spring boot的啟動流程了。

debug技巧

以上就是spring boot通過java -jar命令啟動的原理了,了解了原理以后我們可不可以通過debug來進一步加深一下理解呢?通常我們在IDEA里啟動時是直接運行main方法,因為依賴的Jar都讓IDEA放到classpath里了,所以spring boot直接啟動就完事了,并不會通過上面的方式來啟動。不過我們可以通過配置IDEA的 run/debug configurations 配置 JAR Application 來實現(xiàn)通過Jar方式啟動。

看完了這篇文章,相信你對簡單分析SpringBoot java-jar命令行啟動原理有了一定的了解,想了解更多相關知識,歡迎關注億速云行業(yè)資訊頻道,感謝各位的閱讀!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI