玩轉Gradle構建工具(七)、SpringBoot外掛原始碼分析

語言: CN / TW / HK

theme: cyanosis highlight: agate


持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第7天,點選檢視活動詳情

前言

本系列目錄

  1. Task
  2. Project、Task常用API
  3. 檔案操作
  4. 依賴管理
  5. 多模組構建
  6. 外掛編寫
  7. SpringBoot外掛原始碼分析
  8. 過度到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原始碼下的一個小模組,位於下面這個路徑。

image.png

我們首先開啟他的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等。