玩轉Gradle構建工具(七)、SpringBoot外掛原始碼分析
theme: cyanosis highlight: agate
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第7天,點選檢視活動詳情
前言
本系列目錄
- Task
- Project、Task常用API
- 檔案操作
- 依賴管理
- 多模組構建
- 外掛編寫
- SpringBoot外掛原始碼分析
- 過度到Kotlin
SpringBoot提供的Gradle外掛用來打包SpringBoot專案,我們知道SpringBoot專案打包後的jar有幾個特點,他會把我們的class放在BOOT-INF/classes
下,並把專案用到的所有庫,放在BOOT-INF/lib
下,並設定Main方法入口為org.springframework.boot.loader.JarLauncher
,由JarLauncher啟動我們自己的Main。
而Gradle外掛就是做這個事情的,但這篇文章不會很詳細的介紹他原始碼,因為以我現在的功力,無法深入到Gradle,加上網上沒有找到一篇關於他的文章,所以這裡只介紹個大概。
而且除錯過程,也非常心累,我嘗試把這個外掛重新編譯後,放在Gradle快取目錄下,也就是替換掉原來的外掛,Linux下位於路徑/home/hxl/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-gradle-plugin/x.x.x
。
由於在原始碼中增加了一些日誌,所以我期望的是在專案中使用bootJar
時,會出現這些日誌,但是絕望的是,這不一定可行,因為有兩個快取位置不能確定,當我在終端執行bootJar
時,時而會列印,時而不會(日誌所寫的位置是task被執行時,如果被執行,一定會列印),而在IDEA裡面也是如此,但是神奇的是,只要IDEA重啟後,新編譯的外掛程式碼才會生效,而在專案開啟時,重新編譯外掛在放入原來目錄下,是不行的,必須重啟IDEA。
不知道是什麼原因引起,但這樣極大拖慢了除錯速度。
但沒有辦法,找不到原因。
原始碼
SpringBoot的這個外掛原始碼並不多,但是關聯性很強,幾乎每句都是使用Gradle提供的功能,這就導致不熟悉Gradle底層API,很難看懂。
這個外掛原始碼並不是單獨的專案,而是在SpringBoot原始碼下的一個小模組,位於下面這個路徑。
我們首先開啟他的build.gradle,可以看到他對外掛的配置,比如id為org.springframework.boot,外掛的實現類是org.springframework.boot.gradle.plugin.SpringBootPlugin。
java
gradlePlugin {
plugins {
springBootPlugin {
id = "org.springframework.boot"
displayName = "Spring Boot Gradle Plugin"
description = "Spring Boot Gradle Plugin"
implementationClass = "org.springframework.boot.gradle.plugin.SpringBootPlugin"
}
}
}
所以,我們應該從SpringBootPlugin下的apply下開始看,這是Gradle進行回撥的地方,也就是入口。
java
@Override
public void apply(Project project) {
verifyGradleVersion();
createExtension(project);
Configuration bootArchives = createBootArchivesConfiguration(project);
registerPluginActions(project, bootArchives);
}
第一句是驗證版本,就不看了,第二句是建立一個擴充套件,在上一篇文章我們演示擴充套件是如何使用的,在這裡SpringBoot建立了一個名為springBoot的擴充套件,例項是SpringBootExtension。
java
private void createExtension(Project project) {
project.getExtensions().create("springBoot", SpringBootExtension.class, project);
}
檢視SpringBootExtension後,可以發現能配置一個mainClass屬性,還有buildInfo,他用來生成META-INF/build-info.properties檔案,用的不多,就不說了,如下,是他的基本用法,之後執行bootJar
任務後就會生成上面這個檔案。
java
springBoot{
mainClass="com.xh"
buildInfo {
println(this.destinationDir)
properties.group="com.h"
}
}
createBootArchivesConfiguration方法用來建立一個名為bootArchives的Configuration。
最後就是registerPluginActions,用來註冊任務,bootJar任務就是從這裡註冊的。
java
private void registerPluginActions(Project project, Configuration bootArchives) {
SinglePublishedArtifact singlePublishedArtifact = new SinglePublishedArtifact(bootArchives.getArtifacts());
@SuppressWarnings("deprecation")
List<PluginApplicationAction> actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact),
new WarPluginAction(singlePublishedArtifact), new MavenPluginAction(bootArchives.getUploadTaskName()),
new DependencyManagementPluginAction(), new ApplicationPluginAction(), new KotlinPluginAction());
for (PluginApplicationAction action : actions) {
withPluginClassOfAction(action,
(pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project)));
}
}
上面程式碼就是依次呼叫實現類中的execute方法,比如bootJar任務是由JavaPluginAction實現,除了bootJar任務,還有bootWar任務等,但我們主要分析的是bootJar任務,所以直接看JavaPluginAction.execute方法。
JavaPluginAction
在JavaPluginAction.execute方法下做了很多事,最關鍵的一步就是呼叫configureBootJarTask配置bootJar任務,如下。
java
private TaskProvider<BootJar> configureBootJarTask(Project project) {
....
return project.getTasks().register(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class, (bootJar) -> {
bootJar.setDescription(
"Assembles an executable jar archive containing the main classes and their dependencies.");
bootJar.setGroup(BasePlugin.BUILD_GROUP);
bootJar.classpath(classpath);
Provider<String> manifestStartClass = project
.provider(() -> (String) bootJar.getManifest().getAttributes().get("Start-Class"));
bootJar.getMainClass().convention(resolveMainClassName.flatMap((resolver) -> manifestStartClass.isPresent()
? manifestStartClass : resolveMainClassName.get().readMainClassName()));
});
}
SpringBoot這個外掛打包Jar並不是從0開始打包,而是繼承了Gradle提供好的一個Jar任務,只需要配置幾個值就可以了,比如main方法所在類,還有jar檔案中目錄結構是怎樣的,需要放入哪些檔案等,如上面,SpringBoot自己實現了一個BootJar,繼承自Gradle提供的Jar任務,並向manifest檔案中配置一個Start-Classs屬性,這個屬性的值是我們自己的main方法入口,在執行時,首先啟動的是org.springframework.boot.loader.JarLauncher
由他通過反射啟動Start-Classs所指向的類。
BootJar
核心還是在BootJar中的配置,其構造方法中呼叫了下面這個方法。
java
private void configureBootInfSpec(CopySpec bootInfSpec) {
bootInfSpec.into("classes", fromCallTo(this::classpathDirectories));
bootInfSpec.into("lib", fromCallTo(this::classpathFiles)).eachFile(this.support::excludeNonZipFiles);
this.support.moveModuleInfoToRoot(bootInfSpec);
this.support.moveMetaInfToRoot(bootInfSpec);
}
上面方法用來做檔案複製,也就是將我們編寫的所有class,複製在classes資料夾下,並把所有第三方jar包,複製到lib目錄下,這裡的源(指的是我們的class和jar)路徑就是從java這個外掛提供的API中獲得,可以從上面configureBootJarTask方法下看到,將獲得的classpath輸出時候,將會是一堆jar檔案,還有我們class存放的父路徑。
其中引數CopySpec是Gradle提供用來做檔案複製的一個API。
在複製過程中,會把所有第三方jar的壓縮級別設定為STORED,而這兩種級別具體不太瞭解,只知道SpringBoot在啟動時候,會檢測第三方jar的壓縮級別如果不是ZipCompression.STORED ,那就會丟擲異常,導致無法啟動。
java
protected ZipCompression resolveZipCompression(FileCopyDetails details) {
return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
}
在原始碼中,有這樣一段程式碼,如下,他是提供一個介面讓我們可以在打包時候複製一些自定義檔案的。
java
public CopySpec bootInf(Action<CopySpec> action) {
CopySpec bootInf = getBootInf();
action.execute(bootInf);
return bootInf;
}
下面是他的使用方式,作用是在打包時,把/home/xxx.jar這個檔案複製到/lib下。
java
tasks.named("bootJar"){
bootInf{
from("/home/xxx.jar")
into("/lib")
}
}
還可以設定classpath等。
- 用Kotlin寫Elasticsearch究竟有多爽?
- 玩轉Gradle構建工具(七)、SpringBoot外掛原始碼分析
- 玩轉Gradle構建工具(六)、外掛編寫
- 什麼是C10K問題以及NIO實現原理
- 群友的這個需求不錯,馬上安排!
- 優雅的實現介面統一返回,看著真舒服
- SpringBoot動態註冊靜態資源對映分析
- 消失的這一星期,我寫了一個加減乘除法作業系統
- Java實現貪吃蛇能玩出什麼花樣?桌面圖示版的體驗一下
- SpringBoot中jar啟動原理你可能不知道的事情
- 開源一款自動構建/部署工具CoolDeploy
- 更新草稿功能、釋出自動部署工具
- 一個埠可以讓多個程序繫結嗎?
- TCP重置攻擊演示
- Linux下必須掌握的6條Touch命令技巧
- Java冷門包java.beans有什麼用?
- Kotlin遇到一個任務由多個接口才能完成時,如何優雅的執行
- 開源一款個人部落格OneBlog
- 《盲盒交友》內部如何保證不重複抽到同一個人?
- 高仿微信小程式進入時載入動畫