author-gradle-2
gradle-basic-flow
gradle.xmind
gradle

gradle流程图
android打包流程

这里的 AIDL 是 Android Interface Definition Language 的意思。Dex 是 Dalvik executable format 的意思。

aapt2编译流程

基本概念

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

gradle 是事实上的 android/Kotlin 默认构建工具,虽然基于 JVM,但是可以用来构建其他语言

每一个插件新增在 gradle 脚本里,都会带来新的 tasks。gradle 的 build 会触发很多的任务,包括但不限于构建和测试我们做构建完了以后通常会看到这样的文字:460 actionable tasks: 460 executed-因为很多任务针对不同的 module 和 source set 会重复执行,所以项目越大,actionable tasks 越多。我们常见的 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(build.gradle 实际上就是在描述这个类型对象的生成)
  • 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
31
32
33
34
35
36
37
38
buildscript // 配置脚本(build.gradle)的 classpath

allprojects // 配置项目及其子项目
respositories // 配置仓库地址,后面的依赖都会去这里配置的地址查找
dependencies // 配置项目的依赖


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

buildscript { // 配置项目(build.gradle)的 classpath
// 每个 {} 圈定的闭包都是对一个单独的对象实例进行配置,配置的流程就是一行一行地调用方法
// 在这个块中定义的依赖项和仓库只对构建脚本本身可用,不会影响项目的依赖。通常用于添加 Gradle 插件依赖。
repositories { // 项目的仓库地址,会按顺序依次查找
// 这是一种调用法,寻找插件就在这里面寻找
google()
jcenter()
mavenLocal()
}
dependencies { // 项目的依赖
// 这是使用参数的调用法
classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.xx.plugin:xxplugin:0.0.1'
// 把插件引入类路径以后,下方还需要/可以 apply plugin: 'io.ebean'
classpath "io.ebean:ebean-gradle-plugin:12.2.3"
}

}

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

这里面的 buildscript 的 dependencies 是给 build.gradle 这个项目脚本构建中使用的。而我们常见的其他地方的 dependencies 是给项目打包/运行的时候项目自身用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.yaml.snakeyaml.Yaml

buildscript {
repositories { // Where to find the plugin or library
maven {
url = uri("https://plugins.gradle.org/m2/")
}
mavenCentral()
}
dependencies {
classpath 'org.yaml:snakeyaml:1.19' // The library's classpath dependency
classpath 'com.gradleup.shadow:shadow-gradle-plugin:8.3.4' // Plugin dependency for legacy plugin application
}
}

// Applies legacy Shadow plugin
apply plugin: 'com.gradleup.shadow'

// Uses the library in the build script
def yamlContent = """
name: Project Name
"""
def yaml = new Yaml()
def data = yaml.load(yamlContent)

在 idea 里,右侧的栏目列出的只有任务(插件看不见,被融入任务列表)+依赖

gradle 能支持的仓库类型/风格有 remote(maven、ivy,可以用Nexus/Sonatype/JFrog)和 local(flatDir)。

Task

任务可以被分为:Application tasks、Build tasks、Documentation tasks、Other tasks。

任务管理

A task represents some independent unit of work that a build performs, such as compiling classes, creating a JAR, generating Javadoc, or publishing archives to a repository.

1
./gradlew tasks

基本的任务创建形式

最新版的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
tasks.register("task1") {  
println("REGISTER TASK1: This is executed during the configuration phase")
}

tasks.named("task1") {
println("NAMED TASK1: This is executed during the configuration phase")
doFirst {
println("NAMED TASK1 - doFirst: This is executed during the execution phase")
}
doLast {
println("NAMED TASK1 - doLast: This is executed during the execution phase")
}
}

To create a custom task, you must subclass DefaultTask in Groovy DSL or DefaultTask in Kotlin DSL.要创建一个自定义任务,你必须在 Groovy DSL 中继承 DefaultTask 类,或者在 Kotlin DSL 中继承 DefaultTask 类。

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),第一个参数是 name,第二个参数是 configureClosure"
}
}

// 多任务依赖,这里面 task1 和 task2 之间没有依赖关系
task task1<<{ // 每个任务都使用了 << 操作符,这是一种在旧版本 Gradle 中添加任务动作的方式。 (注意:<< 操作符在新版本的 Gradle 中已被弃用,现在推荐使用 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执行之后执行

// 分组&描述:分组是对任务的分类,便于归类整理;描述是说明任务的作用;建议两个一起配置,便于快速了解任务的分类和用途。和上面的“以任务名+Map创建”本质上相同
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
// 伪代码
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
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."
}
}

向任务注入对象

可注入对象:

  • ObjectFactory- 允许创建模型对象。
  • ProjectLayout- 提供对关键项目位置的访问权限。
  • BuildLayout- 提供对 Gradle 构建的重要位置的访问。
  • ProviderFactory- 创建Provider实例。
  • WorkerExecutor- 允许任务并行运行。
  • FileSystemOperations- 允许任务在文件系统上运行操作,例如删除文件、复制文件或同步目录。
  • ArchiveOperations- 允许任务对存档文件(例如 ZIP 或 TAR 文件)运行操作。
  • ExecOperations- 允许任务运行外部进程,并提供专门的运行外部java程序的支持。
  • ToolingModelBuilderRegistry- 允许插件注册 Gradle 工具 API 模型。
1
2
3
4
5
6
7
8
tasks.register("myObjectFactoryTask") {
doLast {
def objectFactory = project.objects
def myProperty = objectFactory.property(String)
myProperty.set("Hello, Gradle!")
println myProperty.get()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DownloadExtension {
// A nested instance
private final Resource resource;

@Inject
public DownloadExtension(ObjectFactory objectFactory) {
// Use an injected ObjectFactory to create a Resource object
resource = objectFactory.newInstance(Resource.class);
}

public Resource getResource() {
return resource;
}
}

public interface Resource {
Property<URI> getUri();
}

属性管理

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}"
}
}

惰性加载

为什么要使用 property 而不使用原始类型

Gradle 使用两个接口表示惰性属性:

  • property - 表示可以查询和更改的值。属性可能是可变的,这意味着它同时具有get()方法和set()方法(类似C#)。
  • provider - 表示只能查询而不能更改的值。也叫Read-only Managed Properties (Providers)。

Plugin

Plugins are the primary method to organize build logic and reuse build logic within a project.

Plugins are used to extend Gradle’s capability(功能) and optionally contribute tasks to a project.

plugin 里带有很多 tasks。一个 plugin 主要影响 source set 和 configuration,带来了属性(SourceSet就性质而言是一种 property,但文档把它称作 domain objects)、方法和任务。

  • Java 插件
    • 插件 ID: java
    • 主要功能:
      • 添加 Java 编译能力
      • 提供标准的 Java 项目结构和任务(如 compileJava, test)
      • 添加基本的依赖配置(如 implementation, testImplementation)
    • 适用场景:基本的 Java 项目,很多插件都扩展本插件,包括但不限于:Application、Java Library
  • Application 插件
    • 插件 ID: application
    • 主要功能:
      • 包含 Java 插件的所有功能
      • 添加运行和打包应用程序的能力
      • 提供 run 任务来执行应用程序
      • 可以创建可分发的 ZIP 和 TAR 包
    • 适用场景:需要作为独立应用程序运行的 Java 项目
  • Java Library 插件
    • 插件 ID: java-library
    • 主要功能:
      • 扩展了 Java 插件
      • 引入 api 和 implementation 依赖配置的区别
      • 更好地控制库的 API 暴露
    • 适用场景:开发供其他项目使用的 Java 库
  • Spring Boot 插件
    • 插件 ID: org.springframework.boot
    • 主要功能:
      • 提供 Spring Boot 特定的任务(如 bootRun)
      • 能够创建可执行的 JAR 或 WAR 文件
      • 管理 Spring Boot 依赖版本
      • 提供 Spring Boot 的自动配置支持
    • 适用场景:Spring Boot 应用程序开发
  • Spring Dependency Management 插件
    • 插件 ID: io.spring.dependency-management
    • 主要功能:
      • 提供类似 Maven BOM(Bill of Materials)的依赖管理
      • 允许在不指定版本的情况下导入依赖
      • 可以与 Spring Boot 插件配合使用,也可以单独使用
    • 适用场景:需要统一管理依赖版本的项目,特别是 Spring 项目
  • 主要区别和使用场景:
    • 基本 Java 项目:使用 java 插件。
    • 可执行的 Java 应用:使用 application 插件。
    • Java 库开发:使用 java-library 插件。
    • Spring Boot 应用开发:使用 org.springframework.boot 插件,通常与 io.spring.dependency-management 插件一起使用。
    • 需要精细控制依赖版本的项目:使用 io.spring.dependency-management 插件。

这些插件可以组合使用,例如:

  • 一个 Spring Boot 应用通常会同时使用 java、org.springframework.boot 和 io.spring.dependency-management 插件。
  • 一个作为库开发的 Spring 项目可能会使用 java-library 和 io.spring.dependency-management 插件。

大部分构建类的插件都是派生自 Base 插件,比如 java 插件,然后 The Java Library Plugin also integrates the above tasks into the standard Base Plugin lifecycle tasks,比如jar is attached to assemble,test is attached to check。

分类

  1. Core plugins - Gradle develops and maintains a set of Core Plugins. Gradle Core plugins are a set of plugins that are included in the Gradle distribution itself. 核心插件不需要指定版本。
  2. Community plugins - Gradle’s community shares plugins via the Gradle Plugin Portal. Community plugins are plugins developed by the Gradle community, rather than being part of the core Gradle distribution. These plugins provide additional functionality that may be specific to certain use cases or technologies.
  3. Local plugins/Custom plugins - Gradle enables users to create custom plugins using APIs.Custom or local plugins are developed and used within a specific project or organization. These plugins are not shared publicly and are tailored to the specific needs of the project or organization.

Convention plugins are plugins used to share build logic between subprojects (modules). Users can wrap common logic in a convention plugin. For example, a code coverage plugin used as a convention plugin can survey code coverage for the entire project and not just a specific subproject.

Gradle highly recommends the use of Convention plugins.

所有的插件的定义都有这样一段tasks.register

区分插件的标准是看是已经被编译成字节码(binary plugin),还是能看到源码(script plugin i)。A plugin often starts as a script plugin (because they are easy to write). Then, as the code becomes more valuable, it’s migrated to a binary plugin that can be easily tested and shared between multiple projects or organizations.二进制插件可以更高效共享和运行。

插件的使用总是分两步:

  1. resolve 解析:这是配置 repo 和 class path 的意义
  2. apply:会调用Plugin.apply(T)

声明

1
2
3
4
5
6
7
8
9
plugins {
id("org.barfuin.gradle.taskinfo") version "2.1.0"
}
allprojects {
apply(plugin = "org.barfuin.gradle.taskinfo")
repositories {
mavenCentral()
}
}

如果在顶层指定过版本,则在其他 method 里可以不指定版本了。

现代的 gradle有个特殊的目录和文件,对全部项目可以这样声明:

1
2
3
4
5
6
7
8
9
10
11
12
plugins {
id 'java-gradle-plugin'
}

gradlePlugin {
plugins {
myPlugins {
id = 'my-plugin'
implementationClass = 'my.MyPlugin'
}
}
}

插件仓库

1
2
3
4
5
6
7
8
9
pluginManagement {  
plugins {
}
resolutionStrategy {
}
repositories {
gradlePluginPortal()
}
}

java

javaPluginTasks

这几幅图说明了 java 插件是如此重要!我们常见的构建任务都是这个插件带来的。

SourceSet

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

源码集的概念是由 java 插件引入的,不是 gradle 自带的。

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

这个插件的假定 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(所以它 maven-compatible)。按照 gradle 的观点,只要共享一个 complilation 和 runtime classpath,就可以把 test 配在同一个 source set 里。main 是用来存放 production source code,而 test 是用来存放测试 source code(默认是 unit test,不是 integration test、acceptance test)。这些 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']
}
}
}

source set 要关注这三个问题:

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

java-sourcesets-compilation

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
plugins {
// 源码集的概念是由插件引入的,不是 gradle 自带的
id 'java'
}

repositories {
mavenCentral()
}

dependencies {
// 仅在编译时需要的依赖
sourceSets.main.compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'

// 编译时和运行时都需要的依赖
sourceSets.main.implementation 'org.springframework.boot:spring-boot-starter:2.7.4'
sourceSets.main.implementation 'org.springframework.boot:spring-boot-starter-aop:2.7.4'
sourceSets.main.implementation 'org.springframework.boot:spring-boot-starter-logging:2.7.4'
}

Most language plugins, Java included, automatically create a source set called main, which is used for the project’s production code. This source set is special in that its name is not included in the names of the configurations and tasks, hence why you have just a compileJava task and compileOnly and implementation configurations rather than compileMainJava, mainCompileOnly and mainImplementation respectively.

java-sourcesets-process-resources

每个源码集都有一个专有的 processSourceSetResources (or processResources for the main source set)。

什么时候我们需要使用一个 custom source set?

当我们要形成特定的布局定义的时候:

  • 编译需要一个独特的类路径(unique classpath)
  • 生成需要特殊处理的类,不同于 main 和 test

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sourceSets {
main {
// 等价于 java.srcDirs = ['src']
java {
// 这里用第三方的源代码代替了原始的 src/main,如果要把新旧源码混合到一起最好使用数组形式 srcDirs
srcDir 'thirdParty/src/main/java'
}
// manifest.srcFile 'AndroidManifest.xml'
// java.srcDirs = ['src']
// resources.srcDirs = ['src']
// aidl.srcDirs = ['src']
// renderscript.srcDirs = ['src']
// res.srcDirs = ['res']
// assets.srcDirs = ['assets']
// jniLibs.srcDirs = ['libs']

}
}

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

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

参考setting up integration tests

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
}

依赖配置(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

java-platform

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
plugins {
id 'java-platform'
}

dependencies {
// 该dependencies块提供了一个constraints可用于帮助 Gradle 选择依赖项的特定版本的块,这是指定依赖的一种方法
constraints {
api 'org.springframework:spring-core:5.3.10'
api 'org.springframework:spring-context:5.3.10'
runtime 'org.slf4j:slf4j-api:1.7.32'
}
}

dependencies {
implementation platform(project(':platform'))
implementation 'org.springframework:spring-core'
}

maven 的 bom 必须这样在 gradle 里使用。

依赖项约束的功能与依赖项类似,主要区别在于它们本身不会引入依赖项。相反,约束定义了版本要求,当依赖项通过其他方式引入项目时,这些要求会影响解析过程。

虽然默认情况下约束不是严格版本,但您可以根据需要指定严格版本约束。一旦包含依赖项,约束指定的版本就会参与冲突解决,就像将其声明为直接依赖项一样。

日志

级别

  • ERROR (—quiet 或 -q):仅显示错误信息。这个级别用于最简化的输出,只在构建失败时提供必要的错误信息。
  • WARNING:显示警告和错误信息。默认情况下,Gradle会在WARNING级别显示输出,提供有关潜在问题的警告信息。
  • LIFECYCLE:显示构建生命周期的主要事件,例如任务的开始和结束。这是Gradle的默认日志级别,适合大多数用户的日常使用。
  • INFO (—info):显示详细的构建信息,包括任务执行的更多细节和配置信息。这个级别适用于需要了解构建过程更多细节的用户。
  • DEBUG (—debug):显示非常详细的调试信息,包括内部状态和详细的执行过程。这个级别通常用于调试构建脚本或插件。
  • TRACE:显示最详细的日志信息,包括所有可能的内部操作和状态变化。这个级别通常用于开发Gradle本身或深入调试复杂问题。

依赖管理

Dependency management is an automated technique for declaring and resolving external resources required by a project.

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

  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(所以 api 也是一种 configuration),a named collection of dependencies
  • Module coordinate:group name + artifact name + version

libs.versions.toml

目前这个方案被拿来当作 version catalog 用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[versions]
groovy = "3.0.5"
checkstyle = "8.37"

[libraries]
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" }
groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" }
groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" }
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } }

[bundles]
// 把上面的 artifact ref 过来,以后可以这样用:implementation bundles.groovy
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }
1
2
3
4
5
plugins {
id 'java-library'
// libs 是文件,plugins 是文件里的 plugins 段,versions 就是一个 ref
alias(libs.plugins.versions)
}

configuration 分类

Configuration Name Description Used to:
api 用于编译和运行时都需要的依赖,并包含在发布的 API 中。
Dependencies required for both compilation and runtime, and included in the published API.
Declare Dependencies
implementation 用于编译和运行时都需要的依赖。
Dependencies required for both compilation and runtime.
Declare Dependencies
compileOnly 仅在编译时需要的依赖,不包含在运行时或发布中。
Dependencies needed only for compilation, not included in runtime or publication.
Declare Dependencies
compileOnlyApi 仅在编译时需要的依赖,但包含在发布的 API 中。
Dependencies needed only for compilation, but included in the published API.
Declare Dependencies
runtimeOnly 仅在运行时需要的依赖,不包含在编译类路径中。
Dependencies needed only at runtime, not included in the compile classpath.
Declare Dependencies
testImplementation 用于编译和运行测试所需的依赖。
Dependencies required for compiling and running tests.
Declare Dependencies
testCompileOnly 仅用于测试编译的依赖。
Dependencies needed only for test compilation.
Declare Dependencies
testRuntimeOnly 仅用于运行测试的依赖。
Dependencies needed only for running tests.
Declare Dependencies

implementation 才是最需要使用的configuration,因为大多数情况下我们除了专门的 sdk module,不需要 publish api。

本质上只有2种类路径 compileClassPath + runtimeClassPath:

  • compileOnly、runtimeOnly、published api 可叠加。
  • implementation = compileOnly + runtimeOnly
  • api = implementation + published api

java-main-configurations

此外还有隐藏用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
// 添加编译时依赖
compileClasspath 'org.apache.commons:commons-lang3:3.10'

// 添加运行时依赖
runtimeClasspath 'com.google.guava:guava:28.2-jre'
}

task printRuntimeClasspath {
doLast {
configurations.runtimeClasspath.each { println it }
}
}

依赖约束和冲突解决 Dependency Constraints and Conflict Resolution

  1. 版本冲突:当两个或多个依赖项需要给定的模块但版本不同时。
  2. 功能冲突:当依赖图包含提供相同功能的多个工件时。
  • gradle 的做法:Gradle 会考虑所有请求的版本,无论它们出现在依赖关系图中的哪个位置。默认情况下,它会从这些版本中选择最高的版本
  • maven 的做法:
    • 默认规则:最近优先(Nearest Definition)
      • Maven 会选择依赖树中离项目最近的版本。
      • 如果路径长度相同,则选择首先声明的版本。
    • 可传递性:
      • Maven 默认会传递所有依赖。

冲突解决有这些方法:Resolution Rules、Dependency Substitution、Dynamic Versions(这是对构建最危险的,对产生 reproducible builds 造成危害)、Dependency Locking( resolutionStrategy.activateDependencyLocking())。最常用的是使用 configuration 的版本 force-没有使用 lock file。

CLI 接口

常用命令

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
55
56
# 修复某些 spring 的构建
gradle objenesisRepackJar
gradle cglibRepackJar

# 初始化项目
gradle init --type java-application --dsl groovy

# 基于 gradlew 构建,去掉 clean 更快
./gradlew clean build -x test -x javadoc --console=verbose

# 只对某个模块进行构建,这样不用 cd 进文件夹里了
./gradlew :app:clean :app:build

# 列出 root project 依赖树,列出各种配置标题。每个标题(如 annotationProcessor, apiElements 等)代表一个 Gradle 配置。配置是依赖的容器,用于不同的目的。
./gradlew dependencies

allCodeCoverageReportClassDirectories - Supplies the class directories used to produce all aggregated JaCoCo coverage data reports
--- project : (*)

allCodeCoverageReportSourceDirectories - Supplies the source directories used to produce all aggregated JaCoCo coverage data reports
--- project : (*)

annotationProcessor - Annotation processors and their dependencies for source set 'main'.
No dependencies

apiElements - API elements for main. (n)
No dependencies

archives - Configuration for archive artifacts. (n)
No dependencies

# 列出模块依赖树-真正的依赖树要这样列出来才有意义,最好从头部 entrance sub project/module 进去
./gradlew :moduleName:dependencies

# 深度解析依赖树,后两个参数是必须的
./gradlew dependencyInsight --dependency opentelemetry-api-metrics

# 更重要的方法是使用 idea 的 analyzer

# 直接执行 task,build 是一个聚合(aggregate)task, 它触发一个依赖链,build -> assemble -> compileJava
# Executing Gradle on the command line conforms to the following structure:
gradle [taskName...] [--option-name...]
# Options are allowed before and after task names.
gradle [--option-name...] [taskName...]
# If multiple tasks are specified, you should separate them with a space.
gradle [taskName1 taskName2...] [--option-name...]
# --dry-run 可以不执行任务,但是显示任务执行的顺序,对性能影响很小

# 把内置选项和任务选项区分开
gradle [--built-in-option-name...] -- [taskName...] [--task-option-name...]

# 通用输出 SKIPPED、NO-SOURCE
# 使用构建缓存,输出会带有 UP-TO-DATE、FROM-CACHE,所以增量编译是模块-任务级的
gradle [...] --build-cache
# 不使用缓存
gradle [...] --no-build-cache

构建管理

项目结构

Maven

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

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

  • out
    • production
      • classes
      • resources

Gradle

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 特有的文件

依赖解析

dep-man-basics-1

module = component = com.fasterxml.jackson.core:jackson-databind

所谓的图就是搞明白 direct dependencies (explicitly declared in the build script) and transitive dependencies (dependencies of the direct dependencies and other transitive dependencies)。

依赖图由以下节点组成:

  • 每个节点代表一个变体。
  • 每个依赖项从一个组件中选择一个变体。

这些图就是./gradlew app:dependencies的输出。
强力的依赖解析可以使用./gradlew :app:dependencyInsight --configuration runtimeClasspath --dependency com.fasterxml.jackson.core:jackson-databind:2.17.2命令。

任意的 module 的metadata都包含两个variants,如:

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
[
{
"name": "apiElements"
},
{
"name": "runtimeElements",
"attributes": {
"org.gradle.category": "library",
"org.gradle.dependency.bundling": "external",
"org.gradle.libraryelements": "jar",
"org.gradle.usage": "java-runtime"
},
"dependencies": [
{
"group": "com.fasterxml.jackson.core",
"module": "jackson-annotations",
"version": {
"requires": "2.17.2"
}
},
{
"group": "com.fasterxml.jackson.core",
"module": "jackson-core",
"version": {
"requires": "2.17.2"
}
},
{
"group": "com.fasterxml.jackson",
"module": "jackson-bom",
"version": {
"requires": "2.17.2"
}
}
],
"files": [
{
"name": "jackson-databind-2.17.2.jar"
}
]
}
]

Gradle 通常在编译时会选择apiElements,而在运行时会选择runtimeElements。因为apiElements 通常更小,只包含公共 API,这样可以加快编译速度。runtimeElements 包含完整实现,确保应用在运行时有所有需要的类和资源。似乎选用 api configuration 会把某个本 project 的依赖加入到本项目的 apiElements variants 里。

gradle 的 download step 就是找到这些 files 里的文件。而transform指的是如果文件不是 jar 而是 zip,可以从 zip 转成 jar。

版本和范围

  • 确保关键依赖使用特定版本(strictly)。
  • 确保依赖不低于某个版本(require)。
  • 优先选择某个版本以避免不必要的升级(prefer)。
  • 排除已知有问题的版本(rejects)。

默认的版本解析策略是基于最高版本策略(highest version strategy),即上面的一个都不选

性能优化

  1. 并行构建-最好修改 properties
  2. 启用各式缓存-局部缓存要单独配,局部的增量构建也是-最好修改 properties
  3. 减少解析依赖的时间-优化仓库、使用版本-选择最好的镜像
  4. 使用 scan 看构建图,查看慢速的部分-使用构建选项

缓存的好处

  • 使用本地缓存加速开发人员构建
  • 在 CI 构建之间共享结果
  • 通过重复使用 CI 结果来加速开发人员的构建
  • 将远程结果与本地缓存相结合
  • 在开发人员之间共享成果

可缓存与不可缓存

默认情况下构建缓存是不打开的。—build-cache 和 properties 配置是两种使用构建缓存的方法。针对相同的输入,可能触发构建缓存。

:classes和:assemble是生命周期任务, 和:processResources 是:jar复制类任务,它们不可缓存,因为执行它们通常更快。

可缓存的任务有:

  • Java toolchain: JavaCompile, Javadoc
  • Groovy toolchain: GroovyCompile, Groovydoc
  • Scala toolchain: ScalaCompile, org.gradle.language.scala.tasks.PlatformScalaCompile (removed), ScalaDoc
  • Native toolchain: CppCompile, CCompile, SwiftCompile
  • Testing: Test
  • Code quality tasks: Checkstyle, CodeNarc, Pmd
  • JaCoCo: JacocoReport
  • Other tasks: AntlrTask, ValidatePlugins, WriteProperties

All other built-in tasks are currently not cacheable.

自定义缓存

@CacheableTask 注解可以用来缓存那些耗时比较久的任务,比如自定义的 Javascript 任务。

1
2
3
4
5
6
7
8
9
10
11
12
buildCache {
local {
directory = new File(rootDir, 'build-cache')
}
}

buildCache {
remote(HttpBuildCache) {
// When attempting to load an entry, a GET request is made to https://example.com:8123/cache/«cache-key». The response must have a 2xx status and the cache entry as the body, or a 404 Not Found status if the entry does not exist.
url = 'https://example.com:8123/cache/'
}
}

配置缓存

The configuration cache is a feature that significantly improves build performance by caching the result of the configuration phase and reusing this for subsequent builds.

测试管理

自带的 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

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
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