首页 >> 大全

深入理解 Gradle Tooling API

2023-12-19 大全 22 作者:考证青年

1. 简介

构建系统是用来从源代码生成目标产物的自动化工具,目标产物包括库、可执行文件、生成的脚本等,构建系统一般会提供平台相关的可执行程序,外部通过执行命令的形式触发构建,如 GUN Make、Ant、CMake、 等等。 是一个灵活而强大的开源构建系统,它提供了跨平台的可执行程序,供外部在命令行窗口通过命令执行 构建,如 ./ 命令触发 构建任务。

现代成熟的 IDE 中会把需要的构建系统集成进来,结合多种命令行工具,封装为一套自动化的构建工具,并提供构建视图工具,提高开发人员的生产力。在 IDEA 中,可以通过 视图工具触发执行 任务,但它并不是通过封装命令行工具来实现的,而是集成了 专门提供的编程 SDK - API,通过此 API 可以将 构建能力嵌入到 IDE 或其他工具软件中:

为什么要专门提供一个 API 供外部集成调用,而不是像其他构建系统一样,只提供基于可执行程序的命令方式呢? API 是对 的一个重大扩展,它提供了比命令方式更可控、更深入的构建控制能力,可以让 IDE 和其他工具更方便、紧密地和 能力结合。 API 接口可以直接返回构建结果,无需像命令方式一样再手动解析命令行程序的日志输出,并且可以独立于版本运行,这意味着相同版本的 API 可以处理不同 版本的构建,同时向前和向后兼容。

2. 接口功能及调用示例 2.1 接口功能

API 提供了执行和监控构建、查询构建信息等功能:

关键 API 如下:

2.2 调用示例 查询项目结构和任务

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

如上文 API 介绍,首先通过 API 的入口类 创建一个到参与构建工程的连接 ,然后通过 (Class ) 获取此工程的结构信息模型 ,该模型包含我们要查询的项目结构、项目任务等信息。

执行构建任务

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

此例中通过 的 () 方法创建了一个用于执行构建任务的 ,然后通过 (... tasks) 配置要执行的 任务以及配置执行进度监听等等,最后通过 run() 触发执行任务。

3. 原理分析 3.1 如何与 构建进程通信?

API 并不具备真正的 构建能力,而是提供了调用本机 程序的入口,方便以编码形式与 通信,在我们自己的工具程序中通过 API 触发调用 构建能力后,还需要和真正的 构建程序进行跨进程通信。不论是通过 API 与 交互的 IDE 或工具程序,还是以 形式与 交互的命令行窗口程序,这种跨进程调用 构建程序的客户端程序,都是一个 ,真正执行任务的 构建程序才是 build .

是长期存在的 build ,通过规避构建 JVM 环境和内存缓存提高构建速度,对于集成 API 的 ,会始终启用 。也就是说,集成了 API 的工具程序,会始终与 跨进程通信,调用 构建能力。 是动态创建的, 若要连接到动态创建的 ,就需要通过服务注册和服务发现机制,将 注册记录下来并开放查询, 就提供了这样的机制。

客户端 -

下面以获取工程结构信息为切入点,从源码角度分析 API 的跨进程通信机制:

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

从代码上看,虽然 像是建立了一个到 的链接,但并没有,而是在 (Class ) 方法中才会真正去建立与 的链接,此方法内部,会从 API 侧调用到 源码中,最后在 or.java 中查找可用的 :

深入理解计算机系统__深入理解新发展理念

public DaemonClientConnection connect(ExplainingSpec constraint) {final Pair, Collection> idleBusy = partitionByState(daemonRegistry.getAll(), Idle);final Collection idleDaemons = idleBusy.getLeft();final Collection busyDaemons = idleBusy.getRight();// Check to see if there are any compatible idle daemonsDaemonClientConnection 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 idleconnection = connectToCanceledDaemon(busyDaemons, constraint);if (connection != null) {return connection;}// No compatible daemons available - start a new daemonhandleStopEvents(idleDaemons, busyDaemons);return startDaemon(constraint);
}

通过以上 查找逻辑及相关代码,可以得出:

包括 Idle、Busy、、、、 六种状态;

通过 模式执行 构建时,会依次尝试查找 Idle、 状态且环境兼容的 ,如果没有找到,就新建一个与 环境兼容的 ;

所有的 记录在 .java 注册表中,供 获取;

的环境兼容判断包括 版本、文件编码、JVM heap size 等属性;

获取到一个兼容的 后,会通过 链接到 监听的端口,然后通过 与 通信;

服务端 -

当一个 调用 构建能力时,会触发 的创建,进程入口函数在 .java 中,然后会转到 .java 中初始化 ,最后在 .java 中开启 并绑定监听一个指定的端口:

public ConnectionAcceptor accept(Action 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);}...
}

随后会在 r.java 中将 记录到注册表中:

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

这样 就可以在注册表中获取到兼容的 及其端口,从而与 建立连接实现通信,具体流程如下图:

总结梳理一下 API 与 的连接建立流程:

API 本身代码量并不是太多,调用获取项目信息接口经过 抽象封装后,会进入到 源码中,但还属于 进程中;

在 or 中会尝试从 获取可用的、兼容的 ,如果没有,就新建一个 ;

启动后会通过 绑定监听到固定端口,然后将监听端口等自身信息记录到 中,供 查询、获取以及建立连接;

3.2 如何实现向前和向后兼容?

API 支持 2.6 及更高版本,即某一版本的 API 与其他版本 向前和向后兼容,支持调用旧版或新版 进行 构建,但 API 所包含的接口功能并非适用于所有 版本; 5.0 及更高版本对 API 版本也有要求,需要 API 3.0 及更高版本。 和 API 不同版本之间是如何实现兼容的呢?

思考一个问题,如果我们有两个软件:主软件 A 和专门用于调用 A 的工具软件 B,如何才能实现 A、B 之间最大程度且优雅的版本兼容?下面深入分析 API 和 源码,看看 在版本兼容方面采取了哪些值得关注的技术方案。

深入理解新发展理念__深入理解计算机系统

版本适配

在 API 源码仓库中,有一张介绍获取项目信息调用链的流程图:

我们只关注图中的 - 从 API 调用到 模块的关键类:

has entry to calls from

API 侧最终在 .java 中通过自定义 加载 ,自定义 类加载路径指定了对应 版本 lib 下的 jar 包,从而可以实现加载不同 版本的 :

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

API 通过自定义 Java 类加载器调用到本机指定版本的 源码,需要注意的是,虽然 已经是 侧的源码,但还属于 端进程,即 IDE 等工具软件程序中。

模型类适配

通过 (Class ) 方法可以从 中获取工程结构信息模型 ,而不同 版本可能有不同的 定义,如何在同一版本 API 中兼容多个版本的信息模型结构呢?

API 在请求获取信息模型前,会在 .java 中根据 版本判断是否支持获取该模型,若支持,才会向 发出获取请求。 将对应版本的信息模型返回后,在 API 的 er.java 中会对其封装一层动态代理,最终以 Proxy 形式返回:

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

最终 API 返回的 仅仅是一个动态代理接口,如下:

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

可以看到,即使是支持的信息模型,其中的某些内容也可能由于 版本不匹配而不支持获取,调用会抛出 异常。

通过动态代理接口方式,实现了适配不同版本的模型类,但这种方式也带来一个缺点,在 API 侧由于只能拿到模型信息的接口,并不是真正的模型实体类,那后续对整个模型信息类做序列化或传递时,就需要再做一层转换,构造出一个真正包含内容的实体类, 库中就针对 模型,构造了的真正包含内容的实体类 l。

4. 总结

本文首先从现代 IDE 与构建系统的结合方式出发,引出 API,介绍了它对于 构建系统的特殊意义,然后通过 API 具体的 API 及调用示例介绍了它的主要功能,最后在原理分析方面,结合源码着重分析了跨进程通信与版本兼容原理,这也是 API 中非常重要的两个机制。

通过对 API 的分析学习,可以对 API 整体的架构原理深度掌握,从而更好地基于它开发具有 能力的工具软件,另外还可以学习到一些类似技术架构场景下的方法论:在需要与程序运行时动态创建的服务通讯时,一般可以引入服务注册和服务发现机制去实现对动态服务的查询、连接;作为一个供外部接入的工具程序,在同类程序都仅提供命令行方式时,我们要敢于打破常规、提供一种全新的方式,从而可以更大程度给其他软件赋能,实现双方共赢。

5. 参考文章

6. 加入我们

我们是字节跳动终端技术团队( )下的 Tools 团队,负责打造公司范围内,面向不同业务场景的研发工具,提升移动应用研发效率。目前急需寻找 移动研发工程师 / iOS 移动研发工程师 / 服务端研发工程师。

了解更多信息请联系:,邮件主题 简历-姓名-求职意向-期望城市-电话。

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了