自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入理解 Gradle Tooling API

開發(fā) 前端
本文首先從現(xiàn)代 IDE 與構(gòu)建系統(tǒng)的結(jié)合方式出發(fā),引出 Gradle Tooling API,介紹了它對(duì)于 Gradle 構(gòu)建系統(tǒng)的特殊意義,然后通過(guò) Tooling API 具體的 API 及調(diào)用示例介紹了它的主要功能,最后在原理分析方面,結(jié)合源碼著重分析了跨進(jìn)程通信與版本兼容原理,這也是 Tooling API 中非常重要的兩個(gè)機(jī)制。

1. 簡(jiǎn)介

構(gòu)建系統(tǒng)是用來(lái)從源代碼生成目標(biāo)產(chǎn)物的自動(dòng)化工具,目標(biāo)產(chǎn)物包括庫(kù)、可執(zhí)行文件、生成的腳本等,構(gòu)建系統(tǒng)一般會(huì)提供平臺(tái)相關(guān)的可執(zhí)行程序,外部通過(guò)執(zhí)行命令的形式觸發(fā)構(gòu)建,如 GUN Make、Ant、CMake、Gradle 等等。Gradle 是一個(gè)靈活而強(qiáng)大的開源構(gòu)建系統(tǒng),它提供了跨平臺(tái)的可執(zhí)行程序,供外部在命令行窗口通過(guò)命令執(zhí)行 Gradle 構(gòu)建,如 ./gradlew assemble 命令觸發(fā) Gradle 構(gòu)建任務(wù)。

現(xiàn)代成熟的 IDE 中會(huì)把需要的構(gòu)建系統(tǒng)集成進(jìn)來(lái),結(jié)合多種命令行工具,封裝為一套自動(dòng)化的構(gòu)建工具,并提供構(gòu)建視圖工具,提高開發(fā)人員的生產(chǎn)力。在 IntelliJ IDEA 中,可以通過(guò) Gradle 視圖工具觸發(fā)執(zhí)行 Gradle 任務(wù),但它并不是通過(guò)封裝命令行工具來(lái)實(shí)現(xiàn)的,而是集成了 Gradle 專門提供的編程 SDK - Gradle Tooling API,通過(guò)此 API 可以將 Gradle 構(gòu)建能力嵌入到 IDE 或其他工具軟件中:

Gradle 為什么要專門提供一個(gè) Tooling API 供外部集成調(diào)用,而不是像其他構(gòu)建系統(tǒng)一樣,只提供基于可執(zhí)行程序的命令方式呢?Tooling API 是對(duì) Gradle 的一個(gè)重大擴(kuò)展,它提供了比命令方式更可控、更深入的構(gòu)建控制能力,可以讓 IDE 和其他工具更方便、緊密地和 Gradle 能力結(jié)合。Tooling API 接口可以直接返回構(gòu)建結(jié)果,無(wú)需像命令方式一樣再手動(dòng)解析命令行程序的日志輸出,并且可以獨(dú)立于版本運(yùn)行,這意味著相同版本的 Tooling API 可以處理不同 Gradle 版本的構(gòu)建,同時(shí)向前和向后兼容。

2. 接口功能及調(diào)用示例

2.1 接口功能

Tooling API 提供了執(zhí)行和監(jiān)控構(gòu)建、查詢構(gòu)建信息等功能:

  • 查詢構(gòu)建信息,包括項(xiàng)目結(jié)構(gòu)、項(xiàng)目依賴項(xiàng)、外部依賴項(xiàng)和項(xiàng)目任務(wù)等;
  • 執(zhí)行構(gòu)建任務(wù)并監(jiān)聽構(gòu)建的進(jìn)度信息;
  • 取消正在執(zhí)行的構(gòu)建任務(wù);
  • 自動(dòng)下載與項(xiàng)目匹配的 Gradle 版本;

關(guān)鍵 API 如下:

2.2 調(diào)用示例

查詢項(xiàng)目結(jié)構(gòu)和任務(wù)

try (ProjectConnection connection = GradleConnector.newConnector()
.forProjectDirectory(new File("someFolder"))
.connect()) {
GradleProject rootProject = connection.getModel(GradleProject.class);
Set<? extends GradleProject> subProject = rootProject.getChildren();
Set<? extends GradleTask> tasks = rootProject.getTasks();
}

如上文 API 介紹,首先通過(guò) Tooling API 的入口類 GradleConnector 創(chuàng)建一個(gè)到參與構(gòu)建工程的連接 ProjectConnection ,然后通過(guò) getModel(Class modelType) 獲取此工程的結(jié)構(gòu)信息模型 GradleProject,該模型包含我們要查詢的項(xiàng)目結(jié)構(gòu)、項(xiàng)目任務(wù)等信息。

執(zhí)行構(gòu)建任務(wù)

String[] gradleTasks = new String[]{"clean", "app:assembleDebug"};
try (ProjectConnection connection = GradleConnector.newConnector()
.forProjectDirectory(new File("someFolder"))
.connect()) {
BuildLauncher build = connection.newBuild();
build.forTasks(gradleTasks)
.addProgressListener(progressListener)
.setColorOutput(true)
.setJvmArguments(jvmArguments);
build.run();
}

此例中通過(guò) ProjectConnection 的 newBuild() 方法創(chuàng)建了一個(gè)用于執(zhí)行構(gòu)建任務(wù)的 BuildLauncher,然后通過(guò) forTasks(String... tasks) 配置要執(zhí)行的 Gradle 任務(wù)以及配置執(zhí)行進(jìn)度監(jiān)聽等等,最后通過(guò) run() 觸發(fā)執(zhí)行任務(wù)。

3. 原理分析

3.1 如何與 Gradle 構(gòu)建進(jìn)程通信?

Gradle Tooling API 并不具備真正的 Gradle 構(gòu)建能力,而是提供了調(diào)用本機(jī) Gradle 程序的入口,方便以編碼形式與 Gradle 通信,在我們自己的工具程序中通過(guò) API 觸發(fā)調(diào)用 Gradle 構(gòu)建能力后,還需要和真正的 Gradle 構(gòu)建程序進(jìn)行跨進(jìn)程通信。不論是通過(guò) Gradle Tooling API 與 Gradle 交互的 IDE 或工具程序,還是以 command 形式與 Gradle 交互的命令行窗口程序,這種跨進(jìn)程調(diào)用 Gradle 構(gòu)建程序的客戶端程序,都是一個(gè) Gradle client,真正執(zhí)行任務(wù)的 Gradle 構(gòu)建程序才是 Gradle build process.

Gradle daemon process 是長(zhǎng)期存在的 Gradle build process,通過(guò)規(guī)避構(gòu)建 Gradle JVM 環(huán)境和內(nèi)存緩存提高構(gòu)建速度,對(duì)于集成 Gradle Tooling API 的 Gradle client,會(huì)始終啟用 daemon process。也就是說(shuō),集成了 Gradle Tooling API 的工具程序,會(huì)始終與 daemon process 跨進(jìn)程通信,調(diào)用 Gradle 構(gòu)建能力。Gradle daemon process 是動(dòng)態(tài)創(chuàng)建的,Gradle client 若要連接到動(dòng)態(tài)創(chuàng)建的 daemon process,就需要通過(guò)服務(wù)注冊(cè)和服務(wù)發(fā)現(xiàn)機(jī)制,將 daemon process 注冊(cè)記錄下來(lái)并開放查詢,DaemonRegistry 就提供了這樣的機(jī)制。

客戶端 - Gradle Client

下面以獲取工程結(jié)構(gòu)信息為切入點(diǎn),從源碼角度分析 Gradle Tooling API 的跨進(jìn)程通信機(jī)制:

try (ProjectConnection connection = GradleConnector.newConnector()
.forProjectDirectory(new File("someFolder"))
.connect()) {
GradleProject rootProject = connection.getModel(GradleProject.class);
}

從代碼上看,雖然 ProjectConnection 像是建立了一個(gè)到 daemon process 的鏈接,但并沒有,而是在 getModel(Class modelType) 方法中才會(huì)真正去建立與 daemon process 的鏈接,此方法內(nèi)部,會(huì)從 Tooling API 側(cè)調(diào)用到 Gradle 源碼中,最后在 DefaultDaemonConnector.java 中查找可用的 daemon process:

public DaemonClientConnection connect(ExplainingSpec<DaemonContext> constraint) {
final Pair<Collection<DaemonInfo>, Collection<DaemonInfo>> idleBusy = partitionByState(daemonRegistry.getAll(), Idle);
final Collection<DaemonInfo> idleDaemons = idleBusy.getLeft();
final Collection<DaemonInfo> busyDaemons = idleBusy.getRight();
// Check to see if there are any compatible idle daemons
DaemonClientConnection connection = connectToIdleDaemon(idleDaemons, constraint);
if (connection != null) {
return connection;
}
// Check to see if there are any compatible canceled daemons and wait to see if one becomes idle
connection = connectToCanceledDaemon(busyDaemons, constraint);
if (connection != null) {
return connection;
}
// No compatible daemons available - start a new daemon
handleStopEvents(idleDaemons, busyDaemons);
return startDaemon(constraint);
}

通過(guò)以上 daemon process 查找邏輯及相關(guān)代碼,可以得出:

  1. Daemon process 包括 Idle、Busy、Canceled、StopRequested、Stopped、Broken 六種狀態(tài);
  2. 通過(guò) daemon process 模式執(zhí)行 Gradle 構(gòu)建時(shí),會(huì)依次嘗試查找 Idle、Canceled 狀態(tài)且環(huán)境兼容的 daemon process,如果沒有找到,就新建一個(gè)與 Gradle client 環(huán)境兼容的 daemon process;
  3. 所有的 Daemon process 記錄在 DaemonRegistry.java 注冊(cè)表中,供 Gradle client 獲取;
  4. Daemon process 的環(huán)境兼容判斷包括 Gradle 版本、文件編碼、JVM heap size 等屬性;
  5. 獲取到一個(gè)兼容的 daemon process 后,會(huì)通過(guò) Socket 鏈接到 daemon process 監(jiān)聽的端口,然后通過(guò) Socket 與 daemon process 通信;

服務(wù)端 - Daemon process

當(dāng)一個(gè) Gradle client 調(diào)用 Gradle 構(gòu)建能力時(shí),會(huì)觸發(fā) daemon process 的創(chuàng)建,進(jìn)程入口函數(shù)在 GradleDaemon.java 中,然后會(huì)轉(zhuǎn)到 DaemonMain.java 中初始化 process,最后在 TcpIncomingConnector.java 中開啟 Socket Server 并綁定監(jiān)聽一個(gè)指定的端口:

public ConnectionAcceptor accept(Action<ConnectCompletion> action, boolean allowRemote) {
final ServerSocketChannel serverSocket;
int localPort;
try {
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(addressFactory.getLocalBindingAddress(), 0));
localPort = serverSocket.socket().getLocalPort();
} catch (Exception e) {
throw UncheckedException.throwAsUncheckedException(e);
}
...
}

隨后會(huì)在 DaemonRegistryUpdater.java 中將 daemon process 記錄到注冊(cè)表中:

public void onStart(Address connectorAddress) {
LOGGER.info("{}{}", DaemonMessages.ADVERTISING_DAEMON, connectorAddress);
LOGGER.debug("Advertised daemon context: {}", daemonContext);
this.connectorAddress = connectorAddress;
daemonRegistry.store(new DaemonInfo(connectorAddress, daemonContext, token, Busy));
}

這樣 Gradle client 就可以在注冊(cè)表中獲取到兼容的 daemon process 及其端口,從而與 daemon process 建立連接實(shí)現(xiàn)通信,具體流程如下圖:

總結(jié)梳理一下 Tooling API 與 Gradle Daemon process 的連接建立流程:

  1. Tooling API 本身代碼量并不是太多,調(diào)用獲取項(xiàng)目信息接口經(jīng)過(guò) ModelProducer 抽象封裝后,會(huì)進(jìn)入到 Gradle 源碼中,但還屬于 Gradle client 進(jìn)程中;
  2. 在 DefaultDaemonConnector 中會(huì)嘗試從 DaemonRegistry 獲取可用的、兼容的 daemon process,如果沒有,就新建一個(gè) daemon process;
  3. Daemon process 啟動(dòng)后會(huì)通過(guò) Socket 綁定監(jiān)聽到固定端口,然后將監(jiān)聽端口等自身信息記錄到 DaemonRegistry 中,供 Gradle client 查詢、獲取以及建立連接;

3.2 如何實(shí)現(xiàn)向前和向后兼容?

Tooling API 支持 Gradle 2.6 及更高版本,即某一版本的 Tooling API 與其他版本 Gradle 向前和向后兼容,支持調(diào)用舊版或新版 Gradle 進(jìn)行 Gradle 構(gòu)建,但 Tooling API 所包含的接口功能并非適用于所有 Gradle 版本;Gradle 5.0 及更高版本對(duì) Tooling API 版本也有要求,需要 Tooling API 3.0 及更高版本。Gradle 和 Tooling API 不同版本之間是如何實(shí)現(xiàn)兼容的呢?

思考一個(gè)問(wèn)題,如果我們有兩個(gè)軟件:主軟件 A 和專門用于調(diào)用 A 的工具軟件 B,如何才能實(shí)現(xiàn) A、B 之間最大程度且優(yōu)雅的版本兼容?下面深入分析 Tooling API 和 Gradle 源碼,看看 Gradle 在版本兼容方面采取了哪些值得關(guān)注的技術(shù)方案。

Gradle 版本適配

在 Gradle Tooling API 源碼倉(cāng)庫(kù)中,有一張介紹獲取項(xiàng)目信息調(diào)用鏈的流程圖:

我們只關(guān)注圖中的 DefaultConnection- 從 Tooling API 調(diào)用到 Gradle launcher 模塊的關(guān)鍵類:

  • DefaultConnection has entry points to accept calls from different ToolingAPI versions

Tooling API 側(cè)最終在 DefaultToolingImplementationLoader.java 中通過(guò)自定義 URLClassLoader 加載 DefaultConnection,自定義 URLClassLoader 類加載路徑指定了對(duì)應(yīng) Gradle 版本 lib 下的 jar 包,從而可以實(shí)現(xiàn)加載不同 Gradle 版本的 DefaultConnection:

private ClassLoader createImplementationClassLoader(Distribution distribution, ProgressLoggerFactory progressLoggerFactory, InternalBuildProgressListener progressListener, ConnectionParameters connectionParameters, BuildCancellationToken cancellationToken) {
ClassPath implementationClasspath = distribution.getToolingImplementationClasspath(progressLoggerFactory, progressListener, connectionParameters, cancellationToken);
LOGGER.debug("Using tooling provider classpath: {}", implementationClasspath);
FilteringClassLoader.Spec filterSpec = new FilteringClassLoader.Spec();
filterSpec.allowPackage("org.gradle.tooling.internal.protocol");
filterSpec.allowClass(JavaVersion.class);
FilteringClassLoader filteringClassLoader = new FilteringClassLoader(classLoader, filterSpec);
return new VisitableURLClassLoader("tooling-implementation-loader", filteringClassLoader, implementationClasspath);
}

Tooling API 通過(guò)自定義 Java 類加載器調(diào)用到本機(jī)指定版本的 Gradle 源碼,需要注意的是,雖然 DefaultConnection 已經(jīng)是 Gradle 側(cè)的源碼,但還屬于 Gradle client 端進(jìn)程,即 IDE 等工具軟件程序中。

模型類適配

通過(guò) getModel(Class modelType) 方法可以從 Gradle daemon process 中獲取工程結(jié)構(gòu)信息模型 GradleProject,而不同 Gradle 版本可能有不同的 GradleProject 定義,如何在同一版本 Tooling API 中兼容多個(gè)版本的信息模型結(jié)構(gòu)呢?

Tooling API 在請(qǐng)求獲取信息模型前,會(huì)在 VersionDetails.java 中根據(jù) Gradle 版本判斷是否支持獲取該模型,若支持,才會(huì)向 daemon process 發(fā)出獲取請(qǐng)求。daemon process 將對(duì)應(yīng)版本的信息模型返回后,在 Tooling API 的 ProtocolToModelAdapter.java 中會(huì)對(duì)其封裝一層動(dòng)態(tài)代理,最終以 Proxy 形式返回:

private static <T> T createView(Class<T> targetType, Object sourceObject, ViewDecoration decoration, ViewGraphDetails graphDetails) {
......
// Create a proxy
InvocationHandlerImpl handler = new InvocationHandlerImpl(targetType, sourceObject, decorationsForThisType, graphDetails);
Object proxy = Proxy.newProxyInstance(viewType.getClassLoader(), new Class<?>[]{viewType}, handler);
handler.attachProxy(proxy);
return viewType.cast(proxy);
}

最終 Tooling API 返回的 GradleProject 僅僅是一個(gè)動(dòng)態(tài)代理接口,如下:

public interface GradleProject extends HierarchicalElement, BuildableElement, ProjectModel {
......
File getBuildDirectory() throws UnsupportedMethodException;
}

可以看到,即使是支持的信息模型,其中的某些內(nèi)容也可能由于 Gradle 版本不匹配而不支持獲取,調(diào)用會(huì)拋出 UnsupportedMethodException 異常。

通過(guò)動(dòng)態(tài)代理接口方式,實(shí)現(xiàn)了適配不同版本的模型類,但這種方式也帶來(lái)一個(gè)缺點(diǎn),在 Tooling API 側(cè)由于只能拿到模型信息的接口,并不是真正的模型實(shí)體類,那后續(xù)對(duì)整個(gè)模型信息類做序列化或傳遞時(shí),就需要再做一層轉(zhuǎn)換,構(gòu)造出一個(gè)真正包含內(nèi)容的實(shí)體類,Android sdktools 庫(kù)中就針對(duì) AndroidProject 模型,構(gòu)造了的真正包含內(nèi)容的實(shí)體類 IdeAndroidProjectImpl。

4. 總結(jié)

本文首先從現(xiàn)代 IDE 與構(gòu)建系統(tǒng)的結(jié)合方式出發(fā),引出 Gradle Tooling API,介紹了它對(duì)于 Gradle 構(gòu)建系統(tǒng)的特殊意義,然后通過(guò) Tooling API 具體的 API 及調(diào)用示例介紹了它的主要功能,最后在原理分析方面,結(jié)合源碼著重分析了跨進(jìn)程通信與版本兼容原理,這也是 Tooling API 中非常重要的兩個(gè)機(jī)制。

通過(guò)對(duì) Gradle Tooling API 的分析學(xué)習(xí),可以對(duì) Tooling API 整體的架構(gòu)原理深度掌握,從而更好地基于它開發(fā)具有 Gradle 能力的工具軟件,另外還可以學(xué)習(xí)到一些類似技術(shù)架構(gòu)場(chǎng)景下的方法論:在需要與程序運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建的服務(wù)通訊時(shí),一般可以引入服務(wù)注冊(cè)和服務(wù)發(fā)現(xiàn)機(jī)制去實(shí)現(xiàn)對(duì)動(dòng)態(tài)服務(wù)的查詢、連接;作為一個(gè)供外部接入的工具程序,在同類程序都僅提供命令行方式時(shí),我們要敢于打破常規(guī)、提供一種全新的方式,從而可以更大程度給其他軟件賦能,實(shí)現(xiàn)雙方共贏。

5. 參考文章

  • org.gradle.tooling (Gradle API 7.2)https://docs.gradle.org/current/javadoc/org/gradle/tooling/package-summary.html
  • Gradle & Third-party Toolshttps://docs.gradle.org/current/userguide/third_party_integration.html#embedding
  • Gradle | Gradle Featureshttps://gradle.org/features/#embed-gradle-with-tooling-api
  • The Gradle Daemonhttps://docs.gradle.org/current/userguide/gradle_daemon.html
責(zé)任編輯:未麗燕 來(lái)源: 字節(jié)跳動(dòng)技術(shù)團(tuán)隊(duì)
相關(guān)推薦

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數(shù)據(jù)結(jié)構(gòu)hash函數(shù)

2020-07-21 08:26:08

SpringSecurity過(guò)濾器

2013-09-22 14:57:19

AtWood

2009-09-25 09:14:35

Hibernate日志

2023-10-19 11:12:15

Netty代碼

2021-02-17 11:25:33

前端JavaScriptthis

2020-09-23 10:00:26

Redis數(shù)據(jù)庫(kù)命令

2017-01-10 08:48:21

2017-08-15 13:05:58

Serverless架構(gòu)開發(fā)運(yùn)維

2019-06-25 10:32:19

UDP編程通信

2024-02-21 21:14:20

編程語(yǔ)言開發(fā)Golang

2024-10-12 15:18:05

PythonAPI操作系統(tǒng)

2022-11-04 09:43:05

Java線程

2015-11-04 09:57:18

JavaScript原型

2021-05-13 21:27:24

ThreadLocal多線程多線程并發(fā)安全

2013-06-14 09:27:51

Express.jsJavaScript

2021-04-20 23:25:16

執(zhí)行函數(shù)變量

2023-02-10 08:11:43

Linux系統(tǒng)調(diào)用

2017-01-13 22:42:15

iosswift
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)