Theme Preview

Hue:

You are using an outdated browser that does not support OKLCH colors. The color setting will not take effect.

Gradle之旅(2):Gradle结构概述

4686 字

一、安装环境

Gradle 是一种开源自动化构建工具,支持多语言环境,吸收 Ant、Maven 相关思想,同时规避 Ant 的不规范和 Maven 的复杂配置方式以及多种限制的生命周期,并且支持 DSL(领域特定语言,如Groovy)编写构建,这使得脚本更加灵活精悍。

本文以 Gradle 7 为例,需要如下SDK:

  1. JDK8及以上
  2. Gradle

JDK 的安装本文不再叙述; 这里要说明的是官网下载的 Gradle 有不同的后缀,其内容也有所不同:

  • all.zip : 完整版,包含可执行程序、源代码文件和离线文档
  • bin.zip : 可执行程序版,仅包含可执行程序
  • src.zip : 源码版,仅包含了 Gradle 的源码,不能用来编译工程

因此如果只是为了编译工程,可以选可执行程序版本这样就少了几十兆大小,如 gradle-7.2-bin.zip

接下来将下载好的压缩包放在自定义的目录下并解压,将其中的 bin 目录在 bashrc 中配置环境变量即可:

# vim ~/.bashrc

export PATH=$PATH:$HOME/.gradle/wrapper/dists/gradle-7.2-bin/gradle-7.2/bin

# 保存关闭 bashrc 文件之后执行如下语句使其生效
source ~/.bashrc

配置完成之后,再终端上验证一下看是否成功:

$ gradle -v

------------------------------------------------------------
Gradle 7.2
------------------------------------------------------------

Build time: 2021-08-17 09:59:03 UTC
Revision: a773786b58bb28710e3dc96c4d1a7063628952ad

Kotlin: 1.5.21
Groovy: 3.0.8
Ant: Apache Ant(TM) version 1.10.9 compiled on September 27 2020
JVM: 17.0.10 (Private Build 17.0.10+7-Ubuntu-122.04.1)
OS: Linux 6.5.0-35-generic amd64

二、创建 Gradle 项目

Gradle 环境安装好之后,接下来创建一个 Gradle 项目演示其他的内容,Gradle 中提供了初始化项目目录的命令 init,这使得我们不用手工来创建 build.gradle 文件和其他的源代码目录:

# 创建一个空目录 gradleTest,进入 gradleTest 目录后执行init指令
$ gradle init

这里需要说明的是不同的 Gradle 所能支持创建的项目类型都有所不同,这里采用 Gradle 7 有如下四种类型:

  1. basic : 缺省值,仅仅创建简单的 settings 和 build 文件
  2. application : 构建 Java 应用程序
  3. library : 构建 Java 库
  4. Gradle plugin : 构建 Gradle 插件

这样跟着提示,依次配置项目类型、使用的语言、脚本语言、包名等,这里我以 basic 为例:

$ gradle init

Select type of project to generate:
1: basic
2: application
3: library
4: Gradle plugin
Enter selection (default: basic) [1..4] 1

Select build script DSL:
1: Groovy
2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Project name (default: gradleTest):

> Task :init
Get more help with your project: Learn more about Gradle by exploring our samples at https://docs.gradle.org/7.2/samples

BUILD SUCCESSFUL in 1m 55s
2 actionable tasks: 2 executed

这样就得到如下的内容:

build.gradle  gradle  gradlew  gradlew.bat  settings.gradle

三、Gradle 结构类型简介

在学习怎样使用 Gradle 脚本之前,我们先对 Gradle 的重要结构及类型有一个简要了解,下图是根据 Gradle 7.2 版本的 官网 介绍总结:

在上图中看似分了很多内容,其实主要划分为两类:构建脚本模块对象类型; 构建脚本模块中包含不同的块,每个块都有其特定的用途和上下文。这些块定义了构建过程中不同阶段的行为和配置;至于图中的核心类型、发布类型、容器类型等都是 Gradle 中定义的一个个对象,都包含各自的成员变量或作属性、函数、脚本块等,这些对象根据各自的功能划分为一类。接下来对图中几个最重要也是最常用的核心对象类型来做说明。

3.1 Gradle 对象类型

Gradle 对象是在项目初始化时构建,全局单例,只有一个对象,按照主要功能划分如下,更多见官网Gradle

3.2 Project 对象类型

Project 是与 Gradle 交互的主接口。它提供了 Gradle 各个能力的入口。android 开发中最为我们所熟悉的就是 build.gradle 文件,这个文件与 Project 是一对一的关系,build.gradle 文件是 project 对象的委托。Gradle 构建进程启动的时候会根据 build.gradle 去实例化 Project 类。即构建时,每个 build.gradle 文件会生成一个 Project 对象,这个对象负责当前 module 的构建,主要内容如下所示,更多见官网Project;

3.3 Settings 对象类型

因为 Gradle 的初始化阶段和 settings.gradle 文件息息相关,而 settings.gradle 文件又和 Settings 对象一一对应。Settings 接口类的定义如下,更多见官网Settings

3.4 Task 对象类型

任务gradle 的最小执行单元,一个 build.gradle 是由一系列的 Task 组成,Task 可以使用 TaskContainer 类的各种方法来创建和查找 task 实例(可以是 Gradle 内置类的实例,也可以是我们自定义 task 类的实例),其主要核心内容如下图所示,更多见官网Task

3.4.1 Task创建

上面介绍了 Task 的核心内容,那么接下来我们来介绍下如何创建一个 task,创建Task的方式有很多种,但 DefaultTask 是所有任务的基类,因此在创建 Task 时,默认继承 DefaultTask :

//1. 直接声明
task taskOne{
println "method1"
}

//2. 参数定义名称
task('taskTwo'){
println "method2"
}

//3. 使用tasks集合属性,引用了TaskContainer的一个实例对象。还可以使用Project.getTasks()
tasks.create('taskThree'){
println "method3"
}

//4. 使用with
tasks.with(){
println "methodContainer"
}

//5. 使用withType声明
tasks.withType(Test){
print "method5"
}
//6. 使用register,register执行的是延迟创建。也就是说只有当task被需要使用的时候才会被创建。
tasks.register('taskSix'){
println "method6"
}

//7. 带参数任务:定义一个名字为paramTask的task,属于zsk分组,并且依赖myTask1和myTask2两个task
project.task('paramTask', group: "zsk", description: "我自己的Task", dependsOn: ["myTask1", "myTask2"] ).doLast {
println "execute paramTask"
}
// 或者采用闭包的方式
task zskTask {
group "zsk"
description "我自己的Task"
dependsOn myTask1, myTask2
doLast {
println "execute zsk"
}
}

//8. 指定Type
//通过 type: SomeType 指定Type,Type其实就是告诉Gradle,这个新建的Task对象会从哪个基类Task派生,类似的还有Delete、Exec、Zip等
task copyDocs(type: Copy) {
from 'src/main/doc'
into 'build/target/doc'
}


3.4.2 Task依赖
task之间的依赖关系是通过 task name 来决定的,具体可以分为同项目依赖和跨项目依赖:

task hello { 
doLast { println 'Hello zsk!' }
}
task intro {
dependsOn hello
doLast { println "I'm lili" }
}

跨项目进行task的依赖,如果是跨项目的task依赖的话,需要指定task的路径:

project('zsk-service') { 
task taskX {
dependsOn ':zsk-dao:taskY'
doLast { println 'service' }
}
}

project('zsk-dao') {
task taskY {
doLast { println 'dao' }
}
}

3.4.3 条件执行

我们可以给task一些描述信息,这样我们在执行gradle tasks的时候,就可以查看到:

有时候我们需要根据build文件中的某些属性来判断是否执行特定的task,我们可以使用onlyIf :

task hello { 
doLast { println 'zsk' }
}
hello.onlyIf { !project.hasProperty('skipHello') }

或者我们可以抛出StopExecutionException异常,如果遇到这个异常,那么task后面的任务将不会被执行:

task compile { 
doLast { println 'We are doing the compile.' }
}

compile.doFirst {
if (true) {
throw new StopExecutionException()
}
}

task myTask {
dependsOn('compile')
doLast {
println 'I am not affected'
}
}

我们还可以启动和禁用task:

myTask.enabled = false

3.4.4 任务规则

如果我们想要给某些task定义一些规则,那么可以使用tasks.addRule:

//定义了如下rule,如果taskName是以ping开头的话,那么将会输出对应的内容。
tasks.addRule("Pattern: ping<ID>") {
String taskName ->
if (taskName.startsWith("ping")) {
task(taskName) {
doLast { println "Pinging: " + (taskName - 'ping') }
}
}
}

3.4.5 任务分组

task分组,我们可以通过 task.group 属性来指定task的分组,没有指明的分组,会默认为 other,这具体可以通过如下命令查看:

gradle tasks --all

四、扩展属性

Gradle 中的属性分为内置属性和扩展属性,接下来使用一些示例来展示这些属性的用法及特性:

① 内置属性可以直接赋值,无需声明

group = 'com.zsk'
version = '1.0.0'

② 自定义属性可以使用 groovy 语法,也可以与 Java 语法结合

//groovy定义属性
def pname = "projectName:" + project.name
//java类型接收
String pname = "projectName:" + project.name

4.1 扩展属性及使用

扩展属性使用 ext 来定义:

ext.prop1 = "prop1"
ext.prop2 = "prop2"
ext {
prop3 = "prop3"
prop4 = "prop4"
prop5 = "prop5"
}
println("① : "+prop1)
println("② : "+ext.prop2)
println("③ : "+prop3)
println("④ : "+project.ext.prop4)
println("⑤ : "+project.property("prop5"))

五、Gradle 的生命周期

Gradle 的核心是一种基于依赖的变成语言,任务与任务之间有一定的依赖关系,并且每个任务只会执行一次。在构建时,Gradle 会将这些任务串联起来形成有向无环图。那么 Gradle 实在什么时候进行串联的呢?这就需要充分了解 Gradle 在各个阶段做了什么事情了,这一过程我们称为生命周期。官网介绍如下:

从上图中可以看到,Gradle 的生命周期分为三个阶段:

1. 初始化阶段: Gradle 支持单项目和多项目构建,在此阶段,Gradle 会解析 setting.gradle 文件,确定哪些项目需要参与构建,并为这些项目创建对应的 Project 实例。

2. 配置阶段: 当完成初始化阶段后,Gradle 会进入配置阶段。配置阶段会解析所有 project 中的 build.gradle 文件获取所有的 task,形成有向无环图后执行依赖关系,并且所有 project 中的build script 部分和 task 的配置阶段都会在这一阶段调用

3. 执行阶段: 当完成任务依赖图之后,Gradle 就做好了一切准备,然后进入执行阶段。按照有向无环图中的 task 列表的顺序,执行所有被指定的 task。

Gradle 在生命周期三个阶段都设置了相应的钩子函数调用:

5.1 初始化阶段

//settings.gradle
gradle.beforeSettings {
//Gradle.buildStarted()在6.0中弃用,7.0中彻底删除,采用beforeSettings替代
println '在settings加载之前执行,但是这个函数又放在了settings中,所以不会执行'
}
gradle.settingsEvaluated {
println 'settings脚本执行完成之后调用'
}
//每个module的build.gradle执行之前都会调用,闭包会传入当前的project对象作为参数
gradle.projectsLoaded {
println "所有的project对象加载完成"
}

//上述的写法也可以采用如下方式书写
gradle.addBuildListener(new BuildListener() {
@Override
void beforeSettings(Settings settings) {
super.beforeSettings(settings)
}

@Override
void settingsEvaluated(Settings settings) {}

@Override
void projectsLoaded(Gradle gradle) {}
})

5.2 配置阶段

//settings.gradle
//每个module的build.gradle执行之前都会调用,闭包会传入当前的project对象作为参数
//注意:如果在build.gradle中书写project.beforeProject闭包并不会执行(子模块的情况另说),参照gradle.beforeSettings函数
gradle.beforeProject{ project ->
println 'beforeProject $project'
}
gradle.afterProject{ project ->
println 'afterProject $project'
}

//所有module的build.gradle脚本执行完成之后,表示build.gradle执行完成
gradle.projectsEvaluated {
println 'projectsEvaluated'
}

// task有向无环图构建完成,配置阶段完成,TaskExecutionGraph对象作为对象传入闭包
gradle.taskGraph.whenReady { graph ->
println 'whenReady'
}
// 同gradle.taskGraph.whenReady
gradle.addListener(new TaskExecutionGraphListener() {
@Override
void graphPopulated(TaskExecutionGraph taskExecutionGraph) {
println "TaskExecutionGraphListener.graphPopulated"
}
})
//project.beforeEvaluate的执行时机
public class DemoPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.beforeEvaluate {
println("beforeEvaluate")
}
}
}
//如果这个DemoPlugin是被写在`build.gradle`文件里,像这样`apply plugin: 'demoPlugin'`,`beforeEvaluate()`方法不会执行,而如果是在rootPlugin里,调用subProject的`apply plugin 'demoPlugin'`,则`beforeEvaluate()`会被执行。原因是`beforeEvaluate()`这个方法是在解析`build.gradle`文件之前执行,在`build.gradle`中才注册`beforeEvaluate()`监听方法,已经晚了。

基于此,我们想在解析`build.gradle`之前,改变`build.gradle`文件的内容,可以在
beforeEvaluate() 这个方法里修改`build.gradle`。

5.3 执行阶段

//settings.gradle
gradle.taskGraph.beforeTask {
//7.3已过时,8.0中将删除
}

gradle.taskGraph.afterTask {
//7.3已过时,8.0中将删除
}

//或者如下写法
gradle.addListener(new TaskActionListener() {
@Override
void beforeActions(Task task) {
}
@Override
void afterActions(Task task) {
}
})

六、Gradle 命令行

与 Gradle 交互有两种方式,一是命令行界面(也即是指令形式),二是IDE的图形界面,如Android Studio,IDE方式固然使用比较方便,可以一则是未必包含所有的指令,其次是在类似 CI/CD 的场景下就不如命令行形式方便,因此掌握基本的命令行指令是非常有必要的。

Gradle 的指令形式定义如下:

gradle [taskName...] [--option-name...]
  • 任务名(taskName)有多个时,使用空格分开,如gradle task1 task2
  • 在多项目工程中,执行某个项目的任务时,可以用“:”将项目名添加到任务名之前,如
$ gradle projectName:taskName
$ gradle :projectName:taskName
  • 可选项(option-name)如果接收参数,建议使用=拼接,如:
$ gradle task1 --console=plain
  • 可选项规定一个行为时,可使用–no-作为其全称(long-form)前缀,来指定它的对立行为。如:
--build-cache
--no-build-cache
  • 可选项的全称名称常有简写形式 ,如:
--help
-h

6.1 内建任务指令

Gradle 中内置了一部分任务指令,用于简化构建过程,以下只列举了部分常用指令,更多参见 官网内建任务选项

  • gradle build。生成所有的输出,并执行所有的检查。
  • gradle run。生成应用程序并执行某些脚本或二进制文件
  • gradle check。执行所有检测类任务如tests、linting等
  • gradle clean。删除build文件目录。
  • gradle projects。查看项目结构。
  • gradle tasks。查看任务列表。查看某个任务详细信息,可用gradle help –task someTask
  • gradle dependencies。查看依赖列表。
  • gradle 任务 –scan。生成一个包含构建执行过程的详细信息,如依赖解析、插件使用、任务执行时间等。并将这个数据发送到Gradle的服务器

6.2 调试类指令

以下仅列举部分,更多详见 官网调试类选项

  • -?, -h, –help。查看帮助信息。
  • -v, –version。查看版本信息。
  • -s, –stacktrace。执行任务时,打印栈信息。如gradle build –s

6.3 日志类指令

以下仅列举部分,更多详见 官网日志类选项

  • -q, –quiet。只打印errors类信息。
  • -i, –info。打印详细的信息。

6.4 自定义指令

Gradle 也支持扩展自定义指令,详见 Command Line options

这里需要使用 Option注释,定义格式如下:

@Option(option = "flag", description = "Sets the flag")

举例来说,自定义任务 UrlVerify 通过进行 HTTP 调用并检查响应代码来验证是否可以解析 URL。要验证的 URL 可通过属性进行配置,属性的 setter 方法使用@Option进行注释:

import org.gradle.api.tasks.options.Option;

public class UrlVerify extends DefaultTask {
private String url;

@Option(option = "url", description = "Configures the URL to be verified.")
public void setUrl(String url) {
this.url = url;
}

@Input
public String getUrl() {
return url;
}

@TaskAction
public void verify() {
getLogger().quiet("Verifying URL '{}'", url);
// verify URL by making a HTTP call
}
}

创建一个 UrlVerify 实例:

tasks.register('verifyUrl', UrlVerify)

执行 verifyUrl 指令:

$ gradle -q verifyUrl --url=http://www.google.com/
验证 URL 'http://www.google.com/'

七、Gradle Wrapper

Gradle Wrapper 称为Gradle包装器,是对Gradle的一层包装,所有使用 gradle 指令的地方都可以使用 Gradle Wrapper 来替换。之所以使用 Gradle Wrapper,原因有如下几点:

  1. 保持版本一致:确保开发者在构建项目使用相同版本的Gradle,避免版本差异
  2. 简化安装:对于未安装 Gradle 的开发者,使用 Gradle Wrapper 能在需要时自动下载和配置指定版本的 Gradle 环境
  3. 便于集成:Gradle Wrapper 非常适合作为 CI/CD 流程中的一部分,因为它可以确保构建环境与开发环境一致
  4. 简化项目管理:Gradle Wrapper 相关的文件和配置可以随项目一同提交到版本控制系统,方便团队共享
  5. 提高开发效率:对于新加入项目的开发者,无需担心 Gradle 环境问题,只需执行 Wrapper 提供的脚本即可快速开始构建项目

7.1 构建Gradle Wrapper

在确保计算机中已经配置好 Gradle 环境后,执行如下命令即可生成 Gradle Wrapper 相关的文件和配置:

$ gradle wrapper
> Task :wrapper

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

这会在项目根目录下生成如下文件:

├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat

每个文件的含义如下:

  • gradle-wrapper.jar :包含Gradle运行时的逻辑代码。
  • gradle-wrapper.properties :负责配置包装器运行时行为的属性文件,用来配置使用哪个版本的Gradle等属性。
  • gradlew:Linux平台下,用于执行Gralde命令的包装器脚本。
  • gradlew.bat:Windows平台下,用于执行Gralde命令的包装器脚本。

当生成好了上面的这些目录与文件后,用户就可以将工程push到远程,当其他用户clone下来后就可以直接进行项目的构建,节省了用户单独下载Gradle的时间,并且可以确保Gradle版本的一致。

7.2 配置Gradle Wrapper

gradle-wrapper.properties 是 Gradle Wrapper的属性文件,用来配置 Gradle Wrapper,Gradle 7.2 版本对应的gradle-wrapper.properties 如下所示

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

字段的含义如下:

  • distributionBase:Gradle解包后存储的主目录。
  • distributionPath:distributionBase指定目录的子目录。distributionBase+distributionPath就是Gradle解包后的存放位置。
  • distributionUrl:Gradle发行版压缩包的下载地址。
  • zipStoreBase:Gradle压缩包存储主目录。
  • zipStorePath:zipStoreBase指定目录的子目录。zipStoreBase+zipStorePath就是Gradle压缩包的存放位置。

这里我们最需要关注的是distributionUrl这个字段,如果官方的地址下载不了或者缓慢,可以将这个地址换为其他的镜像地址,或者干脆把Gradle发行版压缩包放在服务器上以供下载。

//