背景
庖丁同学在混迹在古典互联网界多年,构建 Java 应用程序一直都是使用Apache Maven,mvn的那几个常用命令早已烂熟于心,在加上现代 IDE 越来越智能,应用程序的构建从来就不是事儿,但是惊喜总会出现,有一天,庖丁的工作性质发生了改变,眼前的构建工具清一色的都是 Gradle,庖丁一脸的懵逼,束手无策,难道Maven 程序员就不能快速上手 Gradle了吗?
实战性或者战术性程序员这时候做的第一件是事情,肯定是先打开 Gradle 的官方网站,快速学习一遍入门教程,先用起来,再去慢慢理解它的运行机制和设计思想,但是庖丁是解牛的,必须“依乎天理”。深刻理解事物的本质,方能游刃有余。
首先回归问题的本质,无论是 Apache Maven 还是 Gradle ,最核心的功能都是应用程序工程的构建工具,只是实现理念和使用方式上有差异。下面庖丁首先回顾下 Apache Maven 的使用方式和工作机制。
Apache Maven 的工作机制
Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project’s build, reporting and documentation from a central piece of information.
上面这段话来自于 Apache Maven 的官网,Apache Maven是一个软件项目管理和理解工具。 基于项目对象模型(POM)的概念,Maven可以通过集中的信息来管理项目的构建,报告和文档。POM 是最基础也是最核心的组件,这也是 Maven 的配置文件命名为 pom.xml的主要原因。POM是通过 XML 文件的格式进行定义。pom.xml 文档的示例片段如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>Maven Quick Start Archetype</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
使用 Maven 管理项目构建时,通常第一步去官网下载Apache Maven 的安装包,配置好环境变量,就可以执行 mvn 命令了,大多数公司会通过 nexus搭建私服,对项目的依赖进行规划化管理。
我们在使用 Maven 管理项目的构建时,IDE 工具或者脚手架工具都会帮我们生成 pom.xml 文件和基本的项目骨架,然后根据项目的需要进行依赖管理,各种各样的插件是必不可少的。一切准备就绪后,我们就可以对项目进行构建了,例如编译,测试,部署等工作。所以Apache Maven 主要帮我们做了两件大事,一个是依赖版本管理,另一个就是各种构建过程。先看看Apache Maven 的构建过程。
Maven的构建过程
下面是Maven读取POM文件执行构建过程的示意图
Maven 的构建过程
总体来说,Maven项目的构建分为五个主要步骤,
- 读取 POM.xml 文件
- 选择需要构建的 profile,这个主要方便多构建目标操作,例如单元测试和集成测试,依赖项不一样
- 下载依赖项目,一般是 Jar 包文件,首先回去读本地仓库,然后选择读取内部私服,最后是远程公共仓库
- 根据构建任务的生命周期,执行构建过程和目标
- 执行插件,部分插件会融合在构建阶段中,一起执行。
例如对于编译这个构建任务,执行如下命令:
mvn compile
实际上,执行是编译 compile 这个构建阶段,真正执行的是如下阶段
- validate
- generate-sources
- process-sources
- generate-resources
- process-resources
- compile
maven 对构建过程的抽象
项目的研发有生命周期,项目的构建过程也一样,是多个独立的任务,Maven 对构建构建过程的抽象中,任务是个虚拟的概念, 因为Maven基于构建生命周期的中心概念。这意味着已明确定义了构建和分发特定工件(项目)的过程。对于构建项目的人来说,这意味着仅需学习少量命令即可构建任何Maven项目,并且POM将确保它们获得所需的结果。这是一种开箱即用的设计,也是一种最佳实践。
POM 文件
执行Maven命令时,Maven根据POM文件中的配置来执行命令。
依赖项和存储库
Pom文件包含了依赖项的配置,依赖项是项目使用的外部JAR文件(Java库)。如果本地库中没有找到依赖项,Maven将从中央库下载依赖项,并存放在本地库中。本地存储库只是本机上的一个目录,这个目录位置可配置。另外除了中央库,还可以配置其他远程库,例如公司内部可以架设一个远程库供所有开发人员使用。这里我们看看 Spring Boot 的依赖项,通过三个维度对依赖包进行管理。
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
构建生命周期、阶段和目标
项目的构建通常包含数个相互独立的任务,可以独立执行,如生成文档,构建jar包等。单个任务的执行过程被称为一个构建生命周期,构建生命周期由一系列的构建阶段组成,这些构建生命周期中的每一个都由不同的构建阶段列表定义,其中,构建阶段代表生命周期中的一个阶段。每个阶段包含一系列的构建目标。
可以执行构建阶段或构建目标。阶段按顺序执行,执行一个阶段则会先执行该阶段之前的所有阶段。当执行构建阶段时,将会按顺序执行其中包含的所有构建目标。构建目标可以被分配到一个或多个构建阶段。还可以直接执行构建目标。
Maven有三个内置的构建生命周期:默认(default),清除(clean)和站点(site)。默认生命周期处理项目部署,清理(clean)生命周期处理项目清理,而站点(site)生命周期处理项目的站点文档的创建。
例如,默认生命周期包含以下阶段:
- 验证(validate)-验证项目是否正确并且所有必要信息均可用
- 编译(compile)-编译项目的源代码
- 测试(test)-使用合适的单元测试框架测试已编译的源代码。这些测试不应要求将代码打包或部署
- 打包(package)-打包已编译的代码,并将其打包为可分发的格式,例如JAR。
- 集成测试(integration-test):处理程序包并将其部署到可以运行集成测试的环境中
- 验证(verify):运行任何检查以验证包装是否有效并符合质量标准
- 安装(install):将软件包安装到本地存储库中,以作为本地其他项目中的依赖项
- 部署(deploy):在集成或发布环境中完成,将最终程序包复制到远程存储库,以便与其他开发人员和项目共享。
插件Plugin
插件是构建目标的集合,也称为MOJO (Maven Old Java Object)。可以把插件理解为一个类,而构建目标是类中的方法。构建阶段包含一系列的构建目标,可以理解为按顺序调用各个插件中的构建目标(方法),然后一系列的构建阶段组成一个构建生命周期。
Maven实际上是一个插件执行框架。如有必要,可以用java开发自定义插件。例如我们经常使用的Running MyBatis Generator With Maven
构建Profile
如果需要构建项目的不同版本,可以使用构建profile。例如,项目中需要构建开发版本、测试版本以及正式版本,这些版本可以通过在pom文件中添加不同构建profile构建。执行maven时指定不同的构建profile就可以。
小结
Apache Maven是一个软件项目管理和理解工具。 基于项目对象模型(POM)的概念,通过 XML 进行配置,构建过程基于任务的的生命周期概念。完成了依赖版本管理和项目构建过程。
对 Gradle 工具的猜想
既然 Maven 已经使用了挺好了,为什么会出现新的构建工具之一 Gradle 呢?显然 Maven 有不足之处或者痛点,想想我们日常工作中的痛点。
- pom.xml文件非常冗长,例如 Apache Flink 的 pom(https://github.com/apache/flink/blob/master/pom.xml) 文件大概有两千行,这和 XML 的表达方式有关。
- 构建过程非常慢,特别是大型项目。
- 扩展性限制较大,无法和你的编程语言或者脚本打通
如果需要一个新的构建工具,你期望它最好具有哪些特征呢?
- 兼容 Maven 的依赖管理机制,毕竟你已经有一些项目通过 Maven 管理,当然还有一些非常优秀的第三方 Jar 包,这些对你的项目运行至关重要
- 编译和构建速度快
- 构建文件的配置简单明了,减少冗余,特别是那些约定俗称的概念
- 扩张性好,最好能和你使用的 Java 语言打通
万变不离其宗,构建工具的本质工作为依赖管理和项目构建,那么 Gradle 会不会给你惊喜呢?
Gradle 初见
惊讶地发现,熟悉的 pom.xml 文件不见了,取而代之的是 build.gradle文件,打开它发现 XML 格式的内容也不见了,莫慌,肯定是某种编程语言,先瞄一眼,build.gradle 脚本示例。
plugins {
id 'java-library'
}
?
repositories {
mavenCentral()
jcenter()
}
dependencies {
api 'org.apache.commons:commons-math3:3.6.1'
implementation 'com.google.guava:guava:28.0-jre'
testImplementation 'junit:junit:4.13'
}
挺简洁,junit 的依赖一行就阐述清楚了
testCompile group: 'junit', name: 'junit', version: '4.13'
如果使用 Maven 的依赖,配置片段应该是如下这样,6 行配置代码,解决了配置文件冗长的 繁琐,并且提高了可读性。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
mavenCentral(),具备Jar 包存储仓库管理和配置,兼容 Maven 仓库,简洁的依赖管理和可配置的插件。已经具备了 Maven的基础功能。Gradle能不能解决 Maven 构建工具的痛点呢?它和Maven 对于项目构建过程的抽象和封装会一致吗?
Gradle 的工作机制
Gradle 简介
Gradle是一个开放源代码的构建自动化工具,旨在灵活地构建几乎任何类型的软件。下图是 Gradle 官网的截图,可以看出 Gradle 的雄心,加快研发人员的生产力 。
Gradle是专注于灵活性和性能的开源构建自动化工具。 Gradle构建脚本是使用[Groovy](https://groovy-lang.org/)或[Kotlin](https://kotlinlang.org/)DSL编写的,主要有如下特质:
- 高度可定制-Gradle以最基本的方式可定制和可扩展的方式建模。
- 快速-Gradle通过重新使用先前执行的输出,仅处理已更改的输入并并行执行任务来快速完成任务。**
- 功能强大–Gradle是Android的官方构建工具,并支持许多流行的语言和技术。
Gradle 重要核心功能
高性能
Gradle通过仅运行需要执行的任务来避免不必要的工作,因为它们的输入或输出已更改。您还可以使用构建缓存来重用以前运行的任务输出,甚至可以使用其他计算机(具有共享的构建缓存)重用任务输出。
Gradle还实施了许多其他优化措施,并且开发团队不断努力以提高Gradle的性能。
JVM基础
Gradle在JVM上运行,并且必须安装Java开发工具包(JDK)才能使用它。对于熟悉Java平台的用户来说,这是一个好处,因为您可以在构建逻辑中使用标准Java API,例如自定义任务类型和插件。它还使在不同平台上运行Gradle变得容易。
请注意,Gradle不仅限于构建JVM项目,它甚至附带对构建本机项目的支持。
约定(Conventions)
Gradle从Maven的书中抽出了一片叶子,并通过实现约定使常见类型的项目(例如Java项目)易于构建。应用适当的插件,您可以轻松地为许多项目使用苗条的构建脚本。但是这些约定并没有限制您:Gradle允许您覆盖它们,添加自己的任务以及对基于约定的版本进行许多其他自定义。
可扩展性
您可以轻松扩展Gradle以提供您自己的任务类型甚至构建模型。有关此示例,请参见Android构建支持:它添加了许多新的构建概念,例如口味和构建类型。
IDE支持
几个主要的IDE允许您导入Gradle构建并与其进行交互:Android Studio,IntelliJ IDEA,Eclipse和NetBeans。 Gradle还支持生成将项目加载到Visual Studio所需的解决方案文件。
洞察力
构建扫描提供了有关构建运行的广泛信息,可用于识别构建问题。他们特别擅长帮助您发现构建性能方面的问题。您还可以与其他人共享构建扫描,如果您需要咨询以解决构建问题,这特别有用。
主要差异和对比
Gradle和Maven之间的主要区别是灵活性,性能,用户体验和依赖性管理。 Maven与Gradle功能比较中提供了这些方面的直观概述。Gradle 官网中,对 Gradle 和 Maven 从以下几个方面做了比较详细的对比:
灵活性
Google选择Gradle作为Android的官方构建工具; 不是因为构建脚本是代码,而是因为Gradle以最基本的方式可扩展的方式进行建模。 Gradle的模型还允许将其用于C / C ++的本机开发,并且可以扩展为涵盖任何生态系统。 例如,Gradle在设计时会考虑使用其Tooling API进行嵌入。
Gradle和Maven都提供了配置约定。 但是,Maven提供了一个非常僵化的模型,使定制变得乏味,有时甚至是不可能的。 尽管这可以使您更容易理解任何给定的Maven构建,但是只要您没有任何特殊要求,它也就不适合许多自动化问题。 另一方面,Gradle的构建考虑了授权和负责任的用户。
性能
缩短构建时间是提高交付速度的最直接方法之一。 Gradle和Maven都采用某种形式的并行项目构建和并行依赖项解析。 最大的差异是Gradle避免重复工作和增加工作量的机制。 使Gradle比Maven快得多的前3个功能是:
- 增量性-Gradle通过跟踪任务的输入和输出并仅运行必要的内容,并且仅在可能时处理已更改的文件,从而避免了工作。
- Build Cache(构建缓存)—重用具有相同输入的任何其他Gradle构建的构建输出,包括在机器之间
- Gradle Daemon —一个长期存在的过程,可将构建信息“热”存储在内存中。
在Gradle与Maven的性能对比中,这些和更多的性能特性使Gradle在几乎每种情况下的速度至少是其两倍(对于使用构建缓存的大型构建而言,则要快100倍)。下图来自 Gradle 官网 。
用户体验
Maven的任期较长,这意味着它对许多用户来说都更支持IDE。 但是,Gradle的IDE支持继续迅速提高。 例如,Gradle现在具有基于Kotlin的DSL,可提供更好的IDE体验。 Gradle团队正在与IDE开发商合作,以使编辑支持变得更好-随时关注更新。
尽管IDE很重要,但是许多用户还是喜欢通过命令行界面执行构建操作。 Gradle提供了一个现代的CLI,该CLI具有可发现性功能,例如“ gradle task”(渐变任务),以及改进的日志记录和命令行完成功能。
最后,Gradle提供了一个基于Web的交互式UI,用于调试和优化构建:构建扫描。 这些也可以在内部托管,以允许组织收集构建历史记录并进行趋势分析,比较构建以进行调试或优化构建时间。
依赖管理
两种构建系统都提供了内置功能,可以解决来自可配置存储库的依赖关系。两者都可以在本地缓存依赖项并并行下载它们。
作为库的使用者,Maven允许重写一个依赖项,但只能按版本。 Gradle提供了可自定义的依赖关系选择和替换规则,这些规则可以声明一次,并在项目范围内处理不需要的依赖关系。这种替换机制使Gradle可以一起构建多个源项目以创建复合构建。
Maven具有很少的内置依赖项作用域,这些作用域在常见的情况下(例如使用测试夹具或代码生成)迫使笨拙的模块体系结构。例如,单元测试和集成测试之间没有分隔。 Gradle允许自定义依赖项范围,从而提供了更好的建模和更快的构建。
Maven依赖关系冲突解决方案使用最短路径,这受声明顺序影响。 Gradle可以完全解决冲突,选择图中最高版本的依赖项。此外,使用Gradle可以将版本严格声明为允许其优先于传递版本的版本,从而可以降级依赖项。
作为库生产者,Gradle允许生产者声明“ api”和“实现”依赖项,以防止有害的库泄漏到使用者的类路径中。 Maven允许发布者通过可选的依赖项提供元数据,但仅作为文档提供。 Gradle完全支持功能变体和可选的依赖项。
以上从四个方面对 Gradle 和 Maven 进行了对比,差距确实明显,但阐述都比较表象,没有深刻阐述本质的区别。Gradle和Maven对如何构建项目有根本不同的视角。 Gradle提供了一个灵活且可扩展的构建模型,该模型将实际工作委托给任务依赖关系图。 Maven使用固定,线性阶段的模型,您可以在其中附加目标(完成工作的事物)。 这可能使两者之间的迁移看起来令人生畏,但是迁移可能出奇的容易,因为Gradle遵循许多与Maven相同的约定(例如标准项目结构),并且其依赖项管理以类似的方式工作。
所以任务依赖关系图和线性阶段模型才是本质上的差别,也是研发和使用人员认知上的本质差别。
Maven 到 Gradle,认知的转变
Gradle是一种灵活而强大的构建工具,当Maven 程序员初次启动时,很容易感到恐惧。但是,了解以下核心原则将使Gradle更加容易上手,并且您将在不知道该工具的情况下熟练掌握该工具。
Gradle是通用的构建工具
Gradle允许您构建任何软件,因为它对您要构建的内容或应如何完成的工作几乎没有任何假设。最明显的限制是,依赖项管理当前仅支持与Maven和Ivy兼容的存储库以及文件系统。
这并不意味着您需要做很多工作来创建构建。 Gradle通过添加一层约定和通过插件的预构建功能,可以轻松构建通用类型的项目(例如Java库)。您甚至可以创建和发布自定义插件来封装自己的约定并构建功能。
核心模型基于任务
Gradle将其构建模型建模为任务(工作单元)的有向无环图(DAG)。这意味着构建实质上配置了一组任务,并根据它们的依赖关系将它们连接在一起以创建该DAG。创建任务图后,Gradle将确定需要按顺序运行的任务,然后继续执行它们。
此图显示了两个示例任务图,一个是抽象图,另一个是具体图,其中任务之间的依赖性表示为箭头:
这样,几乎所有构建过程都可以建模为任务图,这就是Gradle如此灵活的原因之一。任务图可以由插件和您自己的构建脚本定义,任务通过任务依赖机制链接在一起。
任务本身包括:
- 动作-做某事的工作,例如复制文件或编译源代码
- 输入-操作使用或对其进行操作的值,文件和目录
- 输出-操作修改或生成的文件和目录
实际上,以上所有操作都是可选的,具体取决于任务需要执行的操作。某些任务(例如标准生命周期任务)甚至没有任何动作。他们只是为了方便而将多个任务聚合在一起。
您选择要运行的任务。通过指定执行所需任务的任务来节省时间,但仅此而已。如果您只想运行单元测试,请选择执行该任务的任务-通常是测试。如果要打包应用程序,则大多数构建都为此执行组装任务。最后一件事:Gradle的增量构建支持是可靠且可靠的,因此,除非您确实想执行清理,否则避免清理任务可保持构建快速运行。
Gradle有几个固定的构建阶段
重要的是要了解Gradle分三个阶段评估和执行构建脚本:
- 初始化:设置构建环境,并确定哪些项目将参与其中。
- 配置:构造和配置用于构建的任务图,然后根据用户要运行的任务确定需要运行的任务和运行顺序。
- 执行: 运行在配置阶段结束时选择的任务。
这些阶段构成了Gradle的构建生命周期。
与Apache Maven术语的比较Gradle的构建阶段与Maven的阶段不同。 Maven使用其阶段将构建执行分为多个阶段。它们的作用类似于Gradle的任务图,但灵活性较差。
Maven的构建生命周期概念与Gradle的生命周期任务大致相似。
设计良好的构建脚本主要由声明性配置而不是命令式逻辑组成。可以理解,在配置阶段评估该配置。即便如此,许多此类构建也具有任务操作(例如,通过doLast {}和doFirst {}块),这些任务在执行阶段进行评估。这很重要,因为在配置阶段评估的代码不会看到在执行阶段发生的更改。
配置阶段的另一个重要方面是,每次运行构建时都要评估其中涉及的所有内容。因此,最佳做法是在配置阶段避免昂贵的工作。构建扫描可以帮助您识别此类热点。
Gradle的扩展方式不止一种
如果您仅可以使用Gradle捆绑的构建逻辑来构建项目,那将是很好的选择,但这几乎是不可能的。大多数构建都有一些特殊要求,这意味着您需要添加自定义构建逻辑。
Gradle提供了多种机制来扩展它,例如:
- 自定义任务类型。
当您希望构建完成现有任务无法完成的工作时,只需编写自己的任务类型即可。通常,最好将自定义任务类型的源文件放在buildSrc目录或打包的插件中。然后,您可以像Gradle提供的任何任务一样使用自定义任务类型。
- 自定义任务动作。
您可以通过Task.doFirst()和Task.doLast()方法附加在任务之前或之后执行的自定义构建逻辑。
- 项目和任务的额外属性。
这些允许您将自己的属性添加到项目或任务中,然后可以从您自己的自定义操作或任何其他构建逻辑中使用它们。甚至可以将额外的属性应用于您未明确创建的任务,例如Gradle的核心插件创建的任务。
- 自定义约定。
约定是简化构建的强大方法,因此用户可以更轻松地理解和使用它们。从使用标准项目结构和命名约定的构建(例如Java构建)中可以看出这一点。您可以编写自己的提供约定的插件-它们只需要为构建的相关方面配置默认值。
- 自定义模型。
Gradle允许您将新概念引入除任务,文件和依赖项配置之外的内部版本。您可以在大多数语言插件中看到这一点,这些插件将源集的概念添加到了构建中。对构建过程进行适当的建模可以大大提高构建的易用性及其效率。
构建脚本针对API运行
将Gradle的构建脚本视为可执行代码很容易,因为它们就是这样。但这只是一个实现细节:精心设计的构建脚本描述了构建软件所需的步骤,而不是这些步骤应如何工作。这是定制任务类型和插件的工作。
人们普遍误认为Gradle的功能和灵活性来自其构建脚本是代码这一事实。这离事实还远。强大的基础模型和API。正如我们在最佳实践中建议的那样,您应该避免在构建脚本中放置太多(如果有的话)命令式逻辑。
然而,在一个区域中,将构建脚本视为可执行代码很有用:了解构建脚本的语法如何映射到Gradle的API。由Groovy DSL参考和Javadocs组成的API文档列出了方法和属性,并引用了闭包和操作。这些在构建脚本的上下文中是什么意思?查看Groovy Build Script Primer,以了解该问题的答案,以便您可以有效地使用API文档。
由于Gradle在JVM上运行,因此构建脚本也可以使用标准Java API。 Groovy构建脚本可以另外使用Groovy API,而Kotlin构建脚本可以使用Kotlin。
总结
通过对 Maven 和Gradle 工作机制的介绍,Maven 程序员对于如何使用 Gradle 有个认知上的提升,Gradle和Maven对如何构建项目有根本不同的视角。 Gradle提供了一个灵活且可扩展的构建模型,该模型将实际工作委托给任务依赖关系图。 Maven使用固定,线性阶段的模型,您可以在其中附加目标(完成工作的事物)。从构架项目这个领域模型来看,有个本质的差别,从而引起架构设计的差异,因此 Maven 程序员必须深刻理解这个差异,提升认知,就可以快速上手。而依赖管理,两个各有千秋,但差异不大。
参考资料
- Apache Maven 官网:http://maven.apache.org/
- https://stackoverflow.com/questions/7249871/what-is-a-build-tool
- https://gradle.org/