Gradle 是什么

Gradle是一个基于Ant和Maven概念的项目自动化构建工具。遵循和Maven相同的“约定大于配置”(Convention Over Configuration)思想,采用尽量少的配置完成项目的构建,而不是像Ant一样要求程序员写大量的内容。但是其构建速度相比Maven又快出很多,而且构建脚本也是采用Groovy语言编写(也可以采用Kotlin),相比Maven的xml更加简洁方便。

总结来说,Gradle是一款多语言、快速、精简配置Build Tool

虽然很多IDE对Gradle的集成已经非常好了(例如IDEA),但是这篇教程并不打算一开始就说明在IDEA中如何使用Gradle,而是先从最基本的命令行使用方式开始学习,最后再说明一下在IDEA中可以使用那些操作简化命令行中的操作。

Gradle 的安装

安装参考 https://gradle.org/install/

可以使用各种包管理器(官方推荐的是SDKMAN!的sdk命令,在MacOS上也可以采用Homebrew的brew命令,快速安装gradle。如果是windows电脑,或者对版本有一些要求,也可以使用手动安装,总体来说有以下步骤:

  1. 检查安装依赖:已经安装了1.8版本以上的JDK,并且JAVA_HOME环境变量已经配置。
  2. 下载最新的zip压缩包,这里推荐下载binary二进制版就够了。下载地址:https://gradle.org/releases
  3. 解压压缩包,将bin目录所在的绝对路径配置到环境变量PATH中去。
  4. 验证安装:gradle -v,可以看到版本输出。
  5. (可选)为了方便后续使用,建议配置GRADLE_USER_HOME环境变量在~/.gradle。这个是用来做缓存、基本配置、启动自动运行脚本等的地方。

当然,官方非常推荐使用Gradle Wrapper来管理项目,不需要实现下好相同版本的Gradle,也不需要考虑在一台计算机上管理多个Gradle版本。这里列出一些操作:

  1. 给没有gradle wrapper的gradle项目添加gradle wrapper:gradle wrapper --gradle-version=8.1.1 --distribution。前提是本机已经安装了gradle。
  2. 给有gradle wrapper的项目升级gradle wrapper:./gradlew wrapper --gradle-version=8.1.1 --distribution。不需要已安装gradle。如果是windows注意把gradlew换成gradlew.bat,下同。
  3. 缓存对应版本的gradle:gradlew tasks

有了gradlew之后,就可以如同使用本机安装的gradle一样,用gradlew做所有相同的事,只要吧gradle替换成gradlew就可以了。

当然,如果安装了IDEA之后,默认使用的Gradle Wrapper,不需要做任何gradle的安装,就可以直接使用,IDEA已经集成了Gradle的操作。

Gradle 项目结构

在看gradle项目结构之前,先尝试一下使用刚刚安装的Gradle初始化项目。先建立一个新的目录用来存放新项目文件,然后在这个目录下gradle init就可以初始化。

gradle提供了四种项目类型:

  • basic 不含源码目录的最基本目录结构
  • application 含有一个app目录,存放应用的源码
  • library 含有一个lib目录,存放库代码
  • Gradle Plugin 用于开发Gradle插件

这里以application为例。创建后看到如下的目录结构(win10下使用tree /f 命令可以查看目录树层次结构。

.
│  .gitattributes ...git相关
│  .gitignore ...git相关
│  gradlew ...Unix系统下使用的gradle wrapper脚本
│  gradlew.bat ...Windows系统下使用的gradle wrapper脚本
│  settings.gradle ...gradle脚本(核心)
│
├─.gradle
│      file-system.probe
│
├─app ... 源码目录
│  │  build.gradle ...gradle脚本(核心)
│  │
│  └─src
│      ├─main
│      │  ├─java
│      │  │  └─trygradlecmd
│      │  │          App.java
│      │  │
│      │  └─resources
│      └─test
│          ├─java
│          │  └─trygradlecmd
│          │          AppTest.java
│          │
│          └─resources
└─gradle ...gradle wrapper目录
    └─wrapper
            gradle-wrapper.jar
            gradle-wrapper.properties

可见,源码目录结构gradle和maven比较相似。

使用Maven仓库

Gradle不提供一套单独的仓库系统,而是可以直接复用Maven的仓库。

在项目的gradle脚本(settings.gradle或者build.gradle)中,可以找到repositoris定义:

repositories {
    // Use Maven Central for resolving dependencies.
    mavenCentral()
}

默认只有一个mavenCentral,表示会使用maven中央仓库作为依赖寻找的地方。如果要设置本地maven仓库,则在前面加上mavenLocal(),gradle看到这个会自动去找本地的maven仓库位置,默认去读取~/.m2/settings.xml找到仓库位置,或将.m2/repository直接作为本地仓库位置。

repositories下面设置了多个仓库时,寻找依赖会从上到下挨个在所有仓库中找,使用第一个找到的里面的。如果只在远程仓库中找到,则会自动下载缓存到本地,注意缓存的位置在GRADLE_USER_HOME/caches或者~/.gradle/caches下面,而不是缓存到maven本地仓库中。所以严格来说,gradle并不存在本地仓库的概念,只有本地缓存。gradle本地缓存的格式和maven本地仓库的存放格式不同。

默认没有配置mavenLocal的原因是,有可能该计算机之前就没有安装使用过maven,也就不存在maven本地仓库。

如果希望配置类似maven一样的镜像源时,也不需要设置mirrors了,而是直接将镜像仓库放在repositories前面的位置就可以了。例如阿里云maven镜像的gradle配置方法(参考https://developer.aliyun.com/mvn/guide):

allprojects {
  repositories {
    maven {
      url 'https://maven.aliyun.com/repository/public/'
    }
    mavenLocal()
    mavenCentral()
  }
}

如果想要全局配置这个仓库,就要在gradle安装目录下的init.d中创建xxx.gradle脚本,并在这里配置上仓库,这个目录下的脚本是作为构建初始化脚本执行的,在每次构建之前都会执行。例如可以在该目录下新建mirror.gradle脚本,内容放:

allProjects {
    // jar包资源
    repositories {
        mavenLocal()
        maven { name "Alibaba"; url "https://maven.aliyun.com/repository/public/"}
        mavenCentral()
    }
    // 构建脚本本身需要的依赖
    buildScript {
        repositories {
            maven {name "Alibaba"; url "https://maven.aliyun.com/repository/public/"}
            maven {name "M2"; url "https://plugins.gradle.org/m2/"}
        }
    }
}

执行初始化脚本还有别的方式,总结来说有4种:

  1. 放在gradle安装目录下的init.d目录中
  2. 放在~/.gradle目录下
  3. 放在~/.gradle/init.d目录下
  4. 执行gradle命令时,通过--init-script <path>参数指定额外的初始化脚本。

Gradle 常用命令

Gradle常用指令

  • gradle clean 清理build目录
  • gradle classes 编译
  • gradle test 运行测试,会生成测试报告
  • gradle build 编译+测试+打包
  • gradle build -x test 跳过测试构建

会了这些基本的命令,就算不怎么熟悉gradle的语法,也不至于拿到一个别人的项目而不知道怎么配置运行了。

Groovy语法简介

因为gradle脚本默认是使用Groovy语言编写,因此要把gradle学好,就要接触一点groovy语法。好在groovy可以认为是Java语言的拓展,很多概念上的东西还是相通的。只需要知道groovy的几个常用的”魔法“就可以理解gradle脚本中的大多数操作了。

  1. 闭包。Groovy引入了闭包的概念,用{}表示闭包,里面可以放一些参数和语句。乍一看闭包就像lambda表达式,但是他的功能却比单纯的lambda表达式强大很多。闭包可以通俗地理解为包装了一个函数和他声明时所在的环境。也就是说,就算在闭包外面,也可以通过闭包访问声明它的类中的成员方法和成员变量。另外,还可以通过更改闭包的delegate和resolveStrategy,让闭包中调用的函数和使用的变量变成其他对象的。这为使用Groovy定义DSL提供了非常多便利。我们常见的repository {…}的这个花括号块其实就是一个闭包。
  2. 函数调用的省略写法。在Groovy中,如果一个函数只有一个参数,那么调用这个函数的时候可以省略括号。比如println(str)就可以写成println str。类似的,a b c d这样的写法其实等价于a(b).c(d)。这样就解释了gradle脚本中很多看起来像配置文件的地方,其实都是函数调用。另外,如果一个函数的最后一个参数是闭包,那还可以直接把这个闭包写在括号外面。

Gradle task使用

gradle中所有的操作本质都是基于task的执行,所以task是gradle构建的核心。

task的定义

直接在build.gradle文件中定格定义。定义task的多种写法:

  1. task “task1” {}
  2. task task1 {}
  3. task(map, “task1”) {} 这种方式既可以显式传入一个map变量,也可以采用groovy的魔法,按照"action": {println "haha"}类似的形式传入多个键值,例子中action字符串是键,而后面的闭包是值。也可以写成action: {prinln "haha"},因为groovy中map的键的字符串可以省略。
  4. tasks.register(‘task1’) {} 这种方法会延迟创建,当task被使用的时候才会创建
  5. tasks.create(‘task1’) {} 这种方法也会立即创建task

方法3中提到的groovy魔法生效的条件是,在方法定义中必须将map放在第一个参数,否则就不能这样用,只能提供map对象。我们查看gradle源码,发现task方法的一个定义是Task task(Map<String, ?> args, String name, Closure configureClosure);,所以可以这样使用。而且这个name参数不管放在什么参数的什么顺序都没关系。

任务的属性常用的有:

  1. type 任务的类型,类似继承已有的任务
  2. overwrite 是否可以替换存在的task
  3. dependsOn 依赖
  4. action 要执行的动作
  5. description 任务描述
  6. group 任务分组。还可以给已存在任务重新分组,例如clean.group('abc')clean.group = 'abc'

task的行为

直接写在闭包中的语句将在task的配置阶段执行,而写在doFirst和doLast块中的将在任务的执行阶段执行。例如:

task task1 {
    println "配置中。。。"
    doFirst {
        println "first do..."
    }
    doLast {
        println "last do..."
    }
}

doFirst和doLast还可以在任务外定义。例如:

task tast1 {
    doFirst {
        println "first do..."
    }
    doLast {
        println "last do..."
    }
}

task1.doFirst {
    println "first do... out"
}
task1.doLast {
    println "last do... out"
}

注意,有多个doFirst和doLast时,后面定义的doFirst会放在所有已定义的doFirst之前执行,而后面定义的doLast,后面定义的doLast将放在已定义的doLast之后执行。

在第三种定义方式中,传入了一个map,map中可以理解为能包含各种灵活的参数。如果map中有action字段,并且值是一个闭包,则闭包中的语句会在doFirst和doLast之间被执行。例如:

def map = [:]
map.put("action", {println "action doing..."})

task(map, "task1") {
    doFirst {
        println "first doing..."
    }
    doLast {
        println "last doing..."
    }
}

任务的依赖

任务的依赖可以有三种方式定义:

  1. 用参数定义:传入具名参数dependsOn: ['task0', 'task2'] 这个具名参数会被当做map参数的一个键值被处理。值的位置是一个依赖的任务的任务名列表。
  2. 内部依赖:task内部配置位置指定dependsOn = ['task0','task2']
  3. 外部依赖:类似于task1.dependsOn = ['task0']

依赖可以用路径来定义。在gradle中字符串以冒号开头表示是一个从根开始的绝对路径,用:表示子路径。这里的:就像linux目录的/一样。例如:subject01:task1表示subject01子项目下的task1任务。所以依赖的任务还可以是其他子项目的任务。

示例:

task task0 {}
task task1 (dependsOn: ['task0']) {}
task task2 {
    dependsOn = ['task1']
}
task task3 () {}
task3.dependsOn = ['task2']
task3.dependsOn = [':subject01:task1'] // 跨项目依赖

任务的执行

gradle命令执行任务:gradle <taskname> <args...>

常用的任务:

  1. gradle build
  2. gradle run 需要application插件,还需要用mainClassName=’top.claws.app.Main’来指定主启动类名字
  3. gradle clean 清空当前目录的build目录
  4. gradle projects 显示当前项目及其子项目的树状结构
  5. gradle tasks 显示当前项目有分组的task。使用group 'abc'给task分组。gradle tasks –all 显示所有(包括没分组的)。gradle tasks –group=’abc’显示abc组的任务。
  6. gradle help –task task1 查看task1的详细信息。
  7. gradle dependencies 查看项目的依赖信息,用依赖树形式展示。结果中有*的表示有jar包版本冲突,实际会采用最高版本的jar包。
  8. gradle properties 查看项目的所有属性列表。
  9. -x 参数表示–exclude-task 用于跳过某些任务。例如gradle -x test clean build跳过测试构建。
  10. –continue 忽略失败的任务,任务失败后继续执行。

一般还会在gradle.properties中配置一些性能相关选项,例如jvmargs。

gradle支持任务名缩写:gradle connectTask可以缩写成gradle cT

gradle缺省任务的关系:

image-20230508003240057

执行任务的时候也可以使用任务的路径来执行。例如gradle :subproject:task0可以执行subproject下的task0任务。

Gradle任务路径。gradle中使用:作为路径的分隔符,使用方法和文件系统非常相似。例如,如果是不以:开头的路径,默认是以当前项目为参照的相对路径,而以:开头则是以根项目开始的绝对路径。:task0表示的是跟项目的task0任务,而task0表示的是当前项目的task0任务。subject:task0表示的是当前项目的子项目subject下的task0。

任务类型

通过定义任务的时候指定任务的type属性,可以让任务继承已有的任务,直接使用已经定义好的一些操作。具体的官方文档:Delete type的官方文档说明 ,可以在左侧的目录中找到所有Task types的说明。

例如,可以自己定义一个myClean任务:

// 使用tasks.register定义任务,不写type:
tasks.register('myClean', Delete) {
    delete builddir
}

也可以自定义type:

task task2 (type: CustomTask) {
    sayHello()
}

class CustomTask extends DefaultTask {
    def sayHello() {
        println 'hello, everyone!'
    }

    @TaskAction
    def doSelf() {
        println 'self doing...'
    }
}

// 执行 gradle task2 可以看到先输出hello, everyone!,再输出self doing...

任务的动态分配

可以通过循环一次注册多个任务:

4.times { cnt ->
    tasks.register("task$cnt") {
        doLast {
            println "I am task$cnt"
        }
    }
}

并可以在运行时动态地改变任务的依赖项:

tasks.named("task0") {
    dependsOn = ['task1', 'task3']
}

// 运行 gradle task0 就必须先运行task1和task3

任务的关闭开启 超时

通过enabled方法(或修改enabled属性的值)可以控制任务的关闭或者开启。关闭的任务需要执行的时候会直接跳过,相当于执行成功。

task1.enabled = false
tasks.named("task0") {
    dependsOn = ['task1', 'task3']
}
// 不再会执行task1

此外,任务还可以设置超时时间,如果超过指定的超时时间没有执行完,任务会以失败告终。通过timeout属性指定超时的时间间隔。

task "task100" {
    doLast {
        Thread.sleep(1000)
    }
    timeout = Duration.ofMillis(100)
}
// 执行 gradle task100 会失败。如果使用 gradle task100 --continue,就可以跳过失败的任务,继续执行其他任务,否则遇到失败就会立即退出。

任务的查找

在运行时,可以根据任务的路径或者名称查找获得任务的对象,动态更改任务的行为。例如:

// 根据名称查找
tasks.findByName('task0').doFirst({println 'first doing task0...0'})
tasks.getByName('task0').doFirst({println 'first doing task0...1'})
// 根据路径查找
tasks.findByPath(':learnTask:task0').doFirst({println 'first doing task0...2'})
tasks.getByPath(':learnTask:task0').doFirst({println 'first doing task0...3'})

任务规则

任务规则用于,当gradle要执行的任务名不存在时,会尝试执行规则中的代码,如果规则创建了task,就会执行对应创建出的task。例如:

tasks.addRule("hahaRule") { String taskName -> task(taskName) {
        println "printInfo task not defined!"
    }
}

注意:网上很多例子中tasks.addRule后面参数中跟的是类似于"Pattern: printInfo <dir>"之类的形式,这只是单纯的命名,gradle使用规则的时候并不会使用这个pattern进行匹配。具体是否匹配还要看后面的闭包是否创建了task。

例如:

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}
// 执行 gradle ping1234 会输出Pinging: 1234, 而不会报错

任务的onlyIf断言

任务如果设置了onlyIf,当onlyIf中闭包返回false时,这个任务就算需要被执行,也会被跳过(等价于enabled=false)。如果onlyIf返回为true,则不会影响这个task正常的执行(没有特殊效果)

例如:

task3.onlyIf { !project.hasProperty('ok')}

// 执行 gradle task3 -Pok 时,无事发生,task3并不会被执行。-P参数可以临时指定一个属性值。
// 执行 gradle task3 时,task3会被执行。

默认任务

可以用defaultTasks指定默认会执行的任务,当使用gradle命令但是没有指定任何任务名的时候,gradle会执行这些默认任务。

defaultTasks 'task1', 'task2'
// 当没有指定任务名,例如运行 gradle 时,会执行task1和task2两个任务(前提是两个任务enabled都是true,无onlyIf或onlyIf为真)

Gradle 文件操作

文件和文件集合

可以使用Gradle对文件进行操作。使用file(String path)函数创建一个文件对象。这里的path可以是相对路径,也可以是一个绝对路径。如果是相对路径,是参照当前项目(注意不是根项目)的相对路径。

对文件的操作还可以是文件集合的方式,使用files创建文件集合。

File configFile = file('src/config.xml')
configFile.createNewFile()

def fileCollection = files('src/config1.xml', 'src/config2.xml', 'src/config3.xml')
fileCollection.forEach(item -> {item.createNewFile()})

def fileCollection2 = fileCollection + file('src/config4.xml') // 集合中添加File对象
def fileCollection3 = fileCollection2 - files('src/config1.xml') // 集合和集合取差
for (def f in fileCollection3) {
    println f.exists()
}

文件树

可以使用fileTree方法创建一个文件树,文件树可以访问到一个目录下所有子目录中的文件。可以用include和exclude对文件进行过滤。例如:

def tree = fileTree('src')
tree.forEach(item -> { println item.name })
println()
// 对文件进行过滤,include表示仅保留满足匹配的文件,而exclude表示去掉匹配的文件
tree.include('**/*.xml')
tree.forEach(item -> {println item.name})
println()
// 在创建的时候就可以指定一些include和exclude参数
def tree2 = fileTree(dir: 'src', includes: ['**/*.xml', '**/*.jpg'], exclude: '**/*.java')
tree2.forEach(item -> {println item.name})

Copy Task

可以通过创建type为Copy的task,表示一个文件复制任务。

用from表示拷贝来源,用into表示拷贝目标。如果拷贝来源是一个压缩包,会将压缩包解压后的内容拷贝到目标。如果拷贝来源是目录,会将目录下的所有子目录和文件保留原有层次结构复制过去。

例如:

task task1 (type: Copy) {
    from 'src'
    into 'build/copyOutput'
}    

Gradle 依赖

依赖的方式

依赖的三种方式:

  1. 本地依赖:依赖本地的某些个jar包。在配置文件中可以用文件集合或者文件树的形式指定依赖的jar包。
  2. 项目依赖:依赖哪一个project。
  3. 直接依赖:通过依赖的组名、依赖的名称、依赖的版本号这三个和Maven类似的项目坐标来定义依赖。

例如:

// 本地依赖:通过files或者freeTree指定依赖的文件
implementation files('lib/mysql.jar', 'lib/log4j.jar')
implementation fileTree('dir': 'lib', includes: ['*.jar'], excludes: [''])
// 项目依赖:通过路径指定项目
implementation project(':subproject02')
// 直接依赖:两种写法,既可以直接::隔开,也可以手动指定group name version三元组坐标
implementation 'org.projectlombok:lombok:1.18.26'
implementation group: 'org.projectlombok', name: 'lombok', version: '1.18.26'

依赖的类型

有如下几种常见的依赖类型,表示这个依赖用在什么场合下。

类型 来自插件 说明
compileOnly java 表示编译期需要而运行期不需要的依赖。
runtimeOnly java 表示运行期需要而编译器不需要。例如mysql驱动。
implementation java 表示编译和运行期都需要的。
testCompileOnly java 表示仅测试的编译器需要。
testRuntimeOnly java 表示仅测试的运行期需要。
testImplementation java 表示仅测试的编译和运行期需要。
providedCompile war 表示编译、测试阶段代码需要依赖,而运行时不需要,不需要打进包
api java-library 表示依赖项可以传递性地导出给使用者,用于编译时和运行时
compileOnlyApi java-library 在当前项目和使用者的项目编译时使用,运行时不需要

api和implementation:

  • api支持依赖传递,但是底层模块发生变化的时候,上层模块都需要重新编译,编译的速度慢。但是可以避免模块被重复依赖。
  • implementation不支持依赖传递,编译速度快。

这里的依赖传递的意思是:

  • 如果a implementation b, b implementation c,那a中不能直接使用c的内容
  • 如果a implementation b, b api c,则a可以使用c中的内容。

也就是说,a implementation b的时候,会同时依赖所有b通过api依赖的包。以上所说的都是编译阶段。

选择时,优先使用implementation依赖,因为api依赖会显著增加构建的时间

依赖冲突

冲突指的是:a中依赖1.0版本的z,而a中依赖的b依赖了2.0版本的z,依赖的有共同的模块,而且版本不一样。

Gradle默认解决方案:采用所有依赖版本中最高的版本作为最终的依赖。

当然,我们也可以手动选择其他方式:

  1. exclude排除方式:声明依赖的时候,如果同时exclude了某个依赖,意思是引入b的同时不引入采用b依赖的z。implementation('top.claws:b:1.0') { exclude group 'top.claws', module: 'z'}
  2. 禁止依赖传递:通过设置transitive(false)禁止一个依赖项引入他下面的其他依赖。这里的依赖传递和上面api依赖类型中提到的依赖传递不同,这里指的是不会同时依赖这个jar包的依赖。但是这种方式不建议使用。例如:implementation('top.claws:b:1.0') {transitive(false)}
  3. 强制使用某个版本:可以手动强制指定要使用的版本。在一个依赖的版本号上加双感叹号意思就是强制使用这个版本。例如implementation('top.claws:z:1.0!!')或者implementation('top.claws:z:1.0') {version { strictly('1.0')}}

另外,可以采用一些配置,在出现依赖冲突的时候报错提示:

configurations.all() {
    Configuration configuration -> configuration.resolutionStrategy.failOnVersionConflict()
}

如果没有指定具体版本,而是在版本号的地方写了加号+或者latest.integration,则会使用所有仓库中出现的最新版本号。这种称为动态版本声明。不建议使用。

Gradle 插件

插件可以给我们增加一些task,添加一些依赖配置,或者给项目拓展新的属性和方法,还可以对项目进行一些约定,例如可以约定项目默认的文件目录层次结构。

Gradle中的插件分类:

  • 脚本插件
  • 二进制插件(对象插件)
    • 内部插件
    • 第三方插件
    • 自定义插件

脚本插件

准确的说是一个gradle文件。

在当前的build.gradle中通过apply from: 加上插件脚本的本地文件地址、远程url就可以进行引入。

引入脚本插件后,相当于把脚本中的内容全部复制到了当前gradle文件中了,类似Cpp中的include宏。

例如有一个脚本插件,文件名为testext.gradle:

ext{
    company = 'claws ok company'
    spring = [version: 1.0]
}

就可以在其他文件中引入这个文件,然后使用文件中ext下定义的内容:

apply from: 'testext.gradle'

println spring.version

脚本文件最大的意义在于:为gradle构建脚本模块化创造了条件,可以用脚本插件来统一管理项目中用到的依赖版本。

内部插件

Gradle二进制插件都是实现了gradle 的Plugin接口的类编译成的产物。

内部插件官方文档地址:https://docs.gradle.org/current/userguide/plugin_reference.html

引入内部插件有两种方式:

  • 通过plugins DSL。一般情况下就是在脚本的plugins{} 下面直接加上依赖的id即可,例如plugins { id 'java' }
  • 通过apply方式。可以使用map和闭包两种方法。
    • apply(plugin:'java') map方式。这里plugin后面的值可以写:插件id,插件全类名,插件简类名。
    • apply { plugin 'java' } 闭包方式。这里plugin后面也是一样,可以写id,全类名,简类名。

第三方插件

有两种引入方法:

  • 传统方式:先添加仓库,引入依赖,然后apply应用插件
  • plugin DSL方式:直接使用 plugin DSL来引入插件。这种是新的方式,建议使用。

当我们在gradle插件社区中打开任意一个插件的时候,都会给出插件的引入方式,其中一般都会有两种引入方式,分别是使用plugins DSL和传统的引入方式。

image-20230509220421949

java 插件中如果想更改源码jdk版本和编译后字节码jdk版本,可以通过类似如下设置:sourceCompatibility(JavaVersion.VERSION_1_8)targetCompatibility(JavaVersion.VERSION_1_8)

用户自定义插件

用户自定义的插件也是二进制形式的插件。用户编写的插件类要实现Plugin接口。

之后可以用apply Plugin来应用插件。

以下是相关官方文档中提供的自定义插件示例:

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('hello') {
            doLast {
                println 'Hello from the GreetingPlugin'
            }
        }
    }
}

// Apply the plugin
apply plugin: GreetingPlugin

将这段代码粘贴到build.gradle文件中,就可以使用gradle hello命令来执行插件中的hello任务了。

另外,官方文档中还提供了更加复杂的自定义插件示例。可以用自定义插件对插件进行拓展,有两种方式:继承抽象类和实现接口。

buildSrc项目自定义插件

buildSrc是Gradle默认插件目录,放在项目中这个名字目录下面的代码就会被Gradle作为插件,可以在当前根项目中使用。

使用方式是创建一个buildSrc目录(模块),然后给这个特殊的模块的build.gradle进行特殊配置,让它适合于我们编写插件。注意:不需要在跟项目的settings.gradle中include这个buildSrc项目。

使用buildSrc项目编写插件有如下几个步骤:

  • 创建buildSrc目录

  • 创建buildSrc/build.gradle文件,内容如下:

    plugins {
        id 'groovy'
        id 'maven-publish'
    }
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
    }
    
    repositories {
        mavenCentral()
    }
    
    sourceSets {
        main {
            groovy {
                srcDir 'src/main/groovy'
            }
        }
    }
    
  • 创建插件代码文件buildSrc/src/main/groovy/top/claws/Text.groovy,内容如下

    package top.claws
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    class Text implements Plugin<Project> {
        @Override
        void apply(Project project) {
            project.task('gaga') {
                doLast {
                    println '这是自定义插件gaga'
                }
            }
        }
    }
    
  • 创建插件元数据文件buildSrc/src/mian/resources/META-INF/gradle-plugins/top.claws.plugin.properties,文件名就表示了以后使用插件的时候的名称。内容如下

    implementation-class=top.claws.Text
    
  • 引入插件apply plugin: 'top.claws.plugin'

之后就可以用gradle gaga来执行gaga任务了。gradle会自动编译buildSrc下面的插件代码。

自定义插件发布

自定义插件可以在build.gradle中使用maven-publish插件,将插件发布到maven仓库。在build.gradle文件中进行简单的信息说明以及仓库信息配置,就可以正常发布了。

发布之后,就可以在其他项目中使用。其他项目中使用时可以使用传统的引入方式,即指定maven仓库和依赖,然后通过apply plugin应用插件。

build.gradle文件

一个build.gradle对应一个Project对象,同时对应一个(子)项目。在build.gradle脚本中可以直接通过root引用跟项目对象,或者parent引用父项目对象。

常用属性和方法

build.gradle里面配置的都是Project的属性,而使用到的方法大多是Project类的方法。常用的可配置的属性有:

  • group name version。相当于maven项目中的三元组。根项目的name默认配置在settings.gradle中。
  • sourceCompability targetCompability 源码的JDK版本,以及编译的字节码的JDK版本。
  • compileJava.options.encoding 项目源代码中使用的字符集

而可以使用的常用的方法有:

  • task 配置任务
  • plugins 使用插件
  • dependencies 引入依赖
  • repositories 配置仓库
  • allprojects 声明所有项目(当前项目和所有子项目)的配置
  • subprojects 声明所有子项目的配置
  • 通过project(‘name’) 的形式,对某个名字或路径的子project进行设置。写在这个下面的内容,就和写在子项目中build.gradle文件中是基本等价的。这就是为什么前面说一个build.gradle对应一个Project对象。但是不支持plugins dls之类的一些语法。
  • buildscript apply 两个一起,传统的依赖和使用插件

运行gradle命令时,会创建task方法和tasks.create方法声明的所有task,而task的配置相关代码是在创建的时候就执行的。因此,如果将代码直接卸载task的配置阶段,不需要运行gradle task1,也可以看到这段配置代码的运行和输出。

ext拓展用户自定义属性

在build.gradle可以使用ext定义用户自定义属性,之后就可以在当前项目或者当前项目的子项目中使用到这些拓展属性的值。

下面展示了三种使用ext的方式。

// 多个属性
ext {
    phone = '110'
    ip = '127.0.0.1'
}

// 单个属性
ext.haha = 'foo'

task "task1" {
    // 任务内属性,仅限当前任务使用
    ext {
        age = 12
    }
    println "$age"
}

println "$phone, $ip, $haha"

除了task中的ext声明的属性,其他属性都可以在子project中使用。

gradle生命周期和钩子函数

image-20230514152251404

如图所示,gradle的生命周期分为三个阶段:

  • initialzation:初始化脚本
  • configuration:配置阶段,先配置根项目再配置子项目(不管从哪一个子项目的目录执行gradle命令,都会从根项目开始配置)。配置一个项目时,先执行beforeProject,之后执行build.gradle脚本中配置内容,并执行任务的配置(按照脚本从上到下执行,创建任务的时候就会执行任务配置)
  • execution:根据指令中的任务名执行具体的任务。

注意Configuration的配置代码执行顺序,是按照脚本编写顺序执行的。

例如:

println "checkpoint1"

task("task1") {
    println "checkpoint2"
}

println "checkpoint3"

task("task2") {
    println "checkpoint4"
}

println "checkpoint5"
// 输出顺序
// checkpoint1                                                                                                         
// checkpoint2                                                                                                          
// checkpoint3                                                                                                         
// checkpoint4                                                                                                         
// checkpoint5 

钩子函数:如上图,在gradle生命周期中,分布了多个钩子函数,这些钩子函数的实现可以替换成我们自定义实现,并且在特定的时间点会被执行。按照先后顺序,这些钩子函数有:

  1. gradle.settingsEvaluated
  2. gradle.projectsLoaded
  3. project.beforeEvaluate 要放在监听器中
  4. gradle.beforeProject
  5. project.afterEvaluate 要放在监听器中
  6. gradle.afterProject
  7. gradle.projectsEvaluated
  8. gradle.taskGraph.whenReady
  9. gradle.taskGraph.beforeTask 过时
  10. gradle.taskGraph.afterTask 过时
  11. gradle.buildFinished 过时

tips:可以借助钩子函数查看tasks的有向无环图:

在settings.gradle中添加以下代码

gradle.taskGraph.addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph taskExecutionGraph) {
        taskExecutionGraph.allTasks.forEach(task -> {
            taskExecutionGraph.allTasks.forEach(releaseTask -> {
                println "${releaseTask.getProject().name}: ${releaseTask.name}"
                })
            })
    }
})

项目发布

项目发布有三个步骤:

  1. 引入发布插件:maven-publish。可以添加到plugins {}下面,也可以使用apply plugin “maven-publish”的方式。如果要发布jar包,则要保证有java插件,如果想要发布war包,则还要有war插件。
  2. 在publishing下面配置仓库信息和发布信息。发布信息通过publications定义,而仓库信息通过repositories配置。
  3. 执行发布相关Task。具体Task有哪些可以在idea侧栏中publishing这个group下面看到。常用的有publish,将打包产物发布到上面配置的所有repository。另外,还会有具体的发布某一个包到某一个仓库的task可供选择,task名字的组成规则是publish<name>To<reponame>,采用驼峰命名。

使用Gradle命令行创建SpringBoot项目

因为学习Gradle最终是要为SpringBoot项目的创建、构建、部署服务的,所以这一节这也是Gradle使用指南的终极目标。

  1. 首先创建一个示例目录,名字为trySpringbootWithGradle.

  2. 进入目录,执行gradle init,选择basic类型,其他保持默认。

  3. 更改build.gradle,添加插件:

    plugins {
        id 'java'
        id "org.springframework.boot" version "3.0.6"
        id "io.spring.dependency-management" version "1.1.0"
    }
    

    这里dependency-management插件的作用是帮助我们管理spring相关依赖的版本号,有了这个插件,后面在指定spring相关依赖的时候就不需要我们手动指定版本号了,只需要指定group和name即可。

  4. 添加项目基本配置,group、版本和jdk版本:

    group = 'top.claws'
    sourceCompatibility = 17
    version = '1.0'
    
  5. 配置仓库(这里使用阿里云镜像):

    repositories {
        mavenLocal()
        maven {
          url 'https://maven.aliyun.com/repository/public/'
        }
        mavenCentral()
    }
    
  6. 配置依赖:

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    
  7. 使用junit5要添加:

    tasks.named('test') {
        useJUnitPlatform()
    }
    
  8. 创建源代码和测试代码目录,以及相应的基本Application类和测试类源代码文件:

    image-20230514163821565

    DemoApplication.java:

    package top.claws;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class DemoApplication {
    
        public static void main(String[] args) {
            System.out.println("hello, world!");
            SpringApplication.run(DemoApplication.class, args);
        }
    
    }
    

    DemoApplicationTests.java:

    package top.claws;
    
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    
    @SpringBootTest
    class DemoApplicationTests {
    
        @Test
        void contextLoads() {
            System.out.println("hello world from test!");
        }
    
    }
    
  9. 运行测试:gradle test,测试正常执行,并生成测试报告。

  10. 如果想运行application,使用gradle bootRun就可以运行了。如果想要gradle run也正常运行,则要添加application插件,之后添加mainClassName = 'top.claws.DemoApplication',设置好主启动类。bootRun任务应该是可以自动找到spring boot的启动类。

  11. 如果想要打包成可执行jar包,直接使用gradle bootJar命令即可,会在build/libs下面生成jar包。

以上就是手动创建SpringBoot项目的步骤。熟练了之后可以使用Spring脚手架创建项目:Spring官方脚手架 或者 阿里云脚手架

Kotlin-Gradle

虽然Groovy语法是Gradle诞生使用的语法,现在很多在推崇使用Kotlin。这里提供一份参考,关于如何从Groovy语法切换到Kotlin语法。