gradle.xmind
gradle

gradle流程图
android打包流程
aapt2编译流程

基本概念

gradle 是一个自动化构建工具(build automation,而不是像 maven 一样标榜自己是个 project management 工具),通过组织一系列 task 来最终完成自动化构建,所以 task 是 gradle 里最重要的概念之一, 以打包生成apk 为例,整个过程要经过资源的处理,javac 编译,dex 打包,apk 打包,签名等等步骤,每个步骤就对应到 gradle 里的一个 task。因为这些 task 是 gradle 的生命周期里特有的,所以 gradle 的插件应该不能移植到其他构建工具里。

每一个插件新增在 gradle 脚本里,都会带来新的 tasks。gradle 的 build 会触发很多的任务,包括但不限于构建和测试。我们常见的 build 文件夹就是 gradle 会频繁使用的东西。

gradle 可以使用 Groovy 或者 Kotlin DSL编写,这里我们涉及一个概念DSL ,DSL 也就是 Domain Specific Language 的简称,相对应的是 GPL (General-Purpose Language),比如 java语言,与 GPL 相比起来,DSL 使用简单,定义比较简洁,比起配置文件,DSL 又可以实现语言逻辑 对 gradle 脚本来说,通过DSL实现了简洁的定义,又有充分的语言逻辑,以 android {} 为例,这本身是一个函数调用,参数是一个闭包,但是这种定义方式明显要简洁很多

这类项目管理工具的基础是,如何使用声明式风格进行配置。所以它比 Maven 优秀的地方在于支持easy to build by implementing conventions

core types:

  • Project
  • Task
  • Gradle
  • Settings
  • IncludedBuild
  • Script
  • SourceSet
  • SourceSetOutput
  • SourceDirectorySet
  • Configuration
  • ResolutionStrategy
  • ArtifactResolutionQuery
  • ComponentSelection
  • ComponentSelectionRules
  • DependencyAdder
  • ExtensionAware
  • ExtraPropertiesExtension
  • PluginDependenciesSpec
  • PluginDependencySpec
  • PluginManagementSpec
  • ResourceHandler
  • TextResourceFactory
  • InputChanges
  • Distribution

Project 对象

我们常见的属性其实是一个 Project 对象的实例,使用 groovy 进行配置更像是使用一种 DSL 进行设值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
buildscript // 配置脚本的 classpath
allprojects // 配置项目及其子项目
respositories // 配置仓库地址,后面的依赖都会去这里配置的地址查找
dependencies // 配置项目的依赖


// 一个Android Project工程的标准配置

buildscript { // 配置项目的 classpath
// 每个 {} 圈定的闭包都是对一个单独的对象实例进行配置,配置的流程就是一行一行地调用方法
repositories { // 项目的仓库地址,会按顺序依次查找
// 这是一种调用法
google()
jcenter()
mavenLocal()
}
dependencies { // 项目的依赖
// 这是使用参数的调用法
classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.xx.plugin:xxplugin:0.0.1'
}
}

allprojects { // 子项目的配置
repositories {
google()
jcenter()
mavenLocal()
}
}

依赖管理

依赖管理要回答好三个问题:

  1. 你需要什么依赖,名称和版本
  2. 你用来干什么:compilation 还是 running,这一点不易区分开来
  3. 在哪里找这个依赖
1
2
3
4
5
6
7
8
9
repositories {
// 回答问题 3
mavenCentral()
}

dependencies {
// 回答问题 1 和 2
implementation 'org.hibernate:hibernate-core:3.6.7.Final'
}
  • Repository
  • Configuration:implementation是一种 configuration,a named collection of dependencies
  • Module coordinate

任务管理

基本的任务创建形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 以任务名创建:接受一个name参数
def task myTask = task(myTask)
myTask.doLast{
println "第一种创建Task方法,原型为Task task(String name) throws InvalidUserDataException"
}

// 以任务名+Map创建:Map参数用于对创建的task进行配置,可用配置有 type、overwrite、dependsOn、action、description、group
def task myTask = task(myTask,group:BasePlugin.BUILD_GROUP)
myTask.doLast{
println "第二种创建Task方法,原型为Task task(String name,Map<String,?> args) throws InvalidUserDataException"
}

// 以任务名+闭包创建:常见形式
task myTask{
doLast{
println "第三种创建Task方法,原型为Task task(String name,Closure configureClosure)"
}
}

// 多任务依赖
task task1<<{ // << 用在Task定义上相当于doLast
println 'hello'
}
task task2<<{
println 'world'
}
//依赖单个任务
task task3(dependsOn:task1){
doLast{
println 'one'
}
}
//依赖多个任务
task task4{
dependsOn task1,task2
doLast{
println 'two'
}
}

// 强制排序
taskB.shouldRunAfter(taskA) //表示taskB应该在taskA执行之后执行,有可能不会按预设执行
taskB.mustRunAfter(taskA) //表示taskB必须在taskA执行之后执行

// 分组&描述:分组是对任务的分类,便于归类整理;描述是说明任务的作用;建议两个一起配置,便于快速了解任务的分类和用途
def task myTask = task(myTask)
myTask .group = BasePlugin.BUILD_GROUP
myTask .description = '这是一个构建的引导任务'

// 启用&禁用:enable属性可以启动和禁用任务,执行被禁用的任务输出提示该任务被跳过
def task myTask = task(myTask)
myTask.enable = false //禁用任务

执行分析:执行Task的时候实际上是执行其拥有的actions List,它是Task对象实例的成员变量;在创建任务时Gradle会解析其中被TaskAction注解的方法作为其Task执行的action,并添加到 actions List,其中doFirst和doList会被添加到action List第一位和最后一位

每次构建(build)至少由一个project构成,一个project 由一到多个task构成。每个task代表了构建过程当中的一个原子性操作,比如编译,打包,生成javadoc,发布等等这些操作。

project
— task1 (Action1、Action2…)
— task2 (Action1、Action2…)
— …

通过任务把功能注册成插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 伪代码
open class ProjectDependencyGraphGeneratorTask : DefaultTask() {
@TaskAction
fun run() {
File(outputDirectory, projectGenerator.outputFileNameDot).writeText(graph.toString())

val graphviz = Graphviz.fromGraph(graph)

projectGenerator.outputFormats.forEach {
graphviz.render(it).toFile(File(outputDirectory, projectGenerator.outputFileName))
}
}
}



// 伪代码
open class DependencyGraphGeneratorPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.register(projectGenerator.gradleTaskName, ProjectDependencyGraphGeneratorTask::class.java)
}
}

class MyTask : DefaultTask {
@TaskAction
fun doAction(){
println("my task run")
}
}

class FastPlugin : Plugin<Project> {
override fun apply(project: Project) {
println("apply my plugin")
project.tasks.register("mytask", MyTask::class.java)
}
}

定义任务的依赖关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
task putOnSocks {
doLast {
println "Putting on Socks."
}
}

task putOnShoes {
dependsOn "putOnSocks"
doLast {
println "Putting on Shoes."
}
}



task eatBreakfast {
finalizedBy "brushYourTeeth"
doLast{
println "Om nom nom breakfast!"
}
}

task brushYourTeeth {
doLast {
println "Brushie Brushie Brushie."
}
}

属性管理

Project、Task和SourceSet都允许用户添加额外的自定义属性、并对自定义属性进行读取和设置。

  • 方式:通过ext属性,添加多个通过ext代码块
  • 优点:相比局部变量有广泛的作用域,可以跨Project、跨Task访问,只要能访问这些属性所属的对象即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 给Project添加自定义属性
ext.age = 18
ext{
phone = 13888888888
address = 'Beijing'
}

// 给Task添加自定义属性
task customProperty {
ext.inner = 'innnnnner'

doLast{
println project.hasProperty('customProperty') //true
println project.hasProperty('age') //true
println project.hasProperty('inner')//返回fasle
println "${age}"
println "${phone}"
println "${inner}"
}
}

SourceSet

Gradle’s Java support was the first to introduce a new concept for building source-based projects: source sets。

source code and resources files 应该被 logically grouped together。每个 source set 要关注自己的 dependencies 和 classpath。

source set 要关注这三个问题:

  1. 源代码和源代码的定位
  2. 编译的类路径-依赖来自于何处
  3. classes 文件应该被输出到何处

java-sourcesets-compilation

这个图里面,configuration 是 property,白色的是一个编译任务,三个绿方块就是 source set 关注的三个问题。sourceSet 可以被替换为 main 和 test。

java-sourcesets-process-resources

审美时候我们需要使用一个 custom source set?

  • 编译需要一个独特的类路径
  • 生成需要特殊处理的类,不同于 main 和 test
  • 形成一个自然部分(这部分令人难以理解,可能指的是项目自身就要求一个独立的源码集)

如果有个三方的源代码需要加入源码集里,有个例子是:

1
2
3
4
5
6
7
sourceSets {
main {
java {
srcDir 'thirdParty/src/main/java'
}
}
}

srcDir 是个方法,调用方法的目的是 append,与之对应的 srcDirs 是个属性,调用属性的目的是 replace。这是 gradle 的一个设计惯例。是 method 还是 property 看赋值方式是看通过空格还是=赋值,也可以通过查看文档来了解。

Task

Plugin

plugin 里带有很多 tasks。一个 plugin 主要影响 tasks、source set 和 configuration。

java

javaPluginTasks

这个插件的假定 layout 是这样的:

  • src/main/java
    • Production Java source.
  • src/main/resources
    • Production resources, such as XML and properties files.
  • src/test/java
    • Test Java source.
  • src/test/resources
    • Test resources.
  • src/sourceSet/java
    • Java source for the source set named sourceSet.
  • src/sourceSet/resources
    • Resources for the source set named sourceSet.

The Java plugin will compile whatever it finds, and handles anything which is missing.

You configure the project layout by configuring the appropriate source set.

我们常见的 main 和 test 是两个 java plugin 从 apache maven 借来的 convention source set。按照 gradle 的观点,只要共享一个 complilation 和 runtime class path,就可以把 test 配在同一个 source set 里。main 是用来存放 production source code,而 test 是用来存放测试 source code。这些 source 是 set 是用来保管 source code 的集合的,output 是它的附加属性。

比如上面的默认 source set 大致上等价于这样一段配置:

1
2
3
4
5
6
7
8
9
10
sourceSets {
main {
java {
srcDirs = ['src/main/java']
}
resources {
srcDirs = ['src/main/resources']
}
}
}

为集成测试准备的一些配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Assembling a JAR for a source set
tasks.register('intTestJar', Jar) {
// 这种 sourceSet 的 output 在 java 对象实现里是一些 file collection
from sourceSets.intTest.output
}

// Generating the Javadoc for a source set
tasks.register('intTestJavadoc', Javadoc) {
source sourceSets.intTest.allJava
classpath = sourceSets.intTest.compileClasspath
}

// 这是配置集成测试的方法之一
// Running tests in a source set
tasks.register('intTest', Test) {
testClassesDirs = sourceSets.intTest.output.classesDirs
classpath = sourceSets.intTest.runtimeClasspath
}

这个 plugin 会带来一些依赖配置(dependency configuration):

java-main-configurations
java-test-configurations

不同的 dependency 会在不同的 source set 的不同的 phase-compile、implementation、runtime 三重 scope 可见(The compile and runtime configurations have been removed with Gradle 7.0. Please refer to the upgrade guide how to migrate to implementation and api configurations`.)。这是一个 visible 问题。

java plugin 还有一个 java extension,这个 java extension 有一个 java block 来配置,如:

1
2
3
4
5
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

java library

对于 building 一个 java project 而言,最重要的插件是 java library 而不是 java。它会提供如下任务:

  • A compileJava task that compiles all the Java source files under src/main/java
  • A compileTestJava task for source files under src/test/java
  • A test task that runs the tests from src/test/java
  • A jar task that packages the main compiled classes and resources from src/main/resources into a single JAR named -.jar
  • A javadoc task that generates Javadoc for the main classes

管理 api exposed to consumer。

A library is a Java component meant to be consumed by other components. It’s a very common use case in multi-project builds, but also as soon as you have external dependencies.

在使用多模块和外部依赖的时候,管理 library 成了一个常见的用例。

java library 插件会让它的 consumer transitively 看到 api 依赖,但看不到 implementation 依赖。可以说 compile/implementation 是 gradle 自带的 dependency configuration,而 api 是 java library 带来的。

官方有个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class HttpClientWrapper {

private final HttpClient client; // private member: implementation details

// HttpClient is used as a parameter of a public method
// so "leaks" into the public API of this component
public HttpClientWrapper(HttpClient client) {
this.client = client;
}

// public methods belongs to your API
public byte[] doRawGet(String url) {
HttpGet request = new HttpGet(url);
try {
HttpEntity entity = doGet(request);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
entity.writeTo(baos);
return baos.toByteArray();
} catch (Exception e) {
ExceptionUtils.rethrow(e); // this dependency is internal only
} finally {
request.releaseConnection();
}
return null;
}

// HttpGet and HttpEntity are used in a private method, so they don't belong to the API
private HttpEntity doGet(HttpGet get) throws Exception {
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
System.err.println("Method failed: " + response.getStatusLine());
}
return response.getEntity();
}
}

这个模块要暴露的 api 仍然要暴露签名中的 httpclient 相关的类库给消费者,但不需要暴露内部的 ExceptionUtils 的应用。所以合理的 gradle 配置如下:

1
2
3
4
dependencies {
api 'org.apache.httpcomponents:httpclient:4.5.7'
implementation 'org.apache.commons:commons-lang3:3.5'
}

在 Java 9 以后,只要使用一个module-info.java文件,就可以把一个 java 类库变成 java module,如:

1
2
3
4
src
└── main
└── java
└── module-info.java

其内容为:

1
2
3
4
5
module org.gradle.sample {
requires com.google.gson; // real module
requires org.apache.commons.lang3; // automatic module
// commons-cli-1.4.jar is not a module and cannot be required
}

这个插件在两类 source set 视角下,进行 configuration set up 的 graph 如下:

java-library-ignore-deprecated-main
java-library-ignore-deprecated-test

CLI 接口

常用命令

1
2
3
4
5
6
7
8
9
# 修复某些 spring 的构建
gradle objenesisRepackJar
gradle cglibRepackJar

# 基于 gradlew 构建
./gradlew clean build -x test

# 列出依赖树
gradle dependencies

测试管理

自带的 test source set 是为了单元测试用的。如果为了审美(aesthetics)和可管理 (manageability),就需要 separate one from another。

集成测试的存在通常意味着独立的 test task。

配置一个集成测试

假设我们需要使用一个集成测试,需要首先为它准备一个 sourceSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sourceSets {
// 这里其实还需要配 src,这里重点只是这样做,这样做就是让 main 的 production 的 classes 产出全部成为这个 sourceSet 的 dependency
intTest {
compileClasspath += sourceSets.main.output
runtimeClasspath += sourceSets.main.output
}
}

configurations {
// 这是把 production 的依赖全部给与本源码集-需要理解的是,production code 的 classes 和它们自己的类路径依赖是不一样的
intTestImplementation.extendsFrom implementation
intTestRuntimeOnly.extendsFrom runtimeOnly

}

dependencies {
// 这是给本源码集增加测试环境下的特殊依赖
intTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
}

有一个更极端的做法是intTestImplementation.extendsFrom testImplementation,这样单元测试的所有测试依赖也会成为集成测试的依赖。

然后再为它注册一个单独的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tasks.register('integrationTest', Test) {
description = 'Runs integration tests.'
group = 'verification'

// 把输出给单独的任务用
testClassesDirs = sourceSets.intTest.output.classesDirs
// 把自己的 source code 的 runtime class path 当作任务的 class path
classpath = sourceSets.intTest.runtimeClasspath
// 定义任务的依赖
shouldRunAfter test

useJUnitPlatform()

testLogging {
events "passed"
}
}

// 这里让 check 又依赖于 integrationTest,等于 check -> integrationTest -> test
check.dependsOn integrationTest

构建管理

Maven 的项目结构通常是这样的:

这个结构通常是随着 archetype 变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
└───maven-project
├───pom.xml
├───README.txt
├───NOTICE.txt
├───LICENSE.txt
└───src 一级文件夹除了 src 和 target 只允许一些隐藏的 .git 文件夹存在
├───main
│ ├───java
│ ├───resources
│ ├───filters contains files that inject values into configuration properties in the resources folder during the test phase
│ └───webapp
├───test
│ ├───java
│ ├───resources
│ └───filters
├───it failsafe 插件专门用 it 文件夹来承载
├───site
└───assembly
└───target
└───classes com 文件夹和所有的 resources 下的文件
└───dddsample-2.0-SNAPSHOT.jar
└───dddsample-2.0-SNAPSHOT.jar.original
└───generated-sources
└───annotations
└───generated-test-sources
└───test-annotations
└───maven-archiver
└───pom.properties
└───maven-status
└───surefire-reports 每一个测试有一个 txt
└───test-classes com 文件夹和所有的 resources 下的文件

Spring Boot 的构建 output 通常是这样的:

  • out
    • production
      • classes
      • resources

gradle 的构建 output 通常是这样的:

  • build
    • classes
      • java
        • main
    • docs
    • generated
      • sources
        • annotationProcessor
          • java
            • main
    • libs
      • project1.jar:这是一个完整 fatjar
        • BOOT-INF
          • classes:大致等同于一个解压过的 project1-plain.jar
          • lib:各种二方、三方的依赖 jar
          • classpath.idx:索引文件
          • layers.idx:索引文件
        • META-INF
            - MANIFEST.MF
          
        • org
          • springframework
            • boot
              • loader:launcher 和 archive 相关的功能
      • project1-javadoc.jar:这里只有 MANIFEST.MF,大小只有 B
      • project1-plain.jar:结构同 project1-sources 一样,只是 java 文件全部变成 class 文件了
      • project1-sources.jar
        • 这个文件的结构是:
          • com java 源代码包
          • META-INF
            • MANIFEST.MF
          • 全部的 resources 文件:在进入 lib 以后,packaging 消除了 resources 和 java 文件夹的差别
    • resources
      • main:此处也按照 source set 进行分文件夹,也会 include 进测试 classpath 里
    • tmp:这一层看起来是不同的 task 的中间结果
      • bootJar
      • compileJava
      • jar
      • javadoc
      • javadocJar
      • sourcesJar
    • bootJarMainClassName:这个类型是 Spring Boot 特有的文件

支持的仓库类型

  • Maven
  • Ivy
  • Nexus

Groovy 代码片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
task printStringVar << {
def name = "张三”
println '单引号的变量计算:${name}'
println "双引号的变量计算:${name}"
}
// 运行./gradlew printStringVar输出结果:
// 单引号的变量计算:${name}
// 双引号的变量计算:张三



//List
task printList<<{
def numList = [1,2,3,4,5,6];//定义一个List
println numList[1]//输出第二个元素
println numList[-1]//输出最后一个元素
println numList[1..3]//输出第二个到第四个元素
numList.each{
println it//输出每个元素
}
}

//Map
task printlnMap<<{
def map1 =['width':1024,'height':768]//定义一个Map
println mapl['width']//输出width的值
println mapl.height//输出height的值
map1.each{
println "Key:${it.key},Value:${it.value}"//输出所有键值对
}
}



//以集合的each方法为例,接受的参数就是一个闭包
numList.each({println it})
//省略传参的括号,并调整格式,有以下常见形式
numList.each{
println it
}

// 不是一定要定义成员变量才能作为类属性被访问,用get/set方法也能当作类属性
task helloJavaBean<<{
Person p = new Person()
p.name = "张三"
println "名字是: ${p.name}"//输出类属性name,为张三
println "年龄是: ${p.age}"//输出类属性age,为12
}
class Person{
private String name
public int getAge(){//省略return
12
}
}

修复找不到 jar 的情况

1
2
3
4
5
# 把每一个能够下下来的jar做一个本地安装
mvn install:install-file -Dfile=/Users/magicliang/.gradle/caches/modules-2/files-2.1/com.tc.rainbow/java-client/1.0.2-RELEASE/fbaa6826bb26cb2b89bc1261f549abb3c903709e/java-client-1.0.2-RELEASE.jar -DgroupId=com.tc.rainbow -DartifactId=java-client -Dversion=1.0.2-RELEASE -Dpackaging=jar

# 再做一个本地安装
./gradlew clean build -x test -x javadoc --refresh-dependencies