可能很多初學(xué)者會比較困惑,Spring Boot 是如何做到將應(yīng)用代碼和所有的依賴打包成一個獨立的 Jar 包,因為傳統(tǒng)的 Java 項目打包成 Jar 包之后,需要通過 -classpath 屬性來指定依賴,才能夠運行。我們今天就來分析講解一下 Spring Boot 的啟動原理。
1. Spring Boot 打包插件
Spring Boot 提供了一個名叫 spring-boot-maven-plugin
的 maven 項目打包插件,可以方便的將 Spring Boot 項目打成 jar 包。這樣我們就不再需要部署 Tomcat 、Jetty等之類的 Web 服務(wù)器容器啦。
我們先看一下 Spring Boot 打包后的結(jié)構(gòu)是什么樣的,打開 target 目錄我們發(fā)現(xiàn)有兩個jar包:
- hello-0.0.1-SNAPSHOT.jar:17.3MB
- hello-0.0.1-SNAPSHOT.jar.original:3KB
其中,hello-0.0.1-SNAPSHOT.jar 是通過 Spring Boot 提供的打包插件采用新的格式打成 Fat Jar,包含了所有的依賴;而 hello-0.0.1-SNAPSHOT.jar.original 則是Java原生的打包方式生成的,僅僅只包含了項目本身的內(nèi)容。
2. SpringBoot FatJar 的組織結(jié)構(gòu)
我們將 Spring Boot 打的可執(zhí)行 Jar 展開后的結(jié)構(gòu)如下所示:
.
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── com
│ │ └── javanorth
│ │ └── hello
│ │ └── HelloApplication.class
│ └── lib
│ ├── spring-boot-2.5.0.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.5.0.RELEASE.jar
│ ├── spring-boot-configuration-processor-2.5.0.RELEASE.jar
│ ├── spring-boot-starter-2.5.0.RELEASE.jar
│ ├── ...
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.javanorth
│ └── hello
│ ├── pom.properties
│ └── pom.xml
│
├── org
│ └── springframework
│ └── boot
│ └── loader
│ ├── ExecutableArchiveLauncher.class
│ ├── JarLauncher.class
│ ├── Launcher.class
│ ├── MainMethodRunner.class
│ ├── ...
- BOOT-INF目錄:包含了我們的項目代碼(classes目錄),以及所需要的依賴(lib 目錄)
- META-INF目錄:通過 MANIFEST.MF 文件提供 Jar包的元數(shù)據(jù),聲明了 jar 的啟動類
org.springframework.boot.loader
:Spring Boot 的加載器代碼,實現(xiàn)的 Jar in Jar 加載的魔法源
我們看到,如果去掉BOOT-INF目錄,這將是一個非常普通且標(biāo)準(zhǔn)的Jar包,包括元信息以及可執(zhí)行的代碼部分,其/META-INF/MAINFEST.MF指定了Jar包的啟動元信息,org.springframework.boot.loader
執(zhí)行對應(yīng)的邏輯操作。
3. MAINFEST.MF 元信息分析
元信息內(nèi)容如下所示:
Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 11
Implementation-Title: hello
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.javanorth.hello.HelloApplication
Spring-Boot-Version: 2.5.0
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
它相當(dāng)于一個 Properties 配置文件,每一行都是一個配置項目。重點來看看兩個配置項:
- Main-Class 配置項:Java 規(guī)定的 jar 包的啟動類,這里設(shè)置為 spring-boot-loader 項目的 JarLauncher 類,進行 Spring Boot 應(yīng)用的啟動。
- Start-Class 配置項:Spring Boot 規(guī)定的主啟動類,這里設(shè)置為我們定義的 Application 類。
- Spring-Boot-Classes 配置項:指定加載應(yīng)用類的入口
- Spring-Boot-Lib 配置項: 指定加載應(yīng)用依賴的庫
4. 啟動原理
Spring Boot 的啟動原理如下圖所示:
5. 源碼分析
5.1 org.springframework.boot.loader.JarLauncher
JarLauncher 類是針對 Spring Boot jar 包的啟動類, 完整的類圖如下所示:
Spring Boot Start jar 2
其中的 WarLauncher 類,是針對 Spring Boot war 包的啟動類。啟動類 org.springframework.boot.loader.JarLauncher
并非為項目中引入類,而是 spring-boot-maven-plugin
插件 repackage 追加進去的。接下來我們先來看一下 JarLauncher 的源碼,比較簡單,如下圖所示:
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) - > {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return super.getClassPathIndex(archive);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
}
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
當(dāng)執(zhí)行 java -jar
命令或執(zhí)行解壓后的 org.springframework.boot.loader.JarLauncher
類時,JarLauncher 會將 BOOT-INF/classes 下的類文件和 BOOT-INF/lib 下依賴的jar加入到classpath下,后調(diào)用 META-INF/MANIFEST.MF 文件 Start-Class 屬性 [指向項目中的 com.javanorth.hello.HelloApplicatioin
啟動類] 完成應(yīng)用程序的啟動。
JarLauncher 假定依賴項jar包含在 /BOOT-INF/lib 目錄中,并且應(yīng)用程序類包含在 /BOOT-INF/classes 目錄中。它的 main 方法調(diào)用的則是基類 Launcher 定義的 launch 方法,而 Launcher 是ExecutableArchiveLauncher 的父類。
5.2 org.springframework.boot.loader.ExecutableArchiveLauncher
ExecutableArchiveLauncher 是 JarLauncher 的直接父類,繼承了 Launcher 基類,并實現(xiàn)部分抽象方法
public abstract class ExecutableArchiveLauncher extends Launcher {
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
private final Archive archive;
private final ClassPathIndexFile classPathIndex;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
this.classPathIndex = getClassPathIndex(this.archive);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
try {
this.archive = archive;
this.classPathIndex = getClassPathIndex(this.archive);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
return null;
}
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
@Override
protected ClassLoader createClassLoader(Iterator< Archive > archives) throws Exception {
List< URL > urls = new ArrayList< >(guessClassPathSize());
while (archives.hasNext()) {
urls.add(archives.next().getUrl());
}
if (this.classPathIndex != null) {
urls.addAll(this.classPathIndex.getUrls());
}
return createClassLoader(urls.toArray(new URL[0]));
}
private int guessClassPathSize() {
if (this.classPathIndex != null) {
return this.classPathIndex.size() + 10;
}
return 50;
}
@Override
protected Iterator< Archive > getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
Iterator< Archive > archives = this.archive.getNestedArchives(searchFilter,
(entry) - > isNestedArchive(entry) && !isEntryIndexed(entry));
if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives;
}
private boolean isEntryIndexed(Archive.Entry entry) {
if (this.classPathIndex != null) {
return this.classPathIndex.containsEntry(entry.getName());
}
return false;
}
private Iterator< Archive > applyClassPathArchivePostProcessing(Iterator< Archive > archives) throws Exception {
List< Archive > list = new ArrayList< >();
while (archives.hasNext()) {
list.add(archives.next());
}
postProcessClassPathArchives(list);
return list.iterator();
}
protected boolean isSearchCandidate(Archive.Entry entry) {
return true;
}
protected abstract boolean isNestedArchive(Archive.Entry entry);
protected boolean isPostProcessingClassPathArchives() {
return true;
}
protected void postProcessClassPathArchives(List< Archive > archives) throws Exception {
}
@Override
protected boolean isExploded() {
return this.archive.isExploded();
}
@Override
protected final Archive getArchive() {
return this.archive;
}
}
5.3 org.springframework.boot.loader.Launcher
如下則是 Launcher 的源碼
- launch 方法會首先創(chuàng)建類加載器,而后判斷 jar 是否在 MANIFEST.MF 文件中設(shè)置了 jarmode 屬性。
- 如果沒有設(shè)置,launchClass 的值就來自 getMainClass() 返回,該方法由子類實現(xiàn),返回 MANIFEST.MF 中配置的 START_CLASS_ATTRIBUTE 屬性值
- 調(diào)用 createMainMethodRunner 方法,構(gòu)建一個 MainMethodRunner 對象并調(diào)用其 run 方法
jarmode 是創(chuàng)建 docker 鏡像時用到的參數(shù),使用該參數(shù)是為了生成帶有多個 layer 信息的鏡像,這里暫不注意
public abstract class Launcher {
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
@Deprecated
protected ClassLoader createClassLoader(List< Archive > archives) throws Exception {
return createClassLoader(archives.iterator());
}
protected ClassLoader createClassLoader(Iterator< Archive > archives) throws Exception {
List< URL > urls = new ArrayList< >(50);
while (archives.hasNext()) {
urls.add(archives.next().getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
}
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(launchClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
protected abstract String getMainClass() throws Exception;
protected Iterator< Archive > getClassPathArchivesIterator() throws Exception {
return getClassPathArchives().iterator();
}
@Deprecated
protected List< Archive > getClassPathArchives() throws Exception {
throw new IllegalStateException("Unexpected call to getClassPathArchives()");
}
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));
}
protected boolean isExploded() {
return false;
}
protected Archive getArchive() {
return null;
}
}
5.4 org.springframework.boot.loader.MainMethodRunner
從名字可以判斷這是一個目標(biāo)類main方法的執(zhí)行器,此時的 mainClassName 被賦值為 MANIFEST.MF 中配置的 START_CLASS_ATTRIBUTE 屬性值,也就是 com.javanorth.hello.HelloApplication
,之后便是通過反射執(zhí)行 HelloApplication 的 main 方法,從而達到啟動 Spring Boot 的效果。
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
Class< ? > mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
}
總結(jié)
- jar 包類似于 zip 壓縮文件,只不過相比 zip 文件多了一個 META-INF/MANIFEST.MF 文件,該文件在構(gòu)建 jar 包時自動創(chuàng)建
- 想要制作可執(zhí)行 JAR 包,在 MANIFEST.MF 中指定 Main-Class 是關(guān)鍵。使用 java 執(zhí)行 jar 包的時候,實際上等同于使用 java 命令執(zhí)行指定的 Main-Class 程序。
- Spring Boot 提供了一個插件 spring-boot-maven-plugin ,用于把程序打包成一個可執(zhí)行的jar包
- 使用 java -jar 啟動 Spring Boot 的 jar 包,首先調(diào)用的入口類是 JarLauncher,內(nèi)部調(diào)用 Launcher 的 launch 后構(gòu)建 MainMethodRunner 對象,最終通過反射調(diào)用 HelloApplication 的 main 方法實現(xiàn)啟動效果。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7239瀏覽量
90972 -
服務(wù)器
+關(guān)注
關(guān)注
12文章
9681瀏覽量
87259 -
代碼
+關(guān)注
關(guān)注
30文章
4886瀏覽量
70232 -
SpringBoot
+關(guān)注
關(guān)注
0文章
175瀏覽量
313
發(fā)布評論請先 登錄
啟動Spring Boot項目應(yīng)用的三種方法
Spring Boot嵌入式Web容器原理是什么
Spring Boot定時任務(wù)的重寫方法
Spring Boot從零入門1 詳述
「Spring認證」什么是Spring GraphQL?

Spring Boot特有的實踐
強大的Spring Boot 3.0要來了
Spring Boot啟動優(yōu)化實踐
Spring Boot Web相關(guān)的基礎(chǔ)知識
SpringBoot的嵌入式Web容器是什么時候加載的?

Spring Boot配置加載相關(guān)知識
Spring Boot Actuator快速入門
Spring Boot啟動 Eureka流程

評論