深入理解 Gradle Tooling API

发表于 2年以前  | 总阅读数:310 次

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

此例中通过 ProjectConnectionnewBuild() 方法创建了一个用于执行构建任务的 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<T> 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<T> 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

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/cftj6WueoHlLh-So9REEXQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237271次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录