打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
软件工程十大技术之三:软件构建技术

大纲

  1. 三大构建工具比较——Ant vs Maven vs Gradle

  2. 分布式构建系统:Bazel与distcc

  3. Gradle与Maven增量构建技术

一、三大构建工具比较——Ant vs Maven vs Gradle


我们将探讨三个主要构建JVM生态系统的Java构建自动化工具–Ant,Maven和Gradle。

1. Apache Ant

最初,Make是除了自行开发的解决方案之外唯一的构建自动化工具。Make自1976年以来一直存在,因此,它在Java早期用于构建Java应用程序。

但是,C程序中的许多约定都不适合Java生态系统,所以最好将Ant作为更好的替代方案发布。

Apache Ant(“另一个整洁的工具”)是一个Java库,用于自动化Java应用程序的构建过程。**此外,Ant可用于构建非Java应用程序。它最初是Apache Tomcat代码库的一部分,并于2000年作为独立项目发布。

在许多方面,Ant与Make非常相似,并且它非常简单,因此任何人都可以在没有任何特定先决条件的情况下开始使用它。Ant构建文件是用XML编写的,按照惯例,它们被称为build.xml。

Ant的主要好处是它的灵活性。Ant不强加任何编码约定或项目结构。**因此,这意味着Ant要求开发人员自己编写所有命令,这有时会导致难以维护的巨大XML构建文件。

由于没有约定,只知道Ant并不意味着我们将很快理解任何Ant构建文件。习惯于使用不熟悉的Ant文件可能需要一些时间,与其他更新的工具相比,这是一个缺点。

起初,Ant没有内置的依赖管理支持。但是,由于依赖管理在以后的几年中成为必需,Apache Ivy被开发为Apache Ant项目的子项目。它与Apache Ant集成,遵循相同的设计原则。

但是,由于在使用不可管理的XML构建文件时没有内置支持依赖关系管理和挫折的初始Ant限制导致了Maven的创建。

2. Apache Maven

Apache Maven是一个依赖项管理和构建自动化工具,主要用于Java应用程序。Maven继续像Ant一样使用XML文件,但是更易于管理。这里的规则名称是约定优于配置。

虽然Ant提供了灵活性并且需要从头开始编写所有内容,但Maven依赖于约定并提供预定义的命令(目标)。

简而言之,Maven允许我们专注于我们的构建应该做什么,并为我们提供了实现它的框架。Maven的另一个积极方面是它为依赖管理提供了内置支持。

Maven的配置文件包含构建和依赖管理指令,按照惯例称为pom.xml。此外,Maven还规定了严格的项目结构,而Ant也提供了灵活性。

与Ant相反,无需手动定义构建过程中的每个阶段。相反,我们可以简单地调用Maven的内置命令。

正如官方页面所述,Maven的核心可以被认为是一个插件执行框架,因为所有工作都是通过插件完成的。Maven支持各种可用插件,并且每个插件都可以进行额外配置。

其中一个可用的插件是Apache Maven Dependency Plugin,它具有一个复制依赖项目标,可以将我们的依赖项复制到指定的目录。

要显示此插件的运行情况,让我们在pom.xml文件中包含此插件,并为依赖项配置输出目录

Maven变得非常受欢迎,因为构建文件现在已经标准化,与Ant相比,维护构建文件的时间要少得多。但是,虽然比Ant文件更标准化,但Maven配置文件仍然会变得庞大而繁琐。

Maven的严格约定的代价是不如Ant灵活。目标定制非常困难,因此与Ant相比,编写自定义构建脚本要困难得多。

尽管Maven在使应用程序的构建过程更容易和更标准化方面取得了一些重大改进,但由于其灵活性远低于Ant,因此仍然需要付出代价。这导致了Gradle的创造,它结合了两者的优点–Ant的灵活性和Maven的功能。

3. Gradle

Gradle是依赖关系管理和构建自动化工具,它基于Ant和Maven的概念。

关于Gradle,我们可以注意到的第一件事就是它不使用XML文件,这与Ant或Maven不同。

随着时间的推移,开发人员越来越有兴趣使用特定于域的语言 - 这简单地说,允许他们使用为特定域定制的语言来解决特定域中的问题。

这是由Gradle采用的,它使用基于Groovy的DSL 。由于该语言专门用于解决特定的域问题,因此这导致较小的配置文件较少混乱。Gradle的配置文件通常称为build.gradle。

4.结论

在本文中,我们介绍了Ant,Maven和Gradle - 三种Java构建自动化工具。

毫不奇怪,Maven占据了当今构建工具市场的大部分。然而,Gradle在更复杂的代码库中得到了很好的采用,包括许多开源项目,如Spring。

二、分布式构建系统:Bazel与distcc


1、Bazel 构建工具介绍

什么是 Bazel

Bazel 是一个开源的构建和测试工具,类似于Make、Maven 及 Gradle。它使用一种人易于理解的高级构建语言。Bazel 支持多种开发语言的项目,能够基于多个平台来构建。Bazel 支持跨多个制品库和大规模用户的大型代码仓库。

为什么我们需要 Bazel

Bazel 具有以下优势:

  • 高级构建语言 Bazel 使用一种抽象的、人易于理解的、语义级别的高级语言来描述项目的构建属性。与其他工具不同,Bazel 基于库,二进制文件,脚本和数据集的概念进行操作,使您免于陷入将单个调用编写到编译器和链接器等工具的复杂性。

  • Bazel 高效可靠 Bazel 缓存以前完成的所有工作,并跟踪文件内容和构建命令的更改。通过这种方式,Bazel 知道何时需要重建某些东西,并仅重建那些东西。为了进一步加快构建速度,您可以将项目设置为以并行和增量的方式构建。

  • Bazel是跨平台的 Bazel 可以在 Linux,macOS 和 Windows 上运行。Bazel 可以为同一个项目中的多个平台(包括桌面,服务器和移动设备)构建二进制文件和可部署软件包。

  • Bazel扩展性强 Bazel 在使用100k+源文件处理构建时仍然保持良好的性能表现。它适用于多个制品存储库和10K用户规模。

  • Bazel是可扩展的 您可以扩展 Bazel 以支持您选择的语言。

Bazel 核心概念

Bazel 根据在称为工作空间(WORKSPACE)的目录中组织的源代码构建软件。工作空间中的源文件以包的嵌套层次结构进行组织,其中每个包都是包含一组相关源文件和一个 BUILD 文件的目录。BUILD 文件指定可以从源构建哪些软件输出。

工作空间

工作空间是文件系统上的目录,其中包含要构建的软件的源文件,以及指向包含构建输出的目录的符号链接。每个工作空间目录都有一个名为 WORKSPACE 的文本文件,该文件可能为空,或者可能包含对构建输出所需的外部依赖项的引用。

工作空间中代码组织的主要单元是包。包是相关文件的集合,以及它们之间的依赖关系的规范。包被定义为包含名为 BUILD 的文件的目录,该文件位于工作空间中的顶级目录下。包中包含其目录中的所有文件,以及其下的所有子目录,除了那些本身包含 BUILD 文件的子目录。

例如,在以下目录树中有两个包,my/app 和子包 my/app/tests。请注意,my/app/data 不是包,而是属于包 my/app 的目录。

目标

包是一个容器。包的元素称为目标。大多数目标是两种主要类型之一,即文件和规则。此外,还有另一种目标,包组,但它们的数量要少得多。

文件进一步分为两种。源文件通常由开发者的努力编写,并签入代码存储库。生成的文件(有时称为派生文件)不会签入,而是由构建工具根据特定规则从源文件生成。

第二种目标是规则。规则指定一组输入和一组输出文件之间的关系,包括从输入中导出输出的必要步骤。规则的输出始终是生成的文件。规则的输入可以是源文件,但也可以是生成的文件; 因此,一条规则的输出可能是另一条规则的输入,允许构建长链规则。

包组是一组包,其目的是限制某些规则的可访问性。包组由 package_group 函数定义。它们有两个属性:它们包含的包列表及其名称。唯一允许引用它们的方法来自规则的 visibility 属性或 package 函数的 default_visibility 属性; 他们不生成或使用文件。

总结

Bazel 默认支持多种开发语言,如 Java,C++,Javascript, Android 等等,您还可以根据需要扩展它。在很多开源项目中都有用到它,由于其支持并行构建,增量构建等特性,与传统构建工具区别很大,因此备受青睐。

2、distcc

distcc 是一款开源的分布式 C/C++ 编译器,是早期的分布式编译器,已经正确编译了许多软件和系统,例如 Linux Kernel、GNOME 等。distcc 通过向集群中的主机分配编译任务来加速编译[14],根据 Rosen Matev 的测试,使用 4 核虚拟机,本地编译与 80 核机器上的远程编译进行比较,在编译 Gaudi(LHCb 堆栈的基础) 时至少实现了 10 倍时间加速,其中瓶颈已经变成了非分布式工作比如链接。

三、Gradle和Maven增量构建技术


1、Gradle增量构建实现

gradle为了提升构建的效率,提出了增量构建的概念,为了实现增量构建,gradle将每一个task都分成了三部分,分别是input输入,任务本身和output输出。下图是一个典型的java编译的task。

 如果我们将Gradle的Task看作一个黑盒子,那么我们便可以抽象出输入和输出的概念,一个Task对输入进行操作,然后产生输出。比如,在使用java插件编译源代码时,输入即为Java源文件,输出则为class文件。如果多次执行一个Task时的输入和输出是一样的,那么我们便可以认为这样的Task是没有必要重复执行的。此时,反复执行相同的Task是冗余的,并且是耗时的。

  为了解决这样的问题,Gradle引入了增量式构建的概念。在增量式构建中,我们为每个Task定义输入(inputs)和输入(outputs),如果在执行一个Task时,如果它的输入和输出与前一次执行时没有发生变化,那么Gradle便会认为该Task是最新的(UP-TO-DATE),因此Gradle将不予执行。一个Task的inputs和outputs可以是一个或多个文件,可以是文件夹,还可以是Project的某个Property,甚至可以是某个闭包所定义的条件。

  每个Task都拥有inputs和outputs属性,他们的类型分别为TaskInputs和TaskOutputs。在下面的例子中,我们展示了这么一种场景:名为combineFileContent的Task从sourceDir目录中读取所有的文件,然后将每个文件的内容合并到destination.txt文件中。让我们先来看看没有定义Task输入和输出的情况:

task combineFileContentNonIncremental { def sources = fileTree('sourceDir') def destination = file('destination.txt') doLast { destination.withPrintWriter { writer -> sources.each {source -> writer.println source.text } } }}

  多次执行“gradle combineFileContentNonIncremental”时,整个Task都会反复执行,即便在第一次执行后我们已经得到了所需的结果。如果该combineFileContentNonIncremental是一个繁重的Task,那么多次重复执行势必造成没必要的时间耗费。

  这时,我们可以将sources声明为该Task的inputs,而将destination声明为outputs,重新创建一个Task如下:

task combineFileContentIncremental { def sources = fileTree('sourceDir') def destination = file('destination.txt') inputs.dir sources outputs.file destination doLast { destination.withPrintWriter { writer -> sources.each {source -> writer.println source.text } } }}

  相比之下,后一个Task只比前一个Task多了两行代码:

inputs.dir sourcesoutputs.file destination

  当首次执行combineFileContentIncremental时,Gradle会完整地执行该Task。但是紧接着再执行一次,命令行显示:

:combineFileContentIncremental UP-TO-DATEBUILD SUCCESSFULTotal time: 2.104 secs

  我们发现,combineFileContentIncremental被标记为UP-TO-DATE,表示该Task是最新的,Gradle将不予执行。在实际应用中,你将遇到很多这样的情况,因为Gradle的很多插件都引入了增量式构建机制。

  如果我们修改了inputs(即sourceDir文件夹)中的任何一个文件或删除掉了destination.txt,当调用“gradle combineFileContentIncremental”时,Gradle又会重新执行,因为此时的Task已经不再是最新的了。对于outputs,我们还可以使用upToDateWhen()方法来决定一个Task的outputs是否为最新的,该方法接受一个闭包作为检查条件,感兴趣的读者可以自行了解。

2、Gradle和Maven性能对比及技术选型

Gradle和Maven都可以作为Java应用程序的构建工具。

在日常工作中,我们通常构建本地项目或远程构建小型项目的时候,由于现在机器配置本身比较高,可能感知不到二者明显的性能差异。

但如果是大中型项目,这种构建效率就能更有体会了。一次构建所花费的时间可能会很长。那在这种情况下,尤其在自动化构建流程中,当然希望构建速度越快越好。特别是企业的一些核心业务系统,比较重视上线时效,可以说效率就是金钱。

我们知道,虽然Gradle和Maven都支持并行构建项目和并行依赖解析。但从Gradle官网给出的5种压测场景的数据来看,Gradle的处理性能确实比Maven快。(实际也确实如此)

2.1 性能对比

1)在所有场景下,Gradle至少比Maven快2倍。

2)当增量构建时,Gradle比Maven快7-85倍,子项目越多,Gradle快的越多。

3)当Gradle的构建缓存可以解析任务输出的时候,Gradle比Maven快3-30倍。

2.2 Gradle为什么这么快

1)Gradle实现了大量策略来保证构建速度更快。

包含:增量构建机制、构建缓存机制、守护进程机制。

2)Gradle守护线程可以保证构建信息足够新。

Gradle会开启一个守护进程来和各个build任务进行交互,优点就是不需要每次构建都初始化需要的组件和服务。同时因为守护进程是一个一直运行的进程,除了可以避免每次JVM启动的开销之外,还可以缓存项目结构,文件,task和其他的信息,从而提升运行速度。

3)针对各种类型任务的增量任务输入和输出确保不需要每次运行清理命令。

4)增量编译可以分析源文件和类文件之间的依赖关系,并只重新编译改变的部分;

5)当二进制接口没有改变的时候,Gradle的智能类路径分析器避免了不必要的编译;

6)利用Java类插件来提供更好的建模,减少了编译时类路径的体积,提高了性能。

7)Gradle为了提升构建速度,引入了增量构建机制。gradle把每一个任务分为三部分:输入、任务本身和输出,便于事变变更内容。另外也支持跨机器共享构建缓存,这对于云原生CI中自动构建过程来说体验是想当好了。

2.3 技术选型

整体来看,Gradle相比Maven更加灵活。

但Maven的项目相对容易看懂,而且上手会简单一些。虽然,Maven在灵活性和自定义上不是很人性化外,也有它自身的特点,存在即合理。Maven项目相对Gradle而言可读性比较好,另外入门比较容易且支持的插件也比较多。所以,如果项目当中没有自定义需求,则可以选用Maven作为构建工具。

3、Maven增量构建实现

如果要开始任何新的基于Java的项目,则gradle应该是第一选择,但是某些场景或者某些方面,Maven依然有着不错的优势。在编译构建项目时,就会需要一些插件来提供不同的功能支持。

Maven Java编译器插件对增量编译提供了不错的支持,但它无法处理一些极端情况,例如:

  • 源文件夹中文件更改时触发编译。

  • 不更改代码时跳过单元测试。

在大多数情况下,为了处理已删除文件的情况,必须运行mvn clean install,这意味着将编译完整代码并执行单元测试。

偶然发现有一个插件可以解决这个两个问题:

  • 更改代码后触发对应的文件编译并触发完整版本构建。

  • 在不更改代码的情况下跳过单元测试执行。

这两个功能都可以帮助大大减少编译时间,因为在大多数情况下,只有很少的模块被更改并且可以使用以前的生成输出。您可以通过启用此插件来快速构建。

使用maven-build-cache-extension实现Maven增量快速构建Java项目

随着 Maven 3.9.0的发布,现在可以利用maven-build-cache-extensionMaven 项目中的增量构建来获益。

此功能可以缩短构建时间(在您的本地工作流程和 CI 中)。它缓存模块构建,避免不必要和/或昂贵的任务:

  • 可以通过.mvn/extensions.xml

  • 或通过修改您的pom.xml增加:

这个解决方案提供了更多的灵活性,因为你可以通过存储在.mvn文件夹下的maven-build-cache-config.xml文件来配置扩展。如果没有找到配置文件,将使用合理的默认值。

如果您使用pom.xml方法,您可以将该插件添加到您的项目->build->extensions。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
从DevOps到AIOps(四):编译工具
Eclipse 安装Gradle插件
Gradle
Gradle学习系列之一
Java程序员必备的开发工具
Jenkins常用构建工具
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服