Gradle 基础
环境准备
Gradle分为三种版本:
sources:只包含源代码,不可运行
bin:可运行的二进制
all:除了可运行的二进制,还包括文档、源码等
各版本下载路径:https://services.gradle.org/distributions/,完成下载后,配置环境变量,执行gradle -v检查环境配置是否成功。
第一个Gradle脚本
创建build.gradle文件,添加如下内容
task hello {
println 'Hello world!'
}
在命令执行gradle -q hello,成功打印Hello world!
注意,这里-q表示quiet,是可选的,添加该参数禁止输出gradle自身的日志,这样输出的就只有我们要打印的内容,输出更加清爽。
什么是Gradle包装器 —— Wrapper ?
当你把你的项目给你的好友小明看时,他觉得可以给你的项目添加一些新的功能。于是你把代码添加到版本管理工具(Git)中,小明通过Git下载代码,由于小明从来没有用过Gradle构建工具,所以他不知道你使用的哪个版本的Gradle以及怎么安装Gradle,他也不知道怎么去配置Gradle,从以往的经验来看,小明清醒的知道不同版本的构建工具或者运行环境对构建结果影响有多大。对于在一台机器上可以运行,而在另一台机器上无法运行的情况太多了,经常由于运行时环境不兼容的原因,导致构建失败。
对于这个问题Gradle提供了一个非常方便和实用的解决方法:Gradle包装器!包装器是Gradle的一个核心特性,它能在你还没有安装Gradle时,执行一部分Gradle脚本,并且它会从中央仓库中自动下载配置中指定版本的Gradle运行时,解压到你的文件系统用于构建项目。其终极目标就是创建可靠的、可复用的、与操作系统、系统配置或Gradle版本无关的构建。
生成Gradle包装器
简单生成,只需要执行命令gradle wrapper,即在工程根目录下生成了gradle文件夹、gradlew和gradlew.bat。

但我们需要自定义一些属性时,只需要在build.gradle文件中添加一个名为wrapper的 task,然后再执行命令gradle wrapper即可
task wrapper(type: Wrapper){
gradleVersion='6.5'
distributionBase='GRADLE_USER_HOME'
distributionPath='wrapper/dists'
zipStoreBase='GRADLE_USER_HOME'
zipStorePath='wrapper/dists'
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
}
我们可以查看或修改包装器的配置文件:gradle\wrapper\gradle-wrapper.properties
注意,GRADLE_USER_HOME变量是指Gradle的下载目录,这个目录通常是用户目录下的.gradle文件夹,在我的Windows系统下,它是C:\Users\Administrator\.gradle;Linux/Mac 下,就是~/.gradle
这里,关于Gradle包装器的一些具体属性,可以查看其官网文档 Wrapper,或本地离线文档docs/dsl/org.gradle.api.tasks.wrapper.Wrapper.html

创建工程
Gradle 官方已经创建了一些插件,用于生成或管理相应的工程项目。这些插件相当于脚手架,帮我们直接生成模版工程,而不是从零开始手动的创建一个工程,当我们需要自动创建工程时,只需要新建一个工程文件夹,然后在该文件夹路径下执行gradle init命令即可生成模版工程,然后可以通过执行gradle tasks来查看当前有哪些任务可以执行
Gradle 的API
每个Gradle构建都包括三个基本的构建块:项目(projects)、任务(tasks)和属性(properties),每个构建至少包括一个项目,项目包括一个或者多个任务,项目和任务都有很多个属性来控制构建过程。Gradle的项目和任务都在Gradle的API中有一个直接的class来表示。
Project
每个Gradle脚本至少定义了一个项目。当开始构建过程后,Gradle基于你的配置实例化org.gradle.api.Project这个类以及让这个项目通过project变量来隐式的获得。下图列出了API接口和最重要的方法

Task
定义Task
// 快速定义
task hello {
println 'Hello world!'
}
// 定义带有action的任务
task t1{
// 在任务调用前执行的action
doFirst{
println 't1 doFirst'
}
// 在任务调用后执行的action
doLast{
println 't1 doLast'
}
}
// “<<” 是 doLast 的简写,直接定义了任务调用后执行动作
task t2 << {
println 't2'
}
Task有两个重要概念:任务动作(actions)和任务依赖
一个动作就是任务执行时最小的工作单元,这可以简单到只打印hello world,也可以复杂到编译源代码。很多时候一个任务需要在另一个任务之后执行,尤其是当一个任务的输入依赖于另一个任务的输出时,比如项目打包成JAR文件之前先要编译成class文件。
Gradle API中Task的表示:org.gradle.api.Task 接口:

注意,我们可以使用dependsOn来声明一个任务依赖于一个或者多个任务
// 声明一个依赖
task run(dependsOn: build){
println "run..."
}
// 声明多个依赖
task print(dependsOn: [second, first]) << {
logger.quiet "Version: $version"
}
属性
每个Project和Task实例都提供了setter和getter方法来访问属性,属性可以是任务的描述或者项目的版本号,很多时候我们需要定义自己的属性,比如,你想定义一个变量来引用你在构建脚本中多次使用的一个文件,Gradle允许你通过外部属性来定义自己的变量。
Gradle中通常有两种方式来定义外部属性:
直接在脚本文件中声明
// 声明单个 project.ext.myProp = 'myValue' // 一次声明多个 ext { someProp = 1001 filePath = "src/command.txt" } // 访问自定义的属性 task printProperty << { println "property: $myProp" println "property: $someProp" }在属性文件中声明
在项目根目录下创建
gradle.properties文件,如下:someProp = 1001 filePath = "src/command.txt"
Settings
settings文件用来表示项目的层次结构,默认的settings文件被命名为settings.gradle,并且应和根目录下的build.gradle文件放在一起。对于你想添加的每一个子项目,调用include方法来添加:
// 参数是项目路径,不是文件路径
include 'app', 'web'
上例中的项目路径是相对于工程根目录的,我们还可以使用更深层次的目录结构,但注意使用冒号“:”来分隔每一个子目录层次结构。例如你想要表示model/todo/items这个目录,则可以这样表示model:todo:items。
在Gradle开始执行构建之前,它会创建一个Settings类型的实例,换句话说setting.gradle 是每个gradle开始的入口,即初始化阶段。Settings接口直接用来映射settings文件,它的主要作用是添加Project实例参与多项目构建。除了组装多项目构建之外,你可以在脚本中做任何事情,因为你可以直接访问Gradle和Project接口。
下图是你可以访问的Gradle API:

之前介绍过Gradle有三个生命周期,Initialization->Configuration->Execution,Settings处于Initialization阶段,Gradle自动找出一个子项目是否处在一个多项目构建中。

多项目构建
Gradle允许你从根目录或者任何子目录中运行构建任务,只要它包含一个build脚本,Gradle怎么知道一个子项目是不是一个多项目构建的一部分呢?他需要查找settings文件,Gradle通过两步来查找settings文件:
Gradle查找和当前目录具有相同嵌套级别的master目录下的settings文件

如果第一步没有找到settings文件,Gradle从当前目录开始逐步查找父目录

Project 还有一些API用在多项目构建中:

project方法用于声明指定项目的构建代码,需要提供项目的路径,比如:model。有时候你想给所有的项目或者只有子项目定义一些逻辑,你可以使用allprojects和subprojects方法,比如你想给所有的子项目添加Java插件,你需要使用subprojects方法。
自定义脚本
Gradle构建脚本的标准名称是build.gradle,在一个多项目构建的环境中,你可能想自定义你的构建脚本名称,以避免名称混淆的情况,在settings文件中添加逻辑:
// 通过目录来添加子项目
include 'todo-model', 'todo-repository', 'todo-web'
// 设置根项目的名字
rootProject.name = 'todo'
// 迭代访问所有根目录下的子项目,设置自定义的构建脚本名称
rootProject.children.each {
// 使用子项目名 + 文件拓展名.gradle 然后去除前缀'todo-' 来设置脚本文件名
it.buildFileName = it.name + '.gradle' - 'todo-'
}
生命周期监听
作为一个构建脚本的开发者,你不应该局限于编写任务动作或者配置逻辑,有时候你想在指定的生命周期事件发生的时候执行一段代码。生命周期事件可以在指定的生命周期之前、之中或者之后发生,在执行阶段之后发生的生命周期事件就该是构建的完成了。
假设你希望在构建失败时能够在开发阶段尽早得到反馈,给构建生命周期事件添加回调有两种方法:一是通过闭包,二是实现Gradle API的一个监听接口

这里介绍一下通过闭包监听的方法,
在setting.gradle添加代码:
rootProject.name = 'Test'
// 我们知道setting.gradle 肯定会初始化之前执行,因此只需在该脚本中写逻辑即可
println("初始化阶段开始")
在build.gradle中添加代码:
plugins {
id 'groovy'
}
group 'Test'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
compile 'org.codehaus.groovy:groovy-all:2.3.11'
}
//--------------------------- 添加回调 ----------------------------
this.beforeEvaluate {
println("配置阶段开始前")
}
this.afterEvaluate {
println("配置阶段完成后")
}
this.gradle.buildFinished {
println("执行阶段执行完毕")
}
依赖管理
在Java领域里支持声明的自动依赖管理的有两个项目:
Apache Ivy:Ant项目用的比较多的依赖管理器
Maven:在构建框架中包含一个依赖管理器
Ivy和Maven是通过XML描述文件来表达依赖配置,配置包含两部分:依赖的标识加版本号和中央仓库的位置(可以是一个HTTP链接),依赖管理器根据这个信息自动定位到需要下载的仓库然后下载到你的机器中。库可以定义传递依赖,依赖管理器足够智能,分析这个信息然后解析下载传递依赖。如果出现了依赖冲突,依赖管理器会试着解决。库一旦被下载就会存储在本地的缓存中,构建系统先检查本地缓存中是否存在需要的库然后再从远程仓库中下载。下图显示了依赖管理的关键元素:

配置仓库
Gradle 强调的是对现有仓库的支持,因此在gradle中可以配置一个任意的Maven或Apache Ivy仓库URL。
Gradle仓库类型:
| 类型 | 描述 |
|---|---|
| Maven仓库 | 本地文件系统或远程服务器中的Maven仓库,或者预配置的Maven Central |
| Ivy 仓库 | 本地文件系统或远程服务器中的Ivy仓库,具有特定的结构模式 |
| 扁平的目录仓库 | 本地文件系统中的仓库,没有元数据支持 |
配置不同仓库对应的Gradle API:

Maven仓库是Java项目中使用最为广泛的一个仓库,库文件一般是以JAR文件的形式存在,用XML(POM文件)来描述库的元数据和它的传递依赖。所有的库文件都存储在仓库的指定位置,当你在构建脚本中声明了依赖时,这些属性用来找到库文件在仓库中的准确位置。group属性标识了Maven仓库中的一个子目录,下图展示了Cargo依赖属性是怎么对应到仓库中的文件的:

Maven Central 是构建中经常使用的仓库,Gradle想让它尽可能容易的被使用,因此提供了一种快捷方式来声明,而不必每次都定义URL :http://repol.maven.org/maven2,我们可以直接调用mavenCentral()方法:
repositories {
mavenCentral()
}
需要注意,Gradle早期使用mavenCentral(),后来切换到jcenter()。那么mavenCentral和jcenter有什么区别呢?
maven 中央仓库(http://repo1.maven.org/maven2/)是由Sonatype公司提供的服务,而jcenter(https://jcenter.bintray.com)是由JFrog公司提供的仓库,两者都是Maven类型仓库,只是提供的服务商不同。但是JCenter具有更多优势,它是当前世界上最大的Java和Android开源软件构件仓库。 其所有内容都通过内容分发网络(CDN)使用加密Https连接获取。JCenter相比mavenCenter性能更好。但还是有某些库仅存在mavenCenter中,因此有些脚本中会同时配置它们两个。
关于库搜索
当我们需要查询某个库的最新版本号,或者了解它的一些发布信息,可以去仓库的搜索页面检索:
- maven 中央仓库提供的搜索页面(可能需要科学上网):https://search.maven.org/
- JCenter 提供的搜索页面:https://bintray.com/
- 第三方提供的库检索工具:https://mvnrepository.com/
添加自定义Maven仓库
如果指定的依赖不存在于Maven仓库或者你想通过建立自己的企业仓库来确保可靠性,你可以使用自定义的仓库。仓库管理器允许你使用Maven布局来配置一个仓库。Gradle的API提供两种方法配置自定义的仓库:maven()和mavenRepo()
repositories {
mavenCentral()
maven {
name 'Custom Maven Repository',
url 'http://repository.forge.cloudbees.com/release/')
}
}
这段代码添加了一个自定义的仓库,如果Maven仓库中不存在相应的库就会从自定义仓库中查找。
声明依赖
DSL配置块 dependencies用来给配置添加一个或多个依赖。外部依赖并不是为项目声明的唯一的依赖,下面这张表显示了Gradle支持的各种不同类型的依赖:
| 类型 | 描述 |
|---|---|
| 外部模块依赖 | 依赖仓库中的外部类库,包括它所提供的元数据 |
| 项目依赖 | 依赖其他Gradle项目 |
| 文件依赖 | 依赖文件系统中的一系列文件 |
| 客户端模块依赖 | 依赖仓库中的外部类库,具有声明元数据的能力 |
| Gradle运行时依赖 | 依赖Gradle API或封装Gradle运行时的类库 |
每个Gradle项目都有一个DependencyHandler的实例,你可以通过getDependencies()方法来获取依赖处理器的引用,上表中每一种依赖类型在依赖处理器中都有一个相对应的方法。每一个依赖都是Dependency的一个实例,group, name, version, 和classifier这几个属性用来标识一个依赖,下图清晰的表示了项目(Project)、依赖处理器(DependencyHandler)和依赖三者之间的关系:

外部模块依赖
在Gradle的术语里,外部库通常是以JAR文件的形式存在,称之为外部模块依赖,代表项目层次外的一个模块,这种类型的依赖是通过属性来唯一的标识。
依赖属性
当依赖管理器从仓库中查找依赖时,需要通过属性的结合来定位,最少需要提供一个name。
- group: 用来标识一个组织、公司或者项目,可以用点号分隔,Hibernate的group是org.hibernate
- name: 唯一的描述了这个依赖,hibernate的核心库名称是hibernate-core
- version: 一个库可以有很多个版本,通常会包含一个主版本号和次版本号,比如Hibernate核心库3.6.3-Final
- classifier: 有时候需要另外一个属性来进一步的说明,比如说明运行时的环境,Hibernate核心库没有提供classifier
依赖的写法
使用下面的语法在项目中声明依赖:
dependencies {
configurationName dependencyNotation1, dependencyNotation2, ...
}
先声明你要给哪个配置添加依赖,然后添加依赖列表,你可以用map的形式来注明,也可以直接用冒号来分隔属性,例如:

// 声明外部属性
ext.cargoGroup = 'org.codehaus.cargo'
ext.cargoVersion = '1.3.1'
dependencies {
// 使用映射声明依赖
compile group: cargoGroup, name: 'cargo-core-uberjar',version: cargoVersion
// 用快捷方式来声明,引用了前面定义的外部属性
cargo "$cargoGroup:cargo-ant:$cargoVersion"
}
动态版本声明
如果你想使用一个依赖的最新版本,你可以使用latest.integration,比如声明 Cargo Ant tasks的最新版本,你可以这样写 org.codehaus .cargo:cargo-ant:latest-integration,你也可以用一个+号来动态的声明:
dependencies {
// 依赖最新的1.x版本
cargo 'org.codehaus.cargo:cargo-ant:1.+'
}
离线模式和强制刷新
在暂时无法联网的情况下,可以通过命令行选项参数--offset使用离线模式,不检查远程仓库,只使用本地缓存中的依赖,当然前提是缓存中确实存在依赖的库,否则构建失败。
当使用了动态版本的依赖声明时,一旦获取了依赖,它们会被缓存24小时,这样可以使得构建更快捷,从而避免每次都去检查远程仓库的版本是否更新。当我们需要手动刷新缓存时,可以使用命令行选项--refresh-dependencies手动强制刷新。
配置插件
Gradle 插件分为两种:
二进制插件
实现了
org.gradle.api.Plugin接口的插件,它们可以有plugin id脚本插件
仅仅只是一个
.gradle脚本文件,应用这个插件就是把这个脚本给加载进来。这种脚本通常用来配置公共属性。
应用二进制插件的两种语法
// 方式一
apply plugin: 'java-library'
apply plugin: 'eclipse'
apply plugin: 'maven'
apply plugin: 'java'
如上例,'java'是一个plugin id,它是唯一的。Gradle自带的核心插件都有一个容易记忆的短名称。
// 方式二
plugins {
id 'java'
id 'org.hidetake.ssh' version '2.9.0'
}
第二种方式是Gradle 2.1之后的新语法,使得插件应用更加简洁
应用脚本插件
表示加载一个新的脚本文件
apply from:'version.gradle'
应用第三方发布的插件
第三方插件可以去 https://plugins.gradle.org/ 地址搜索。
需要注意,在使用第三方发布的插件时,必须先在buildscript{}中进行配置才能使用,Gradle内置的插件则不需要。例如Android的Gradle插件就属于第三方插件,因此需要如下配置:
buildscript {
repositories {
google()
jcenter()
}
// 配置Android的插件
dependencies {
classpath "com.android.tools.build:gradle:4.0.2"
}
}
然后我们再应用Android的Gradle插件:
apply plugin: 'com.android.application'
其他API
判断属性
hasProperty('xxx'):判断是否有定义了xxx属性。在gradle.properties中定义的属性可以直接访问,但得到的类型为Object,一般需要通过toXXX()方法转型。if(hasProperty('isLoadApp')? isLoadApp.toBoolean():false){ include ':app' }
文件操作
getRootDir().absolutePath:获取根工程的绝对路径getBuildDir().absolutePath:获取工程下Build文件绝对路径getProjectDir().absolutePath:获取当前工程的绝对路径
文件拷贝:
/**
* 将当前文件拷贝到build文件夹下
*/
copy{
from file('proguard-rules.pro')
into getRootProject().getBuildDir()
}
/**
* 文件夹的拷贝,只支持在同一个根工程下操作
*/
copy{
from file('build/outputs/apk/')
into getRootProject().getBuildDir().path + "/apk"
//文件拷贝进行排除操作
exclude {}
//文件重命名
rename {}
}
使用copy闭包方法,from file 从哪个文件开始拷贝,into到哪个目录中去
文件遍历:
/**
* 对文件树进行遍历
*/
fileTree('build/outputs/apk'){ FileTree fileTree->
fileTree.visit { FileTreeElement fileTreeElement->
println "the file name is $fileTreeElement.name"
//在将当前文件下的文件拷贝到根工程build下的test文件夹下
copy{
from fileTreeElement.file
into getRootProject().getBuildDir().path + "/test/"
}
}
}
执行外部命令
/**
* 文件拷贝
*/
task(name:'apkcopy'){
doLast{
// gradle 执行阶段去执行
def sourcePath = this.buildDir.path + '/outputs/apk'
def destinationPath= '/Users/zhangtianzhu/Downloads/'
def command = "mv -f ${sourcePath} ${destinationPath}"
exec {
try{
executable 'bash'
args '-c',command
println 'the command is execute success'
}catch(GradleException e){
println 'the command is execute failed'
}
}
}
}
公众号“编程之路从0到1”