Google 选择 Gradle 作为 Android 的构建工具不是没有理由的,其中一个非常重要的原因就是因为 Gradle 够灵活。
一方面是因为 Gradle 使用的是 groovy 或者 kotlin 语言作为脚本的编写语言,这样极大的提高了脚本的灵活性,但是其本质上的原因是 Gradle 的基础架构能够支持这种灵活性。
你可以使用 Gradle 来构建 Native 的 C/C++ 程序,甚至扩展到任何语言的构建。
相对而言,Maven 的灵活性就差一些,并且自定义起来也比较麻烦,但是 Maven 的项目比较容易看懂,并且上手简单。
所以如果你的项目没有太多自定义构建需求的话还是推荐使用 Maven,但是如果有自定义的构建需求,那么还是投入 Gradle 的怀抱吧。
虽然现在大家的机子性能都比较强劲,好像在做项目构建的时候性能的优势并不是那么的迫切,但是对于大型项目来说,一次构建可能会需要很长的时间,尤其对于自动化构建和 CI 的环境来说,当然希望这个构建是越快越好。
Gradle 和 Maven 都支持并行的项目构建和依赖解析,但是 Gradle 的三个特点让 Gradle 可以跑的比 Maven 快上一点。
以上图为例,input 就是目标 jdk 的版本,源代码等,output 就是编译出来的 class 文件。
增量构建的原理就是监控 input 的变化,只有 input 发送变化了,才重新执行 task 任务,否则 gradle 认为可以重用之前的执行结果。
所以在编写 gradle 的 task 的时候,需要指定 task 的输入和输出。
并且要注意只有会对输出结果产生变化的才能被称为输入,如果你定义了对初始结果完全无关的变量作为输入,则这些变量的变化会导致 gradle 重新执行 task,导致了不必要的性能的损耗。
还要注意不确定执行结果的任务,比如说同样的输入可能会得到不同的输出结果,那么这样的任务将不能够被配置为增量构建任务。
Gradle 可以重用同样 input 的输出作为缓存,大家可能会有疑问了,这个缓存和增量编译不是一个意思吗?
在同一个机子上是的,但是缓存可以跨机器共享.如果你是在一个 CI 服务的话,build cache 将会非常有用。因为 developer 的 build 可以直接从 CI 服务器上面拉取构建结果,非常的方便。
Gradle 会开启一个守护进程来和各个 build 任务进行交互,优点就是不需要每次构建都初始化需要的组件和服务。
同时因为守护进程是一个一直运行的进程,除了可以避免每次 JVM 启动的开销之外,还可以缓存项目结构,文件,task 和其他的信息,从而提升运行速度。
我们可以运行 gradle --status 来查看正在运行的 daemons 进程。
从 Gradle 3.0 之后,daemons 是默认开启的,你可以使用 org.gradle.daemon=false 来禁止daemons。
我们可以通过下面的几个图来直观的感受一下 gradle 和 maven 的性能比较:
使用 gradle 和 maven 构建 Apache Commons Lang 3 的比较:
gralde 和 maven 都可以本地缓存依赖文件,并且都支持依赖文件的并行下载。
在 maven 中只可以通过版本号来覆盖一个依赖项。而 gradle 更加灵活,你可以自定义依赖关系和替换规则,通过这些替换规则,gradle 可以构建非常复杂的项目。
因为 maven 出现的时间比较早,所以基本上所有的 java 项目都支持 maven,但是并不是所有的项目都支持 gradle。
如果你有需要把 maven 项目迁移到 gradle 的想法,那么就一起来看看吧。
根据我们之前的介绍,大家可以发现 gradle 和 maven 从本质上来说就是不同的,gradle 通过 task 的 DAG 图来组织任务,而 maven 则是通过 attach 到 phases 的 goals 来执行任务。
虽然两者的构建有很大的不同,但是得益于 gradle 和 maven 相识的各种约定规则,从 maven 移植到 gradle 并不是那么难。
要想从 maven 移植到 gradle,首先要了解下 maven 的 build 生命周期,maven的生命周期包含了 clean,compile,test,package,verify,install 和 deploy 这几个 phase。
我们需要将 maven 的生命周期 phase 转换为 gradle 的生命周期 task。这里需要使用到 gradle 的 Base Plugin,Java Plugin 和 Maven Publish Plugin。
plugins {
id 'base'
id 'java'
id 'maven-publish'
}
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
dependencies {
implementation 'log4j:log4j:1.2.12'
}
可以看到 gradle 比 maven 写起来要简单很多。
注意这里的 implementation 实际上是由 Java Plugin 来实现的。
我们在 maven 的依赖中有时候还会用到 scope 选项,用来表示依赖的范围
我们看下这些范围该如何进行转换:
compile:
在 gradle 可以有两种配置来替换 compile,我们可以使用 implementation 或者 api。
前者在任何使用 Java Plugin 的 gradle 中都可以使用,而 api 只能在使用 Java Library Plugin 的项目中使用。
当然两者是有区别的,如果你是构建应用程序或者 webapp,那么推荐使用 implementation,如果你是在构建 Java libraries,那么推荐使用 api。
runtime:
test:
provided:
import:
在 gradle 中,可以使用 platform() 或者 enforcedPlatform() 来导入 pom 文件:
dependencies {
implementation platform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')
implementation 'com.google.code.gson:gson'
implementation 'dom4j:dom4j'
}
比如上面的例子中,我们导入了 spring-boot-dependencies。因为这个 pom 中已经定义了依赖项的版本号,所以我们在后面引入 gson 的时候就不需要指定版本号了。
platform 和 enforcedPlatform 的区别在于,enforcedPlatform 会将导入的 pom 版本号覆盖其他导入的版本号:
dependencies {
// import a BOM. The versions used in this file will override any other version found in the graph
implementation enforcedPlatform('org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE')
// define dependencies without versions
implementation 'com.google.code.gson:gson'
implementation 'dom4j:dom4j'
// this version will be overridden by the one found in the BOM
implementation 'org.codehaus.groovy:groovy:1.8.6'
}
repositories {
mavenCentral()
}
repositories {
maven {
url "http://repo.mycompany.com/maven2"
}
}
repositories {
mavenLocal()
}
mavenLocal 只是 maven 在本地的一个 cache,它包含的内容并不完整。比如说一个本地的 maven repository module 可能只包含了 jar 包文件,并没有包含 source 或者 javadoc 文件。那么我们将不能够在 gradle 中查看这个 module 的源代码,因为 gradle会首先在 maven 本地的路径中查找这个 module。
并且本地的 repository 是不可信任的,因为里面的内容可以轻易被修改,并没有任何的验证机制。
如果同一个项目中对同一个模块有不同版本的两个依赖的话,默认情况下 Gradle 会在解析完 DAG 之后,选择版本最高的那个依赖包。
但是这样做并不一定就是正确的, 所以我们需要自定义依赖版本的功能。
首先就是上面我们提到的使用 platform() 和 enforcedPlatform() 来导入 BOM(packaging 类型是 POM 的)文件。
dependencies {
implementation 'org.apache.httpcomponents:httpclient'
constraints {
implementation('org.apache.httpcomponents:httpclient:4.5.3') {
because 'previous versions have a bug impacting this application'
}
implementation('commons-codec:commons-codec:1.11') {
because 'version 1.9 pulled from httpclient has bugs affecting this application'
}
}
}
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
implementation('commons-codec:commons-codec') {
version {
strictly '1.9'
}
}
}
我们可以在implementation中指定特定的version即可。
strictly 表示的是强制匹配特定的版本号,除了strictly 之外,还有 require,表示需要的版本号大于等于给定的版本号。
prefer,如果没有指定其他的版本号,那么就使用 prefer 这个。reject,拒绝使用这个版本。
除此之外,你还可以使用 Java Platform Plugin 来指定特定的 platform,从而限制版本号。
dependencies {
implementation('commons-beanutils:commons-beanutils:1.9.4') {
exclude group: 'commons-collections', module: 'commons-collections'
}
}
<modules>
<module>simple-weather</module>
<module>simple-webapp</module>
</modules>
rootProject.name = 'simple-multi-module'
include 'simple-weather', 'simple-webapp'
maven 中可以使用 profile 来区别不同的环境,在 gradle 中,我们可以定义好不同的 profile 文件,然后通过脚本来加载他们:
if (!hasProperty('buildProfile')) ext.buildProfile = 'default'
apply from: "profile-${buildProfile}.gradle"
task greeting {
doLast {
println message
}
}
ext.message = 'foobar'
ext.message = 'testing 1 2 3'
我们可以这样来运行:
> gradle greeting
foobar
> gradle -PbuildProfile=test greeting
testing 1 2 3
在 maven 中有一个 process-resources 阶段,可以执行 resources:resources 用来进行 resource 文件的拷贝操作。
在 Gradle 中的 Java plugin 的 processResources task 也可以做相同的事情。
task copyReport(type: Copy) {
from file("$buildDir/reports/my-report.pdf")
into file("$buildDir/toArchive")
}
task copyPdfReportsForArchiving(type: Copy) {
from "$buildDir/reports"
include "*.pdf"
into "$buildDir/toArchive"
}