文章目錄
- 版本
- 概述
- JAR 包結構
- `MANIFEST.MF` 描述文件
- JarLauncher
- `Archive` 接口
- `launch` 方法
- `Handlers.register()` 方法
- `getClassPathUrls` 方法
- `createClassLoader` 方法
- 時序圖
- 參考
版本
- Java 17
- SpringBoot 3.2.4
概述
JAR 啟動原理可以簡單理解為“java -jar
的啟動原理”
SpringBoot 提供了 Maven 插件 spring-boot-maven-plugin
,可以將 SpringBoot 項目打包成 JAR 包,這個跟普通 JAR 包有所不同
- 普通 JAR 包:可以被其他項目引用,解壓后就是包名,包里就是代碼
- SpringBoot 打包的 JAR 包:只能運行,不能被其他項目依賴,包里
\BOOT-INF\classes
目錄才是代碼
使用 maven package
指令執行打包命令,將項目打包成 JAR 包,根據 pom.xml
文件中的 name
和 version
標簽作為 JAR 包名稱,比如項目的
pom.xml
配置文件有 <name>springboot-demo</name>
和 <version>0.0.1-SNAPSHOT</version>
,執行 maven package
命令之后打包出來之后為 springboot-demo-0.0.1-SNAPSHOT.jar
和 springboot-demo-0.0.1-SNAPSHOT.jar.original
springboot-demo-0.0.1-SNAPSHOT.jar
之類的 JAR 包:spring-boot-maven-plugin
生成的 JAR 包。包含了應用的第三方依賴,SpringBoot 相關的類,存在嵌套的 JAR 包,稱之為 executable jar 或 fat jar。也就是最終可運行的 SpringBoot 的 JAR 包。可以直接執行java -jar
指令啟動springboot-demo-0.0.1-SNAPSHOT.jar.original
之類的 JAR 包:默認maven-jar-plugin
生成的 JAR 包,僅包含編譯用的本地文件。也就是打包之前生成的原始 JAR 包,僅包含你項目本身的 class 文件和資源文件,不包含依賴項,也不具備 Spring Boot 的啟動結構。通常由 Spring Boot Maven 插件在打包過程中中間步驟生成,Spring Boot 會在這個基礎上重打包(repackage)為可運行的 JAR 文件。
JAR 包結構
springboot-demo-0.0.1-SNAPSHOT.jar
之類的 JAR 包中通常包括 BOOT-INF
,META-INF
,org
三個文件夾
META-INF
:通過MANIFEST.MF
文件提供jar
包的元數據,聲明了 JAR 的啟動類org
:為 SpringBoot 提供的spring-boot-loader
項目,它是java -jar
啟動 Spring Boot 項目的秘密所在BOOT-INF/lib
:SpringBoot 項目中引入的依賴的 JAR 包,目的是解決 JAR 包里嵌套 JAR 的情況,如何加載到其中的類BOOT-INF/classes
:Java 類所編譯的.class
、配置文件等等
應用程序類應該放在嵌套的BOOT-INF/classes目錄中。依賴項應該放在嵌套的BOOT-INF/lib目錄中。
├── BOOT-INF // 文件目錄存放業務相關的,包括業務開發的類和配置文件,以及依賴的 JAR
│ ├── classes
│ │ ├── application.yaml
│ │ └── com
│ │ └── example
│ │ └── springbootdemo
│ │ ├── OrderProperties.class
│ │ ├── SpringbootDemoApplication.class // 啟動類
│ │ ├── SpringbootDemoApplication$OrderPropertiesCommandLineRunner.class
│ │ ├── SpringbootDemoApplication$ValueCommandLineRunner.class
│ │ ├── SpringMVCConfiguration.class
│ │ └── vo
│ │ └── UserVO.class
│ ├── classpath.idx
│ ├── layers.idx
│ └── lib
│ ├── spring-aop-6.1.6.jar
│ ├── spring-beans-6.1.6.jar
│ ├── spring-boot-3.2.5.jar
│ ├── spring-boot-autoconfigure-3.2.5.jar
│ ├── spring-boot-jarmode-layertools-3.2.5.jar
├── META-INF // MANIFEST.MF 描述文件和 maven 的構建信息
│ ├── MANIFEST.MF
│ ├── maven
│ │ └── com.example
│ │ └── springboot-demo
│ │ ├── pom.properties // 配置文件
│ │ └── pom.xml
│ ├── services
│ │ └── java.nio.file.spi.FileSystemProvider
│ └── spring-configuration-metadata.json
└── org└── springframework└── boot└── loader // SpringBoot loader 相關類├── jar│ ├── ManifestInfo.class│ ├── MetaInfVersionsInfo.class├── jarmode│ └── JarMode.class├── launch│ ├── Archive.class│ ├── Archive$Entry.class├── log│ ├── DebugLogger.class│ ├── DebugLogger$DisabledDebugLogger.class│ └── DebugLogger$SystemErrDebugLogger.class├── net│ ├── protocol│ │ ├── Handlers.class│ │ ├── jar│ │ │ ├── Canonicalizer.class│ │ │ ├── Handler.class│ │ └── nested│ │ ├── Handler.class│ │ ├── NestedLocation.class│ └── util│ └── UrlDecoder.class├── nio│ └── file│ ├── NestedByteChannel.class│ ├── NestedByteChannel$Resources.class├── ref│ ├── Cleaner.class│ └── DefaultCleaner.class└── zip├── ByteArrayDataBlock.class├── CloseableDataBlock.class
MANIFEST.MF
描述文件
MANIFEST.MF
是 Java JAR(Java Archive)文件中的一個核心元數據文件,用于描述 JAR 包的配置信息和依賴關系。它位于 JAR 文件內部的 META-INF/
目錄下,是 JVM 啟動可執行 JAR 或加載依賴的關鍵依據。
java -jar
命令引導的具體啟動類必須配置在 MANIFEST.MF
描述文件中的 Main-Class
屬性中,該命令用來引導標準執行的 JAR 文件,讀取的就是 MANIFEST.MF
文件中的 Main-Class
屬性值,Main-Class
屬性就是定義包含了 main
方法的類代表了應用程序執行入口類
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.3.0
Build-Jdk-Spec: 19
Implementation-Title: springboot-demo
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.example.springbootdemo.SpringbootDemoApplication
Spring-Boot-Version: 3.2.5
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Main-Class
:定義程序入口類,格式為完全限定類名(含包名),JVM 通過此屬性找到public static void main(String[] args)
方法啟動程序。設置為spring-boot-loader
項目的 JarLauncher 類,進行 SpringBoot 應用的啟動。Start-Class
:SpringBoot 規定的主啟動類Class-Path
:指定依賴的 JAR 文件或目錄,路徑用空格分隔- 路徑相對于 JAR 文件的位置(非當前工作目錄)
- 依賴需放在 JAR 同級目錄的指定路徑下
Manifest-Version
:指定清單文件版本Created-By
:生成 JAR 的工具信息(如 JDK 版本或構建工具)Implementation-Version
:JAR 的版本號(用于版本管理)
雖然 Start-Class
已經指向了主啟動類路徑,但是不能直接啟動
- 原因一:因為在 JAR 包中,主啟動類并不在這個路徑上,而是在在
BOOT-INF/classes
目錄下,不符合 Java 默認的 JAR 包的加載規則。因此,需要通過 JarLauncher 啟動加載。 - 原因二:Java 規定可執行器的 JAR 包禁止嵌套其它 JAR 包。但是可以看到
BOOT-INF/lib
目錄下,實際有 SpringBoot 應用依賴的所有 JAR 包。因此,spring-boot-loader
項目自定義實現了 ClassLoader 實現類 LaunchedURLClassLoader,支持加載BOOT-INF/classes
目錄下的.class
文件,以及BOOT-INF/lib
目錄下的jar
包。
JarLauncher
JarLauncher
是 Spring Boot 框架中用于啟動可執行 JAR 文件的核心類,屬于 org.springframework.boot.loader
包。它的核心作用是為 Spring Boot 的“胖 JAR”(Fat JAR)提供自定義的啟動機制,解決傳統 JAR 無法直接加載嵌套依賴的問題。
位于 JAR 包中的 org.springframework.boot.loader.launch.JarLauncher
繼承類:Launcher
-> ExecutableArchiveLauncher
-> JarLauncher
public class JarLauncher extends ExecutableArchiveLauncher {public JarLauncher() throws Exception {}protected JarLauncher(Archive archive) throws Exception {super(archive);}protected boolean isIncludedOnClassPath(Archive.Entry entry) {return isLibraryFileOrClassesDirectory(entry);}protected String getEntryPathPrefix() {return "BOOT-INF/";}static boolean isLibraryFileOrClassesDirectory(Archive.Entry entry) {String name = entry.name();return entry.isDirectory() ? name.equals("BOOT-INF/classes/") : name.startsWith("BOOT-INF/lib/");}public static void main(String[] args) throws Exception {(new JarLauncher()).launch(args);}
}
通過 (new JarLauncher()).launch(args)
創建 JarLauncher 對象,調用 launch
方法進行啟動,整體邏輯還是通過父類的父類 Laucher
所提供。
Archive
接口
根據類圖可知,JarLauncher
繼承于 ExecutableArchiveLauncher
類,在 ExecutableArchiveLauncher
類源碼中有對 Archive
對象的構造
public abstract class ExecutableArchiveLauncher extends Launcher {public ExecutableArchiveLauncher() throws Exception {this(Archive.create(Launcher.class));}
}
Archive
接口,是 spring-boot-loader
項目抽象出來的用來統一訪問資源的接口,ExplodedArchive
是針對目錄的 Archive
實現類,JarFileArchive
是針對 JAR 的 Archive
實現類,所以根據 isDirectory
方法進行判斷。
Archive
概念即歸檔文檔概念,在 Linux 下比較常見- 通常就是一個 tar/zip 格式的壓縮包
- JAR 是 zip 格式
public interface Archive extends AutoCloseable {static Archive create(Class<?> target) throws Exception {return create(target.getProtectionDomain());}static Archive create(ProtectionDomain protectionDomain) throws Exception {CodeSource codeSource = protectionDomain.getCodeSource();URI location = codeSource != null ? codeSource.getLocation().toURI() : null;// 拿到當前 classpath 的絕對路徑String path = location != null ? location.getSchemeSpecificPart() : null;if (path == null) {throw new IllegalStateException("Unable to determine code source archive");} else {return create(new File(path));}}static Archive create(File target) throws Exception {if (!target.exists()) {throw new IllegalStateException("Unable to determine code source archive from " + target);} else {return (Archive)(target.isDirectory() ? new ExplodedArchive(target) : new JarFileArchive(target));}}
}
launch
方法
在其父類 Laucher
中可以看出,launcher
方法可以讀取 JAR 包中的類加載器,保證 BOOT-INF/lib
目錄下的類和 BOOT-classes
內嵌的 jar
中的類能夠被正常加載到,之后執行 Spring Boot 應用的啟動。
public abstract class Launcher {protected void launch(String[] args) throws Exception {// 如果當前不是解壓模式(!this.isExploded()),則注冊處理器(Handlers.register())if (!this.isExploded()) {Handlers.register();}try {// 創建類加載器(ClassLoader)用于加載類路徑上的類ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());// 根據系統屬性 "jarmode" 判斷是否使用特定的 JAR 模式運行器類名String jarMode = System.getProperty("jarmode");String mainClassName = this.hasLength(jarMode) ? JAR_MODE_RUNNER_CLASS_NAME : this.getMainClass();// 使用創建的類加載器和主類名調用 launch 方法啟動應用this.launch(classLoader, mainClassName, args);} catch (UncheckedIOException var5) {UncheckedIOException ex = var5;throw ex.getCause();}}protected void launch(ClassLoader classLoader, String mainClassName, String[] args) throws Exception {Thread.currentThread().setContextClassLoader(classLoader);Class<?> mainClass = Class.forName(mainClassName, false, classLoader);Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);mainMethod.setAccessible(true);mainMethod.invoke((Object)null, args);}
}
Handlers.register()
方法
逐步分析 launcher
方法,首先方法中調用的 Handlers.register()
方法,用于動態注冊自定義協議處理器包,并確保 URL 流處理器緩存被正確刷新。
public final class Handlers {private static final String PROTOCOL_HANDLER_PACKAGES = "java.protocol.handler.pkgs";private static final String PACKAGE = Handlers.class.getPackageName();private Handlers() {}public static void register() {// 獲取系統屬性java.protocol.handler.pkgs,該屬性用于指定協議處理器的包名String packages = System.getProperty("java.protocol.handler.pkgs", "");// 如果當前包名未包含在屬性中,則將其追加到屬性值中(以|分隔)packages = !packages.isEmpty() && !packages.contains(PACKAGE) ? packages + "|" + PACKAGE : PACKAGE;System.setProperty("java.protocol.handler.pkgs", packages);// 清除URL流處理器緩存resetCachedUrlHandlers();}private static void resetCachedUrlHandlers() {try {// 強制JVM重新加載URL流處理器URL.setURLStreamHandlerFactory((URLStreamHandlerFactory)null);} catch (Error var1) {}}
}
getClassPathUrls
方法
分析 ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());
中的 (Collection)this.getClassPathUrls()
方法,調用 getClassPathUrls
方法返回值作為參數,該方法為抽象方法,具體實現在 ExecutableArchiveLauncher
中
public abstract class ExecutableArchiveLauncher extends Launcher {protected Set<URL> getClassPathUrls() throws Exception {return this.archive.getClassPathUrls(this::isIncludedOnClassPathAndNotIndexed, this::isSearchedDirectory);}
}
在 ExecutableArchiveLauncher
的 getClassPathUrls
方法執行 Archive
接口定義的 getClassPathUrls
方法返回的是包含所有匹配 URL 的有序集合
class JarFileArchive implements Archive {// 通過流處理遍歷JAR條目,應用過濾器篩選后轉換為URLpublic Set<URL> getClassPathUrls(Predicate<Archive.Entry> includeFilter, Predicate<Archive.Entry> directorySearchFilter) throws IOException {return (Set)this.jarFile.stream().map(JarArchiveEntry::new).filter(includeFilter).map(this::getNestedJarUrl).collect(Collectors.toCollection(LinkedHashSet::new));}// 根據條目注釋判斷是否為解壓存儲的嵌套JAR,若是則調用特殊處理方法,否則直接創建標準URL// archiveEntry:BOOT-INF/classes/private URL getNestedJarUrl(JarArchiveEntry archiveEntry) {try {JarEntry jarEntry = archiveEntry.jarEntry();String comment = jarEntry.getComment();return comment != null && comment.startsWith("UNPACK:") ? this.getUnpackedNestedJarUrl(jarEntry) : JarUrl.create(this.file, jarEntry);} catch (IOException var4) {IOException ex = var4;throw new UncheckedIOException(ex);}}
}
createClassLoader
方法
分析 ClassLoader classLoader = this.createClassLoader((Collection)this.getClassPathUrls());
中的 createClassLoader
方法
LaunchedClassLoader
是 SpringBoot 自定義的類加載器,位于 org.springframework.boot.loader.LaunchedURLClassLoader
, 專門用于加載 Spring Boot 可執行 JAR(即“胖 JAR”)中嵌套的依賴和資源。它的核心作用是解決傳統 Java 類加載器無法直接加載 JAR 內嵌 JAR(如 BOOT-INF/lib/
中的依賴)的問題。且LaunchedClassLoader
在加載類時,會先嘗試自己加載(從嵌套 JAR 或用戶代碼),若找不到再委派父類加載器。這是對傳統雙親委派機制的擴展,確保優先加載應用自身的類。
public abstract class Launcher {protected ClassLoader createClassLoader(Collection<URL> urls) throws Exception {return this.createClassLoader((URL[])urls.toArray(new URL[0]));}private ClassLoader createClassLoader(URL[] urls) {ClassLoader parent = this.getClass().getClassLoader();return new LaunchedClassLoader(this.isExploded(), this.getArchive(), urls, parent);}
}
時序圖
+-----------------+ +--------------+ +----------------------+ +-------------------+
| JVM | | JarLauncher | | LaunchedURLClassLoader| | MainMethodRunner |
+-----------------+ +--------------+ +----------------------+ +-------------------+| | | || 執行 java -jar app.jar | | ||--------------------->| | || | 創建 Archive 對象 | || |------------------------>| || | 解析 MANIFEST.MF | || |<------------------------| || | 調用 launch() | || |------------------------>| || | | 創建類加載器 || | |<--------------------------|| | | 加載 BOOT-INF/classes/lib|| | |-------------------------->|| | | | 反射加載 Start-Class| | | |<------------------|| | | | 調用 main()| | | |------------------>|| | | | 執行用戶代碼 ||<------------------------------------------------------------(結果或異常)|| JVM 退出 | | ||<--------------------| | |
參考
- 一文搞懂 Spring Boot 中 java -jar 的啟動 jar 包的原理_springboot java -jar-CSDN 博客
- 芋道 Spring Boot Jar 啟動原理 | 芋道源碼 —— 純源碼解析博客
- Site Unreachable