Spring Boot 的java -jar命令啟動原理詳解
導語
在運用Spring Boot 後,我們基本上擺脫之前專案每次上線的時候把專案打成war包。當然也不排除一些奇葩的規定,必須要用war包上線,不過很多時候,我們對一些東西只是處在使用的階段,並不會去深入的研究使用的原理是什麼,這貌似也是大多數人的固定思維。
或許正是如此,總會有些沒有固定思維的人會去積極的探索原理,當然這話不是說我是積極的,我其實也是隻原理的搬運工。今天和大家來簡單的說下Spring Boot 的專案在執行Java -jar的原理。
jar包目錄和jar命令啟動入口
在正式開始之前,我們先來看看把jar包進行解壓。然後用tree /f命令檢視目錄結構(由於筆者寫博文時用的是window,所以用的是tree /f命令),由於目錄結構太長,這裡做了相應省略,如下:
├─BOOT-INF │ ├─classes │ │ │ application.properties │ │ │ │ │ └─com │ │ └─spring │ │ └─boot │ │ └─test │ │ SpringBootTestApplication.class │ │ │ └─lib │ classmate-1.5.1.jar │ hibernate-validator-6.0.18.Final.jar │ …………此處省略………… │ ├─META-INF │ │ MANIFEST.MF │ │ │ └─maven │ └─com.spring.boot.test │ └─spring-boot-test │ pom.properties │ pom.xml │ └─org └─springframework └─boot └─loader │ ExecutableArchiveLauncher.class │ JarLauncher.class │ LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class │ LaunchedURLClassLoader.class │ Launcher.class │ MainMethodRunner.class │ PropertiesLauncher$1.class │ PropertiesLauncher$ArchiveEntryFilter.class │ PropertiesLauncher$PrefixMatchingArchiveFilter.class │ PropertiesLauncher.class │ WarLauncher.class │ ├─archive │ Archive$Entry.class │ …………此處省略………… │ ├─data │ RandomAccessData.class │ …………此處省略………… │ ├─jar │ AsciiBytes.class │ Bytes.class │ …………此處省略………… │ └─util SystemPropertyUtils.class
先簡單說下上面目錄結構,大體目錄分三層:BOOT-INF、META-INF、org,BOOT-INF是存放對應的應用服務的.class檔案和Maven依賴的jar包,包括啟動類SpringBootTestApplication,META-INF下存放的是Maven相關的pom資訊和MANIFEST.MF檔案,org資料夾下存放的是Spring boot loader模組編譯的.class檔案,也就是jar啟動的關鍵程式碼所在。
在執行java -jar命令的時候,它的啟動類配置實在jar包目錄下META-INF資料夾下的名MANIFEST.MF檔案中,在這個檔案中有一個名為Main-Class的屬性,我們來看下這個檔案的具體內容:
Manifest-Version: 1.0 Implementation-Title: spring-boot-test Implementation-Version: 0.0.1-SNAPSHOT Start-Class: com.spring.boot.test.SpringBootTestApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.2.3.RELEASE Created-By: Maven Archiver 3.4.0 Main-Class: org.springframework.boot.loader.JarLauncher
從上面的配置檔案中,可以看到Main-Class屬性指向的Class為org.springframework.boot.loader.JarLauncher,而JarLauncher是JAR的啟動器,這個類是在org/springframework/boot/loader/,然後可以看到專案所定義的啟動類是指向Start-Class這個屬性的。
JAR檔案啟動器——JarLauncher
在上面我們說了JarLauncher是JAR可執行的啟動器,那麼它和專案的啟動類SpringBootTestApplication有什麼關聯呢?先給大家來個示例,先來到解壓目錄下執行命令:java org.springframework.boot.loader.JarLauncher,然後便是如下介面:
C:\Users\elisha\Desktop\spring-boot-test-0.0.1-SNAPSHOT>java org.springframework.boot.loader.JarLauncher . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__,| / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.3.RELEASE) 2020-01-18 14:28:19.866 INFO 3644 --- [ main] c.s.boot.test.SpringBootTestApplication : Starting SpringBootTestApplication on LAPTOP-R2NNI9CM with PID 3644 (C:\Users\elisha\Desktop\spring-boot-test-0.0.1-SNAPSHOT\BOOT-INF\classes started by elisha in C:\Users\elisha\Desktop\spring-boot-test-0.0.1-SNAPSHOT)
從上面的執行介面可以看到專案引導類SpringBootTestApplication會被JarLauncher類進行引導,但是如果我們到BOOT-INF/class目錄下,然後也執行java com.spring.boot.test.SpringBootTestApplication,會報SpringApplication的ClassNotFoundException這個錯誤,由此可以得知這是因為java命令未指定Class Path。不過當前Spring Boot依賴的JAR檔案都是存放在BOOT-INF/lib下,而org.springframework.boot.loader.JarLauncher會將JAR作為SpringBootTestApplication類庫的依賴,這也就是為什麼JarLauncher能引導SpringBootTestApplication,反之則是不可以的,那麼對於SpringBootTestApplication是JarLauncher的子程序,還是處於同一層級呢?接下來我們來看看JarLauncher的原理。
JarLauncher實現引導原理
因為org.springframework.boot.loader.JarLauncher的類是在spring-boot-loader中,但是若想在IDEA中來看原始碼,需要在pom檔案中引入如下配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> <scope>provided</scope> </dependency>
在引入上面的配置檔案後,便可以在IDEA中檢視原始碼了,使用CTRL+N命令來搜尋JarLauncher類,那就來看下原始碼,如下:
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); } }
從上面的JarLauncher類中,可以看到兩個常量:BOOT_INF_CLASSES、BOOT_INF_LIB,而它們又分別指向如下路徑:BOOT-INF/classes/、BOOT-INF/lib/,並用isNestedArchive(Archive.Entry entry)方法進行判斷(在Spring Boot中Archive,抽象出了Archive的概念,一個Archive可以是一個Jar(JarFileArchive)、也可以是一個目錄(ExplodedArchive),在這裡可以理解為Spring Boot抽象出來的同一訪問資源層。),從isNestedArchive方法的引數Archive.Entry物件貌似為一個JAR檔案中的資源,譬如application.properties,同時這個物件和JarEntry是類似的,其name屬性(Archive.Entry#getName())便是Jar資源的相對路徑。當application.properties資源在FAT JAR目錄下時,其實Archive.Entry#getName()就是/BOOT-INF/classes/application.properties,此時便符合startsWith方法的判斷,所以isNestedArchive(Archive.Entry entry)便返回為true。當返回為false時,便說明FAT JAR被解壓到檔案目錄了,由此也說明了Spring Boot應用可以通過java org.springframework.boot.loader.JarLauncher命令啟動的原因了。
Archive.Entry的實現
上面說了在Spring Boot中Archive,抽象出了Archive的概念,一個Archive可以是一個Jar(JarFileArchive)、也可以是一個目錄(ExplodedArchive),這裡所說的JarFileArchive、ExplodedArchive便是Archive的兩種是想方式,對於這兩個類的實現程式碼感興趣額同學可以自己去看看。
不過由此也說明了JarLauncher既支援JAR啟動,又支援檔案系統啟動。同時JarLauncher在作為引導類的時候,當執行java -jar 命令式,/META-INF/ 下MANIFEST.MF檔案中的Main-Class屬性將呼叫它的,main(String [])方法,其實它還是呼叫JarLauncher #launch(args)方法,這個方法是實現基類Launcher,這裡簡單看下這個方法的實現:
protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); ClassLoader classLoader = createClassLoader(getClassPathArchives()); launch(args,getMainClass(),classLoader); }
總結
本篇文章簡單的講解了一下,java -jar命令的一個執行的原理,首先說了下jar包目錄和jar命令啟動入口,然後說了下JAR檔案啟動器——JarLauncher和JarLauncher實現引導原理,最後說了下Archive.Entry的實現,這個實現的原理也是比較複雜,後面如果有機會,會再寫篇文章來進行說明。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。