Gradle和Maven的比较
性能比较
虽然现在大家的机子性能都比较强劲,好像在做项目构建的时候性能的优势并不是那么的迫切,但是对于大型项目来说,一次构建可能会需要很长的时间,尤其对于自动化构建和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性能的提升是非常明显的。
可扩展性
Maven和Gradle设计时都采用了插件机制。但显然Gradle更胜一筹。主要原因在于Maven是基于XML进行配置。所以其配置语法太受限于XML。即使实现很小的功能都需要设计一个插件,建立其与XML配置的关联。比如想在Maven中执行一条shell命令,其配置如下:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2</version>
<executions>
<execution>
<id>drop DB => db_name</id>
<phase>pre-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>curl</executable>
<arguments>
<argument>-s</argument>
<argument>-S</argument>
<argument>-X</argument>
<argument>DELETE</argument>
<argument>http://${db.server}:${db.port}/db_name</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
而在Gradle中则一切变得非常简单。
task dropDB(type: Exec) {
commandLine ‘curl’,’-s’,’s’,’-x’,’DELETE’,"http://${db.server}:{db.port}/db_name"
}
在创建自定义插件方面,Maven和Gradle的机制都差不多,都是继承自插件基类,然后实现要求的方法。这里就不展开说明。、
依赖管理系统
Maven为Java世界引入了一个新的依赖管理系统。在Java世界中,可以用groupId、artifactId、version组成的Coordination(坐标)唯一标识一个依赖。任何基于Maven构建的项目自身也必须定义这三项属性,生成的包可以是Jar包,也可以是war包或者ear包。一个典型的依赖引用如下所示:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
从上面可以看出当引用一个依赖时,version可以省略掉,这样在获取依赖时会选择最新的版本。而存储这些组件的仓库有远程仓库和本地仓库之分。远程仓库可以使用世界公用的central仓库,也可以使用Apache Nexus自建私有仓库;本地仓库则在本地计算机上。通过Maven安装目录下的settings.xml文件可以配置本地仓库的路径,以及采用的远程仓库的地址。
Gradle在设计的时候基本沿用了Maven的这套依赖管理体系。不过它在引用依赖时还是进行了一些改进。首先引用依赖方面变得非常简洁。
dependencies {
compile 'org.hibernate:hibernate-core:3.6.7.Final'
testCompile ‘junit:junit:4.+'
}
第二,Maven和Gradle对依赖项的scope有所不同。在Maven世界中,一个依赖项有6种scope,分别是complie(默认)、provided、runtime、test、system、import。而grade将其简化为了4种,compile、runtime、testCompile、testRuntime。那么如果想在gradle使用类似于provided的scope怎么办?别着急,由于gradle语言的强大表现力,我们可以轻松编写代码来实现类似于provided scope的概念(例如How to use provided scope for jar file in Gradle build?)。
第三点是Gradle支持动态的版本依赖。在版本号后面使用+号的方式可以实现动态的版本管理。
第四点是在解决依赖冲突方面Gradle的实现机制更加明确。使用Maven和Gradle进行依赖管理时都采用的是传递性依赖;而如果多个依赖项指向同一个依赖项的不同版本时就会引起依赖冲突。而Maven处理这种依赖关系往往是噩梦一般的存在。而Gradle在解决依赖冲突方面相对来说比较明确。在Chapter 23. Dependency Management 中的23.2.3章节详细解读了gradle是如何处理版本冲突的。
多模块构建
在SOA和微服务的浪潮下,将一个项目分解为多个模块已经是很通用的一种方式。在Maven中需要定义个parent POM作为一组module的聚合POM。在该POM中可以使用
而Gradle也支持多模块构建。而在parent的build.gradle中可以使用allprojects和subprojects代码块来分别定义里面的配置是应用于所有项目还是子项目。对于子模块的定义是放置在setttings.gradle文件中的。在gradle的设计当中,每个模块都是Project的对象实例。而在parent build.gradle中通过allprojects或subprojects可以对这些对象进行各种操作。这无疑比Maven要灵活的多。
比如在parent的build.gradle中有以下代码:
allprojects {
task hello << { task -> println "I'm $task.project.name" }
}
执行命令gradle -q hello会依次打印出父module以及各个submodule的项目名称。这种强大的能力能让gradle对各个模块具有更强的定制化。
一致的项目结构
在Ant时代大家创建Java项目目录时比较随意,然后通过Ant配置指定哪些属于source,那些属于testSource等。而Maven在设计之初的理念就是Conversion over configuration(约定大于配置)。其制定了一套项目目录结构作为标准的Java项目结构。一个典型的Maven项目结构如下:
Gradle也沿用了这一标准的目录结构。如果你在Gradle项目中使用了标准的Maven项目结构的话,那么在Gradle中也无需进行多余的配置,只需在文件中包含apply plugin:'java',系统会自动识别source、resource、test srouce、 test resource等相应资源。不过Gradle作为JVM上的构建工具,也同时支持groovy、scala等源代码的构建,甚至支持Java、groovy、scala语言的混合构建。虽然Maven通过一些插件(比如maven-scala-plugin)也能达到相同目的,但配置方面显然Gradle要更优雅一些。
一致的构建模型
为了解决Ant中对项目构建活动缺乏标准化的问题,Maven特意设置了标准的项目构建周期,其默认的构建周期如下所示:
<phases>
<phase>validate</phase>
<phase>initialize</phase>
<phase>generate-sources</phase>
<phase>process-sources</phase>
<phase>generate-resources</phase>
<phase>process-resources</phase>
<phase>compile</phase>
<phase>process-classes</phase>
<phase>generate-test-sources</phase>
<phase>process-test-sources</phase>
<phase>generate-test-resources</phase>
<phase>process-test-resources</phase>
<phase>test-compile</phase>
<phase>process-test-classes</phase>
<phase>test</phase>
<phase>prepare-package</phase>
<phase>package</phase>
<phase>pre-integration-test</phase>
<phase>integration-test</phase>
<phase>post-integration-test</phase>
<phase>verify</phase>
<phase>install</phase>
<phase>deploy</phase>
</phases>
而这种构建周期也是Maven最为人诟病的地方。因为Maven将项目的构建周期限制的太死,你无法在构建周期中添加新的phase,只能将插件绑定到已有的phase上。而现在项目的构建过程变得越来越复杂,而且多样化,显然Maven对这种复杂度缺少足够的应变能力。比如你想在项目构建过程中进行一项压缩所有javascript的任务,那么就要绑定到Maven的现有的某个phase上,而显然貌似放在哪个phase都不太合适。而且这些phase都是串行的,整个执行下来是一条线,这也限制了Maven的构建效率。而Gradle在构建模型上则非常灵活。在Gradle世界里可以轻松创建一个task,并随时通过depends语法建立与已有task的依赖关系。甚至对于Java项目的构建来说,Gradle是通过名为java的插件来包含了一个对Java项目的构建周期,这等于Gradle本身直接与项目构建周期是解耦的。
从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>
上是一个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'
}
比如上面的例子中,我们导入了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仓库
gradle可以兼容使用maven或者lvy的repository。gradle没有默认的仓库地址,所以你必须手动指定一个。
你可以在gradle使用maven的仓库:
repositories {
mavenCentral()
}
我们还可以直接指定maven仓库的地址:
repositories {
maven {
url "http://repo.mycompany.com/maven2"
}
}
如果你想使用maven本地的仓库,则可以这样使用:
repositories {
mavenLocal()
}
但是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'
}
}
}
注意,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'
}
}
}
我们可以在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' } }
多模块项目
maven中可以创建多模块项目:
<modules>
<module>simple-weather</module>
<module>simple-webapp</module>
</modules>
我们可以在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
}
}
profile-default.gradle:
ext.message = 'foobar'
profile-test.gradle:
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也可以做相同的事情。
比如我可以执行copy任务:
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"
}
注意:本文归作者所有,未经作者允许,不得转载