Gradle系列:深入了解Gradle和Maven的区别

Posted ABin-阿斌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Gradle系列:深入了解Gradle和Maven的区别相关的知识,希望对你有一定的参考价值。

前言

简介

  • GradleMaven 都可以用来构建 Java 程序,甚至在某些情况下,两者还可以互相转换那么他们两个的共同点和不同点是什么?
  • 我们如何在项目中选择使用哪种技术呢?一起来看看吧。

gradle和maven的比较

  • 虽然 GradleMaven都可以作为 Java 程序的构建工具。但是两者还是有很大的不同之处的。我们可以从下面几个方面来进行分析。

可扩展性

  • Google 选择 Gradle 作为 Android 的构建工具不是没有理由的,其中一个非常重要的原因就是因为 Gradle 够灵活。

  • 一方面是因为 Gradle 使用的是 groovy 或者 kotlin 语言作为脚本的编写语言,这样极大的提高了脚本的灵活性,但是其本质上的原因是 Gradle 的基础架构能够支持这种灵活性。

  • 你可以使用 Gradle 来构建 NativeC/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 认为可以重用之前的执行结果。

  • 所以在编写 gradletask 的时候,需要指定 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

  • 我们可以通过下面的几个图来直观的感受一下 gradlemaven 的性能比较:

  • 使用 gradle 和 maven 构建 Apache Commons Lang 3 的比较:

  • 使用 gradle 和 maven 构建小项目(10个模块,每个模块50个源文件和50个测试文件)的比较:

  • 使用gradle和maven构建大项目(500个模块,每个模块100个源文件和100个测试文件)的比较:

  • 可以看到 gradle 性能的提升是非常明显的。

依赖的区别

  • gralde 和 maven 都可以本地缓存依赖文件,并且都支持依赖文件的并行下载。

  • maven 中只可以通过版本号来覆盖一个依赖项。而 gradle 更加灵活,你可以自定义依赖关系和替换规则,通过这些替换规则,gradle 可以构建非常复杂的项目。

从maven迁移到gradle

  • 因为 maven 出现的时间比较早,所以基本上所有的 java 项目都支持 maven,但是并不是所有的项目都支持 gradle

  • 如果你有需要把 maven 项目迁移到 gradle 的想法,那么就一起来看看吧。

  • 根据我们之前的介绍,大家可以发现 gradle 和 maven 从本质上来说就是不同的,gradle 通过 taskDAG 图来组织任务,而 maven 则是通过 attachphases 的 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>
  • 上是一个maven的例子,我们看下gradle的例子怎写:
dependencies {
    implementation 'log4j:log4j:1.2.12'  
}
  • 可以看到 gradlemaven 写起来要简单很多。

  • 注意这里的 implementation 实际上是由 Java Plugin 来实现的。

  • 我们在 maven 的依赖中有时候还会用到 scope 选项,用来表示依赖的范围

  • 我们看下这些范围该如何进行转换:

    • compile:

      • gradle 可以有两种配置来替换 compile,我们可以使用 implementation 或者 api

      • 前者在任何使用 Java Plugingradle 中都可以使用,而 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 或者 lvyrepositorygradle 没有默认的仓库地址,所以你必须手动指定一个。

你可以在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() 来导入 BOMpackaging 类型是 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 pluginprocessResources 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"
}

以上是关于Gradle系列:深入了解Gradle和Maven的区别的主要内容,如果未能解决你的问题,请参考以下文章

gradle 初了解

java 软件开发 的 新技术

Gradle系列之从init.gradle说起

Gradle for Android 系列:为什么 Gradle 这么火

[万字长文]一文带你深入了解Android Gradle

build.gradle系列:maven { url ‘https://jitpack.io‘ }的语法