深入了解 gradle 和 maven 的区别
# 介绍
gradle 和 maven 都可以用来构建 java 程序,甚至在某些情况下,两者还可以互相转换,那么他们两个的共同点和不同点是什么?我们如何在项目中选择使用哪种技术呢?一起来看看吧。
虽然 gradle 和 maven 都可以作为 java 程序的构建工具。但是两者还是有很大的不同之处的。我们可以从下面几个方面来进行分析。
# 可扩展性
Google 选择 gradle 作为 android 的构建工具不是没有理由的,其中一个非常重要的原因就是因为 gradle 够灵活。一方面是因为 gradle 使用的是 groovy 或者 kotlin 语言作为脚本的编写语言,这样极大的提高了脚本的灵活性,但是其本质上的原因是 gradle 的基础架构能够支持这种灵活性。
你可以使用 gradle 来构建 native 的 C/C++ 程序,甚至扩展到任何语言的构建。
相对而言,maven 的灵活性就差一些,并且自定义起来也比较麻烦,但是 maven 的项目比较容易看懂,并且上手简单。
所以如果你的项目没有太多自定义构建需求的话还是推荐使用 maven,但是如果有自定义的构建需求,那么还是投入 gradle 的怀抱吧。
# 性能比较
虽然现在大家的机子性能都比较强劲,好像在做项目构建的时候性能的优势并不是那么的迫切,但是对于大型项目来说,一次构建可能会需要很长的时间,尤其对于自动化构建和 CI 的环境来说,当然希望这个构建是越快越好。
Gradle 和 Maven 都支持并行的项目构建和依赖解析。但是 gradle 的三个特点让 gradle 可以跑的比 maven 快上一点:
# 增量构建
gradle 为了提升构建的效率,提出了增量构建的概念,为了实现增量构建,gradle 将每一个 task 都分成了三部分,分别是 input 输入,任务本身和 output 输出。下图是一个典型的 java 编译的 task。
以上图为例,input 就是目标 jdk 的版本,源代码等,output 就是编译出来的 class 文件。
增量构建的原理就是监控 input 的变化,只有 input 发送变化了,才重新执行 task 任务,否则 gradle 认为可以重用之前的执行结果。
所以在编写 gradle 的 task 的时候,需要指定 task 的输入和输出。
并且要注意只有会对输出结果产生变化的才能被称为输入,如果你定义了对初始结果完全无关的变量作为输入,则这些变量的变化会导致 gradle 重新执行 task,导致了不必要的性能的损耗。
还要注意不确定执行结果的任务,比如说同样的输入可能会得到不同的输出结果,那么这样的任务将不能够被配置为增量构建任务。
# 构建缓存
gradle 可以重用同样 input 的输出作为缓存,大家可能会有疑问了,这个缓存和增量编译不是一个意思吗?
在同一个机子上是的,但是缓存可以跨机器共享. 如果你是在一个 CI 服务的话,build cache 将会非常有用。因为 developer 的 build 可以直接从 CI 服务器上面拉取构建结果,非常的方便。
# Gradle 守护进程
gradle 会开启一个守护进程来和各个 build 任务进行交互,优点就是不需要每次构建都初始化需要的组件和服务。
同时因为守护进程是一个一直运行的进程,除了可以避免每次 JVM 启动的开销之外,还可以缓存项目结构,文件,task 和其他的信息,从而提升运行速度。
我们可以运行 gradle –status 来查看正在运行的 daemons 进程。
从 Gradle 3.0 之后,daemons 是默认开启的,你可以使用 org.gradle.daemon=false 来禁止 daemons。
我们可以通过下面的几个图来直观的感受一下 gradle 和 maven 的性能比较:
# 使用 gradle 和 maven 构建 Apache Commons Lang 3 的比较
# 使用 gradle 和 maven 构建小项目(10 个模块,每个模块 50 个源文件和 50 个测试文件)的比较
# 使用 gradle 和 maven 构建大项目(500 个模块,每个模块 100 个源文件和 100 个测试文件)的比较
可以看到 gradle 性能的提升是非常明显的。
# 依赖的区别
gradle 和 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。
先看下怎么引入这三个 plugin:
plugins { id 'base' id 'java' id 'maven-publish'}
clean 会被转换成为 clean task,compile 会被转换成为 classes task,test 会被转换成为 test task,package 会被转换成为 assemble task,verify 会被转换成为 check task,install 会被转换成为 Maven Publish Plugin 中的 publishToMavenLocal task,deploy 会被转换成为 Maven Publish Plugin 中的 publish task。
有了这些 task 之间的对应关系,我们就可以尝试进行 maven 到 gradle 的转换了。
# 自动转换
我们除了可以使用 gradle init 命令来创建一个 gradle 的架子之外,还可以使用这个命令来将 maven 项目转换成为 gradle 项目,gradle init 命令会去读取 pom 文件,并将其转换成为 gradle 项目。
# 转换依赖
gradle 和 maven 的依赖都包含了 group ID, artifact ID 和版本号。两者本质上是一样的,只是形式不同,我们看一个转换的例子:
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
2
3
4
5
6
7
上是一个 maven 的例子,我们看下 gradle 的例子怎写:
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
可以替换成 runtimeOnly 。
# test
gradle 中的 test 分为两种,一种是编译 test 项目的时候需要,那么可以使用 testImplementation,一种是运行 test 项目的时候需要,那么可以使用 testRuntimeOnly。
# provided
可以替换成为 compileOnly。
# import
在 maven 中,import 经常用在 dependencyManagement 中,通常用来从一个 pom 文件中导入依赖项,从而保证项目中依赖项目版本的一致性。
在 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'
}
2
3
4
5
比如上面的例子中,我们导入了 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'
}
2
3
4
5
6
7
8
9
# 转换 repositories 仓库
gradle 可以兼容使用 maven 或者 lvy 的 repository。gradle 没有默认的仓库地址,所以你必须手动指定一个。
你可以在 gradle 使用 maven 的仓库:
repositories {
mavenCentral()
}
2
3
我们还可以直接指定 maven 仓库的地址:
repositories {
maven {
url "http://repo.mycompany.com/maven2"
}
}
2
3
4
5
如果你想使用 maven 本地的仓库,则可以这样使用:
repositories {
mavenLocal()
}
2
3
但是 mavenLocal 是不推荐使用的,为什么呢?
mavenLocal 只是 maven 在本地的一个 cache,它包含的内容并不完整。比如说一个本地的 maven repository module 可能只包含了 jar 包文件,并没有包含 source 或者 javadoc 文件。那么我们将不能够在 gradle 中查看这个 module 的源代码,因为 gradle 会首先在 maven 本地的路径中查找这个 module。
并且本地的 repository 是不可信任的,因为里面的内容可以轻易被修改,并没有任何的验证机制。
# 控制依赖的版本
如果同一个项目中对同一个模块有不同版本的两个依赖的话,默认情况下 Gradle 会在解析完 DAG 之后,选择版本最高的那个依赖包。
但是这样做并不一定就是正确的, 所以我们需要自定义依赖版本的功能。
首先就是上面我们提到的使用 platform() 和 enforcedPlatform() 来导入 BOM(packaging 类型是 POM 的)文件。
如果我们项目中依赖了某个 module,而这个 module 又依赖了另外的 module,我们叫做传递依赖。在这种情况下,如果我们希望控制传递依赖的版本,比如说将传递依赖的版本升级为一个新的版本,那么可以使用 dependency constraints:
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'
}
}
}
2
3
4
5
6
7
8
9
10
11
注意,dependency constraints 只对传递依赖有效,如果上面的例子中 commons-codec 并不是传递依赖,那么将不会有任何影响。
同时 Dependency constraints 需要 Gradle Module Metadata 的支持,也就是说只有你的 module 是发布在 gradle 中才支持这个特性,如果是发布在 maven 或者 ivy 中是不支持的。
上面讲的是传递依赖的版本升级。同样是传递依赖,如果本项目也需要使用到这个传递依赖的 module,但是需要使用到更低的版本(因为默认 gradle 会使用最新的版本),就需要用到版本降级了。
dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.4'
implementation('commons-codec:commons-codec') {
version {
strictly '1.9'
}
}
}
2
3
4
5
6
7
8
我们可以在 implementation 中指定特定的 version 即可。
strictly 表示的是强制匹配特定的版本号,除了 strictly 之外,还有 require,表示需要的版本号大于等于给定的版本号。prefer,如果没有指定其他的版本号,那么就使用 prefer 这个。reject,拒绝使用这个版本。
除此之外,你还可以使用 Java Platform Plugin 来指定特定的 platform,从而限制版本号。
最后看一下如何 exclude 一个依赖:
dependencies {
implementation('commons-beanutils:commons-beanutils:1.9.4') {
exclude group: 'commons-collections',
module: 'commons-collections'
}
}
2
3
4
5
6
# 多模块项目
maven 中可以创建多模块项目:
<modules>
<module>simple-weather</module>
<module>simple-webapp</module>
</modules>
2
3
4
我们可以在 gradle 中做同样的事情 settings.gradle:
rootProject.name = 'simple-multi-module' include 'simple-weather', 'simple-webapp'
# profile 和属性
maven 中可以使用 profile 来区别不同的环境,在 gradle 中,我们可以定义好不同的 profile 文件,然后通过脚本来加载他们:
build.gradle:
if (!hasProperty('buildProfile')) ext.buildProfile = 'default'
apply from: "profile-${buildProfile}.gradle"
task greeting {
doLast {
println message
}
}
2
3
4
5
6
7
8
9
profile-default.gradle:
ext.message = 'foobar'
profile-test.gradle:
ext.message = 'testing 1 2 3'
我们可以这样来运行:
> gradle greetingfoobar> gradle -PbuildProfile=test greetingtesting 1 2 3
# 资源处理
在 maven 中有一个 process-resources 阶段,可以执行 resources:resources 用来进行 resource 文件的拷贝操作。
在 Gradle 中的 Java plugin 的 processResources task 也可以做相同的事情。
比如我可以执行 copy 任务:
task copyReport(type: Copy) {
from file("buildDir/reports/my-report.pdf")
into file("buildDir/toArchive")
}
2
3
4
更加复杂的拷贝:
task copyPdfReportsForArchiving(type: Copy) {
from "buildDir/reports"
include "*.pdf"
into "buildDir/toArchive"
}
2
3
4
5
当然拷贝还有更加复杂的应用。这里就不详细讲解了。