成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

深入理解 Gradle Tooling API

開發 前端
本文首先從現代 IDE 與構建系統的結合方式出發,引出 Gradle Tooling API,介紹了它對于 Gradle 構建系統的特殊意義,然后通過 Tooling API 具體的 API 及調用示例介紹了它的主要功能,最后在原理分析方面,結合源碼著重分析了跨進程通信與版本兼容原理,這也是 Tooling API 中非常重要的兩個機制。

1. 簡介

構建系統是用來從源代碼生成目標產物的自動化工具,目標產物包括庫、可執行文件、生成的腳本等,構建系統一般會提供平臺相關的可執行程序,外部通過執行命令的形式觸發構建,如 GUN Make、Ant、CMake、Gradle 等等。Gradle 是一個靈活而強大的開源構建系統,它提供了跨平臺的可執行程序,供外部在命令行窗口通過命令執行 Gradle 構建,如 ./gradlew assemble 命令觸發 Gradle 構建任務。

現代成熟的 IDE 中會把需要的構建系統集成進來,結合多種命令行工具,封裝為一套自動化的構建工具,并提供構建視圖工具,提高開發人員的生產力。在 IntelliJ IDEA 中,可以通過 Gradle 視圖工具觸發執行 Gradle 任務,但它并不是通過封裝命令行工具來實現的,而是集成了 Gradle 專門提供的編程 SDK - Gradle Tooling API,通過此 API 可以將 Gradle 構建能力嵌入到 IDE 或其他工具軟件中:

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

2. 接口功能及調用示例

2.1 接口功能

Tooling API 提供了執行和監控構建、查詢構建信息等功能:

  • 查詢構建信息,包括項目結構、項目依賴項、外部依賴項和項目任務等;
  • 執行構建任務并監聽構建的進度信息;
  • 取消正在執行的構建任務;
  • 自動下載與項目匹配的 Gradle 版本;

關鍵 API 如下:

2.2 調用示例

查詢項目結構和任務

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 介紹,首先通過 Tooling API 的入口類 GradleConnector 創建一個到參與構建工程的連接 ProjectConnection ,然后通過 getModel(Class modelType) 獲取此工程的結構信息模型 GradleProject,該模型包含我們要查詢的項目結構、項目任務等信息。

執行構建任務

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();
}

此例中通過 ProjectConnection 的 newBuild() 方法創建了一個用于執行構建任務的 BuildLauncher,然后通過 forTasks(String... tasks) 配置要執行的 Gradle 任務以及配置執行進度監聽等等,最后通過 run() 觸發執行任務。

3. 原理分析

3.1 如何與 Gradle 構建進程通信?

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

Gradle daemon process 是長期存在的 Gradle build process,通過規避構建 Gradle JVM 環境和內存緩存提高構建速度,對于集成 Gradle Tooling API 的 Gradle client,會始終啟用 daemon process。也就是說,集成了 Gradle Tooling API 的工具程序,會始終與 daemon process 跨進程通信,調用 Gradle 構建能力。Gradle daemon process 是動態創建的,Gradle client 若要連接到動態創建的 daemon process,就需要通過服務注冊和服務發現機制,將 daemon process 注冊記錄下來并開放查詢,DaemonRegistry 就提供了這樣的機制。

客戶端 - Gradle Client

下面以獲取工程結構信息為切入點,從源碼角度分析 Gradle Tooling API 的跨進程通信機制:

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

從代碼上看,雖然 ProjectConnection 像是建立了一個到 daemon process 的鏈接,但并沒有,而是在 getModel(Class modelType) 方法中才會真正去建立與 daemon process 的鏈接,此方法內部,會從 Tooling API 側調用到 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);
}

通過以上 daemon process 查找邏輯及相關代碼,可以得出:

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

服務端 - Daemon process

當一個 Gradle client 調用 Gradle 構建能力時,會觸發 daemon process 的創建,進程入口函數在 GradleDaemon.java 中,然后會轉到 DaemonMain.java 中初始化 process,最后在 TcpIncomingConnector.java 中開啟 Socket Server 并綁定監聽一個指定的端口:

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);
}
...
}

隨后會在 DaemonRegistryUpdater.java 中將 daemon process 記錄到注冊表中:

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 就可以在注冊表中獲取到兼容的 daemon process 及其端口,從而與 daemon process 建立連接實現通信,具體流程如下圖:

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

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

3.2 如何實現向前和向后兼容?

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

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

Gradle 版本適配

在 Gradle Tooling API 源碼倉庫中,有一張介紹獲取項目信息調用鏈的流程圖:

我們只關注圖中的 DefaultConnection- 從 Tooling API 調用到 Gradle launcher 模塊的關鍵類:

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

Tooling API 側最終在 DefaultToolingImplementationLoader.java 中通過自定義 URLClassLoader 加載 DefaultConnection,自定義 URLClassLoader 類加載路徑指定了對應 Gradle 版本 lib 下的 jar 包,從而可以實現加載不同 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 通過自定義 Java 類加載器調用到本機指定版本的 Gradle 源碼,需要注意的是,雖然 DefaultConnection 已經是 Gradle 側的源碼,但還屬于 Gradle client 端進程,即 IDE 等工具軟件程序中。

模型類適配

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

Tooling API 在請求獲取信息模型前,會在 VersionDetails.java 中根據 Gradle 版本判斷是否支持獲取該模型,若支持,才會向 daemon process 發出獲取請求。daemon process 將對應版本的信息模型返回后,在 Tooling API 的 ProtocolToModelAdapter.java 中會對其封裝一層動態代理,最終以 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 僅僅是一個動態代理接口,如下:

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

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

通過動態代理接口方式,實現了適配不同版本的模型類,但這種方式也帶來一個缺點,在 Tooling API 側由于只能拿到模型信息的接口,并不是真正的模型實體類,那后續對整個模型信息類做序列化或傳遞時,就需要再做一層轉換,構造出一個真正包含內容的實體類,Android sdktools 庫中就針對 AndroidProject 模型,構造了的真正包含內容的實體類 IdeAndroidProjectImpl。

4. 總結

本文首先從現代 IDE 與構建系統的結合方式出發,引出 Gradle Tooling API,介紹了它對于 Gradle 構建系統的特殊意義,然后通過 Tooling API 具體的 API 及調用示例介紹了它的主要功能,最后在原理分析方面,結合源碼著重分析了跨進程通信與版本兼容原理,這也是 Tooling API 中非常重要的兩個機制。

通過對 Gradle Tooling API 的分析學習,可以對 Tooling API 整體的架構原理深度掌握,從而更好地基于它開發具有 Gradle 能力的工具軟件,另外還可以學習到一些類似技術架構場景下的方法論:在需要與程序運行時動態創建的服務通訊時,一般可以引入服務注冊和服務發現機制去實現對動態服務的查詢、連接;作為一個供外部接入的工具程序,在同類程序都僅提供命令行方式時,我們要敢于打破常規、提供一種全新的方式,從而可以更大程度給其他軟件賦能,實現雙方共贏。

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
責任編輯:未麗燕 來源: 字節跳動技術團隊
相關推薦

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數據結構hash函數

2020-07-21 08:26:08

SpringSecurity過濾器

2023-10-19 11:12:15

Netty代碼

2021-02-17 11:25:33

前端JavaScriptthis

2009-09-25 09:14:35

Hibernate日志

2013-09-22 14:57:19

AtWood

2020-09-23 10:00:26

Redis數據庫命令

2019-06-25 10:32:19

UDP編程通信

2017-01-10 08:48:21

2024-02-21 21:14:20

編程語言開發Golang

2025-05-06 00:43:00

MySQL日志文件MIXED 3

2017-08-15 13:05:58

Serverless架構開發運維

2025-06-05 05:51:33

2024-10-12 15:18:05

PythonAPI操作系統

2015-11-04 09:57:18

JavaScript原型

2013-06-14 09:27:51

Express.jsJavaScript

2021-04-20 23:25:16

執行函數變量

2011-04-11 16:48:12

Solaris權限

2024-03-12 00:00:00

Sora技術數據
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 午夜久久久 | www日本在线 | 色综合天天综合网国产成人网 | 中文字幕在线不卡播放 | 国产黄色精品 | 视频一区欧美 | 国产四虎 | 欧美精品一区二区三区视频 | 国产成人精品一区二三区在线观看 | 欧美久久久久久久 | 一区二区三区不卡视频 | 久久伊人一区二区 | 国产精品国产a级 | 国产网站在线免费观看 | 毛片网站在线观看 | 成人天堂噜噜噜 | 狠狠狠 | 日本中文字幕在线观看 | 欧美日韩中文在线 | av网站免费在线观看 | 中文字幕一区二区三区四区五区 | 久久久久久久久国产精品 | 欧美激情综合五月色丁香小说 | 国产一在线观看 | 成人欧美一区二区三区白人 | 国产精品久久久久免费 | 国产欧美精品一区二区 | 国产精品久久久久久久久久久久 | 成人欧美一区二区三区在线播放 | 在线91| 欧美精品一区二区三区在线播放 | 伊人久久综合 | 91精品国产综合久久精品 | 中文字幕国产第一页 | 国产精品成人在线播放 | 污片在线观看 | 视频在线观看一区二区 | 久久综合一区二区 | 成人午夜激情 | 国产乱码精品一区二区三区忘忧草 | 欧美高清成人 |