Bootstrap

Gradle 的日常使用

1 Scope

[java]
compileOnly
runtimeOnly
implementation
testCompileOnly
testRuntimeOnly
testImplementation

[java-library]
api
compileOnlyApi

[war]
providedCompile

[exclude]
implementation('org.xx:xx:xx') {
        exclude group: 'org.xx'
        exclude module: 'slf4j-api' // or
        exclude group: 'org.xx', module: 'slf4j-api'
}

[transitive]
implementation('org.hibernate:hibernate-core:3.6.3.Final') {
        transitive(false) // no allow transitive
}

[enfore]
implementation('org.slf4j:slf4j-api:1.4.0!!')

2 fast-fail

configurations.configureEach {
    resolutionStrategy {
        failOnVersionConflict() // fast-fail
        force 'animal:cat:1.0.0' // 强制依赖
    }
}
// 同等写法
configurations.configureEach { Configuration config ->
    config.resolutionStrategy.failOnVersionConflict()
    config.resolutionStrategy.force('animal:cat:1.0.0')
}

3 gradle command

gradle tasks
gradle build
gradle run
gradle tasks --all
gradle dependencies
gradle properties

4 commandline args

--max-workers --parallel --no-parallel -Dorg.gradle.logging.level=quiet
-q/quiet -w/warn -i/info -d/debug -x/exclude-task --rerun-tasks # ignore up-to-date
--continue # generate build report

e.g. gradle init --type pom
e.g. gradle myTask is equal to gradle mT

5 dynamic invoke

new A().printA()
// equivalent
def a = new A()
invokeVirtual(a, 'printA')

6 Domain-Sepcific Languages

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }

// equivalent to: select(all).unique().from(names)
select all unique() from names

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

7 Delegate & Closure

Closure closure = {
        sayHello()
}
class Foo {
        void sayHello() {
                println("hello")
        }
}
def foo = new Foo()
closure.delegate = foo
closure() // invoke

equivalent to internal Class

8 Task

task A {}
task(map, 'B')
tasks.create('C') {}
tasks.register('D') {} // Lazy loading

// use exist Type
tasks.register('myClean', Delete) { 
    delete buildDir
}

task sourcesJar(type: Jar, dependsOn: classes) {
	classifier = 'sources'
	from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
	classifier = 'javadoc'
	from javadoc.destinationDir
}

4.times {
        tasks.register("task$it") {
                doLast {
                        println "I'm task number $it"
                }
        }
}

method: tasks.findByName / getByName / findByPath / getByPath(“:project-a”)
addRule(“description”) { taskName -> task(taskName) { doLast { print $taskName } } }

taskName.onlyIf { !project.hasProperty(‘a’) } // return if excute
defaultTasks ‘taskA’, ‘taskB’ // project default execute Task

9 Daemon

jvm start and shutdown is too heavy for system, better than keep running

gradle connect daemon throught socket

10 Core Object

  1. Project

  2. Task

  3. Gradle

  4. Gradle 初始化构建, 全局单例

  5. Project pom.xml 模块唯一对应

  6. Settings 项目唯一, 一般只用到 include 方法

  7. Task 前面的有向无环图基于 Task 对象

11 Life Cycle

Initialzation
        settings.gradle Gradle#settingsEvaluated
        build.gradle Gradle#projectsLoaded
Configuration
        allprojects: Gradle#beforeProject and Project#beforeEvaluate
        subprojects: Gradle#beforeProject and Project#beforeEvaluate
        SubProjectA: Gradle#beforeProject and Project#beforeEvaluate
        EXECUTE build.gradle 配置段脚本
        SubProjectA: Gradle#afterProject and Project#afterEvaluate
        ALL READY:
                Gradle#projectsEvaluated
                Gradle#taskGraph#whenReady
Execution
        TaskExecutionGraph#beforeTask
        EXECUTE Task Action
        TaskExecutionGraph#afterTask
        ALL EXECUTED: Gradle#buildFinish

12 buildscript

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.6.3'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.xxx.xx'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    // useJUnit() for junit four
    useJUnitPlatform() // for junit jupiter
}

// or
buildscript {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/public' }
    }
    dependencies {
        classpath 'org.springframework.boot:spring-boot-gradle-plugin:2.4.1'
    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

13 buildSrc 目录

buildSrc 是 Gradle 默认的插件目录, 编译时自动识别代码, 编译成插件

一般在 gradle init --type pom 之后会生成这个目录

自定义插件

buildSrc/src/main/groovy/插件代码
buildSrc/build.gradle

14 gradle 中的 dependencyManagement

参考: https://docs.gradle.org/current/userguide/java_platform_plugin.html


示例: 如果一个平台声明了org:foo:1.1的约束,并且没有其他东西带来对foo的依赖,foo将不会出现在图中。 但是,如果出现foo,那么通常的冲突解决将开始。如果依赖项引入org:foo:1.0,那么我们将选择org:foo:1.1来满足平台约束。

  1. pom 的创建
ext {
    // ExtraPropertiesExtension ext is shorthand for project.ext, and is used to define extra properties for the project object.
    // When reading an extra property, the ext. is omitted (e.g. println project.springVersion or println springVersion).
    publishReleaseUrl = 'http://localhost:8...'
    publishSnapshotUrl = 'http://localhost:8...'
    publishUsername = 'admin'
    publishPassword = 'admin123'
}

plugins {
    id 'java-platform'
    id 'maven-publish'
}

javaPlatform {
	// 默认为防止用户错误地引入依赖,而不是引入依赖约束,如果引入依赖会报错失败。
	// 通过这个配置可以让Gradle允许引入依赖,当然这是可选的
    allowDependencies() // 既定义约束, 也能加入依赖
}

dependencies {
    constraints { // n. 强制; 束缚
        // 版本号管理 (要统一管理的依赖+版本)
        api 'com.java:java:1.8'
        implementation 'com.google:guava:31.1'
    }
    api platform('com.fasterxml.jackson:jackson-bom:2.9.8')
}

publishing {
	publications {
		myPlatform(MavenPublication) { // MyMavenJava: 发布的任务标识(会出现在面板中)
            // 指定发布成的POM文件 (GAV坐标)
            artifactId = 'poter'
            groupId = 'com.hurry'
            version = '1.0'

            from components.javaPlatform
		}
	}
	repositories {  // 有这个才有: publish{yourdefine:myPlatform}PublicationToMavenRepository
		maven {
            // 私服配置信息
			if (version.endsWith('-SNAPSHOT')) {
				url publishSnapshotUrl
			} else {
				url publishReleaseUrl
			}
			allowInsecureProtocol = true // 是否允许 http
			credentials {
				username publishUsername
				password publishPassword
			}
		}
	}
}
  1. 发布

在 idea gradle 面板上选择

Tasks > publishing > publish{yourdefine}PublicationToMavenLocal
Tasks > publishing > publish{yourdefine}PublicationToMavenRepository

发布到Maven私服中, 此时我们能看见私服上对应的 pom 上是

<packaging>pom</packaging>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>GAV</groupId>
            ...
  1. 使用
// build.gradle
plugins {
    id 'java-platform'
}
dependencies {
    implementation platform('com.hurry:poter-dependencies:1.0')
    implementation 'com.google:guava' // 此时就不用写版本号了
}

项目内

dependencies {
    // get recommended versions from the platform project
    api platform(project(':platform'))
    // no version required
    api 'commons-httpclient:commons-httpclient'
	api platform('com.fasterxml.jackson:jackson-bom:2.9.8')
}

15 发包添加源码

需要插件 java-library 和 maven-publish

  • 获取系统变量 System.getenv(“系统环境变量”)
  • 获取参数: System.getProperty(“-D参数名”)
plugins {
    id 'java-library'
    id 'maven-publish'
    id 'io.freefair.lombok' version '8.4'
}

// 注册一个名为 sourcesJar 的 Gradle 任务,使用 Jar 类型
tasks.register('sourcesJar', Jar) {
    // 任务依赖于 classes 任务(即在执行任务之前会先执行 classes 任务, 因为需要先编译生成后的类文件
    dependsOn classes
    // 设置任务的分类器(classifier)为 'sources'. 使用分类器来区分不同类型的构建输出 (源码、文档、测试报告)
    archiveClassifier = 'sources'
    // sourceSets.main 是 Gradle 中默认的源码集合, 将 sourceSets.main 中的所有源码文件添加到 JAR 文件中
    from sourceSets.main.allSource
    // (二选一) 使用lombok插件
    from sourceSets.main.delombokTask
}

ext {
	uname = 'uname'
	pwd = 'pwd'
	repoUrl = 'http:///'
}
version = '0.0.1-SNAPSHOT'

publishing {
	publications {
		privatePublication(MavenPublication) { // 发布的任务标识 (会出现在gradle面板中)
			from components.java
			artifact sourcesJar
		}
	}
	repositories {
		maven {
			name 'privateRepo'
			// 私服配置信息
			url repoUrl
			allowInsecureProtocol = true
			credentials {
				username = uname
				password = pwd
			}
		}
	}
}

16 发布到中央仓库

https://www.jetbrains.com/help/space/publish-artifacts-to-maven-central.html

17 清除缓存

针对同一个快照版本的 不同更新, 有时候死活拉不下来, 删除都没用时, 可以试试清除缓存

在这里插入图片描述

configurations.all {
	// 动态版本号的缓存时间, 例如在解析依赖项动态版本时 xxx:xxx:1.1.+ 之类的动态版本; 设置为 0 秒, 表示不进行缓存动态版本
    resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
    // 变化模块的缓存时间; 设置为 0 秒, 即每次都从远程仓库下载最新的快照版本
    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}

记录的版本索引都在 maven-metadata.xml 里, 路径为
~/.gradle/caches/modules-2/metadata-2.**/descriptors/<repository>/<group>/<artifact>

  • 清除 Gradle 缓存中所有已下载的依赖项和构建中间文件,从而释放磁盘空间
gradle cleanBuildCache
  • 清除指定项目的依赖项缓存,并从远程仓库重新下载依赖项。
gradle build --refresh-dependencies

18 强制指定依赖

  • resolutionStrategy
configurations.all { it ->
	resolutionStrategy {
		force 'com.google:guava:lastest'
	}
}
  • mavenBom (io.spring.dependency-management 提供的插件)
    • 后面的 bom, 优先级比前面的高
    • 构建时占用内存大
      在 gradle.properties 中增加 org.gradle.jvmargs=-Xmx2024m -XX:MaxMetaspaceSize=512m, 如果是 jdk8 以下 -XX:MaxPermSize=512m
apply plugin: 'io.spring.dependency-management'

buildscript {
	ext {
		springBootVersion = "2.6.3"
	}
	dependencies {
		// spring-boot-gradle-plugin
		classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
		// or denpendency-management
		// classpath "io.spring.gradle:denpendency-management-plugin:1.0.8.RELEASE"
	}
}

dependencyManagement {
	imports {
		mavenBom "com.netflix.dependency:dependencies:1.2.3"
	}
	resolutionStrategy {
        cacheDynamicVersionsFor 0, "seconds"
        cacheChangingModulesFor 0, "seconds"
    }
}

更多用法:

Dependency Sets

dependencyManagement {
    dependencies {
        dependency('org.apache.activemq:activemq-spring:5.18.1') {
            exclude 'commons-logging:commons-logging'
        }
    }
}

详细请见: https://docs.spring.io/dependency-management-plugin/docs/current/reference/html/

  • enforcedPlatform 方式
    • 前面的 bom, 优先级比后面的高 (存在覆盖, 如果后面的 bom 又依赖的前面的 bom, 则前面 bom 定义的版本会被覆盖)
    • 构建占用内存小
dependencies {
	// enforcedPlatform 强制要求项目必须使用声明的版本, 确保所有项目之间的版本一致性
	implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:2.6.3"))
	// platform 也提供了依赖项版本的管理, 但不强制使用特定版本
	implementation platform("com.netflix.dependency:dependencies:1.2.3")
}

19 排除指定依赖

  • 单个

在 Gradle 中,可以使用 “exclude” 块来排除指定的依赖。在需要排除依赖的模块中,添加 “exclude” 块并指定要排除的依赖的组ID和artifact ID。如果需要排除多个依赖,也可以使用通配符来简化配置。例如:

dependencies {
    implementation('com.example:example-library:1.0') {
    	// 阻止单个依赖
        exclude group: 'org.unwanted', module: 'unwanted-*'
        
        // or 阻止 "com.example:example-library:1.0" 模块所依赖的 所有其他模块 被传递到我们的项目中
        transitive = false
    }
}

ps: 如果发现没有生效, 请排查: 1. 是否有别的依赖项传递了 unwanted 依赖; 2. 是否有别的依赖项传递了 example-library 依赖;

  • 全局排除

在 Gradle 中,可以使用 “configurations” 块来进行全局依赖排除。在 “configurations” 块中,可以定义一个名为 “all” 的配置,用于在整个项目中排除指定的依赖。例如:

configurations {
    all {
        exclude group: 'org.unwanted', module: 'unwanted-library'
    }
}

ps: 这种方法可能会影响到整个项目的构建过程,因此应该谨慎使用。如果只需要在特定模块中排除依赖,应该尽量避免在全局配置中进行排除

20 多模块项目, 父工程总是自动创建 src 目录, 如何不让他创建

  1. 当使用 IntelliJ IDEA 创建一个新的 Java 项目时,IDEA 默认会在项目根目录下自动创建 src 目录,并在其中创建 main 和 test 子目录。这是 IDEA 的默认行为,旨在遵循标准的项目结构和约定
  2. 当在 Gradle 构建脚本(如 build.gradle)中应用 Java 插件时,Java 插件会自动假设项目的源代码和资源文件位于 src/main 目录下。这也是 Gradle 的默认行为,旨在符合常见的项目布局约定
  3. IDEA 会识别有 java 插件的模块; 如果不想自动创建, 去掉对应 plugin 插件即可
  4. java-library 插件继承了 java 插件的所有功能,并添加了一些附加特性用于类库
plugins {
	// 去掉 java 插件
    id 'java'
	// or 去掉 java-library 插件
    id 'java-library'
}

21 使用 Gradle 生成 BOM 管理依赖版本

Reference: https://docs.gradle.org/current/userguide/java_platform_plugin.html

Gradle对BOM(Bill of Materials)的支持是从Gradle 6.3版本开始引入的。在Gradle 6.3及更高版本中,可以使用 dependencyManagement 块和 mavenBom 导入Maven BOM来管理依赖版本。

在 Gradle 6.3 之前的版本中,可能需要使用其他方式来管理依赖的版本,例如手动指定版本号或使用第三方插件来实现类似的功能, 而想要发布 BOM,需要使用 Maven 定义并发布 pom.xml 才可以.

好在 Gradle 6.3 开始,官方提供了一个名为 java-platform 插件,无需复杂配置,即可生成 BOM 并发布

  • 什么是 BOM?

BOM (The Bill of Materials in Maven) 是Maven中的概念,表现形式是一个 pom.xml 文件. 当传递的版本号不合要求,可自行定义版本号,覆盖 BOM 中的定义

  • 定义依赖管理
// build.gradle
plugins {
    id 'java-platform' // 引入 java-platform插件
    id 'maven-publish' // 发布插件, 可用来发布 BOM 或 jar 到本地与远程仓库
}

group 'com.sun'
version '0.1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

// 默认为防止用户错误地引入依赖, 而不是引入依赖约束, 如果引入依赖会报错失败
// 通过这个配置可以让 Gradle 允许引入依赖 (可选)
javaPlatform {
    allowDependencies()
}

dependencies {
    // 在 constraints 块中定义依赖版本约束
    constraints{
        api group: 'org.springframework.boot', name: 'spring-boot-starter', version: '2.6.3'
    }
}

publishing{
    publications{
        // 以下 maven 并非关键字,可自定义
        maven(MavenPublication) {
            // 通过 java-platform 生成 BOM
            from components.javaPlatform
        }
    }
}
// setting.gradle
rootProject.name = 'first-bom'
  • 发布
gradle -q publishToMavenLocal
  • 使用
plugins {
    id 'java'
}

group 'com.sun'
version '0.1.0-SNAPSHOT'

repositories {
    mavenLocal()
}

dependencies {
    // 依赖上文本地仓库中的BOM
    implementation platform('com.sun:first-bom:0.1.0-SNAPSHOT')
    // 不带依赖版本引包
    implementation 'org.springframework.boot:spring-boot-starter'
}

22 依赖标识

当使用 dependencies 命令时

# ./gradlew dependencies [--configuration <configuration>]
./gradlew dependencies --configuration compile > 1.txt

在版本号前后是有一些特殊标识的:->, ©, (*)
* omitted 第一次展示完整, 之后省略
c constraints 只限制最低版本, 不引入依赖 constraints { implementation "xxxx:xx:1.0.0" }
xxxx:xx:{strictly 1.0.0} 限制版本, 替换 force = true , implementation "xxxx:xx" { version { strictly '1.0.0' } }

Dependency resolution

https://docs.gradle.org/current/userguide/dependency_resolution.html

-> 标识代表 依赖冲突,也是在 dependencies graph 中最常见的一种标识。

比如 1.3.2 -> 1.6.0,表示当前依赖树中依赖的版本是 1.3.2,但由于全局的依赖冲突,最终被升级到了 1.6.0 版本。gradle 处理依赖冲突的总体原则是取冲突中的最高版本,但有很多特例。

特例情况我们本次不具体展开,只看常规情况,实际上仅是常规情况也有让人迷惑之处。

我们假设下面这样一个demo,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.0.0'
    implementation 'com.group_a:module_d:1.0.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
}

当我们查看 app module 的 dependencies 输出时(下文 dependencies 的输出都是基于 app module的),结果如下,

\--- com.group_a:module_a:1.0.0
     +--- com.group_a:module_c:1.0.0
     \--- com.group_a:module_d:1.0.0

现在引入依赖冲突,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.0.0'
    implementation 'com.group_a:module_d:1.0.0'
}

// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.1.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
    implementation 'com.group_a:module_b:1.0.0'
}

此时也比较简单,因为 module B 中依赖了 1.1.0 版本的 module C,依赖发生冲突以最高版本为准,所以最终 dependencies 的输出如下,此时 -> 表示了冲突的结果,

+--- com.group_a:module_a:1.0.0
|    +--- com.group_a:module_c:1.0.0 -> 1.1.0
|    \--- com.group_a:module_d:1.0.0
\--- com.group_a:module_b:1.0.0
     \--- com.group_a:module_c:1.1.0

再复杂一点,引入一个间接冲突,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.0.0'
    implementation 'com.group_a:module_d:1.0.0'
}

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.1.0'
    implementation 'com.group_a:module_d:1.1.0'
}

// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.1.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
    implementation 'com.group_a:module_b:1.0.0'
}

此时,module B 不再直接依赖 module C,而是通过依赖高版本的 module A,间接引入了 1.1.0 版本的 module C,dependencies 输出如下,

+--- com.group_a:module_a:1.0.0 -> 1.1.0
|    +--- com.group_a:module_c:1.1.0
|    \--- com.group_a:module_d:1.1.0
\--- com.group_a:module_b:1.0.0
     \--- com.group_a:module_a:1.1.0 (*)

注意此时 module A 到 module C 这条引用链上的版本标识:对于 module A,由于依赖冲突,版本变为 1.0.0 -> 1.1.0 ;但对于 module C,版本并不是 1.0.0 -> 1.1.0,而直接是 1.1.0。也就是说叶子节点的版本是以父节点版本的右值为准的。

如果我们再修改一下 demo,可以更清晰的解释这里的逻辑。我们把 demo 调整成如下这样,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.0.0'
    implementation 'com.group_a:module_d:1.0.0'
}

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.1.0'
    implementation 'com.group_a:module_d:1.1.0'
}

// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.1.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
    implementation 'com.group_a:module_b:1.0.0'
    implementation 'com.group_a:module_c:1.2.0'
}

module A、B、C、D 之间的依赖关系不变,但我们在 app module 直接依赖 1.2.0 版本的 module C,此时 dependencies 是怎样的呢?

+--- com.group_a:module_a:1.0.0 -> 1.1.0
|    +--- com.group_a:module_c:1.1.0 -> 1.2.0
|    \--- com.group_a:module_d:1.1.0
+--- com.group_a:module_b:1.0.0
|    \--- com.group_a:module_a:1.1.0 (*)
\--- com.group_a:module_c:1.2.0

可以很清晰的看到,对于发生冲突的版本:从父节点找子节点,看的是父节点的右值;而从子节点向父节点追溯,看的子节点的左值。

但单纯从视觉的直觉上看,我们可能会误以为 1.2.0 版本的 module C 是由 module A 引入的,导致排查问题时南辕北辙,特别在排查大型项目的 dependencies 输出时,一定要注意每一层节点之间的冲突版本的左值与右值。

Dependency omitted

在前面的 demo 里,(*) 这个标识已经出现过了,这个标识由于跟常规语境下的含义不太一样,所以也具有一定的迷惑性。(*) 符号字面意思是删减,但并不是依赖关系上的删减,仅仅是展示层面的删减。

还是以如下 demo 为例,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.0.0'
    implementation 'com.group_a:module_d:1.0.0'
}

// module B, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
}

// module E, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
}

// module F, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
    implementation 'com.group_a:module_b:1.0.0'
    implementation 'com.group_a:module_e:1.0.0'
    implementation 'com.group_a:module_f:1.0.0'
}

输出如下,

+--- com.group_a:module_a:1.0.0
|    +--- com.group_a:module_c:1.0.0
|    \--- com.group_a:module_d:1.0.0
+--- com.group_a:module_b:1.0.0
|    \--- com.group_a:module_a:1.0.0 (*)
+--- com.group_a:module_e:1.0.0
|    \--- com.group_a:module_a:1.0.0 (*)
\--- com.group_a:module_f:1.0.0
     \--- com.group_a:module_a:1.0.0 (*)

这里 (*) 代表省略了 module A 以下的依赖关系子树,因为假设我们按照 demo 来输出一个完整的依赖关系图,应该是下面这样的,

+--- com.group_a:module_a:1.0.0
|    +--- com.group_a:module_c:1.0.0
|    \--- com.group_a:module_d:1.0.0
+--- com.group_a:module_b:1.0.0
|    \--- com.group_a:module_a:1.0.0
|         +--- com.group_a:module_c:1.0.0
|         \--- com.group_a:module_d:1.0.0
+--- com.group_a:module_e:1.0.0
|    \--- com.group_a:module_a:1.0.0
|         +--- com.group_a:module_c:1.0.0
|         \--- com.group_a:module_d:1.0.0
\--- com.group_a:module_f:1.0.0
     \--- com.group_a:module_a:1.0.0
          +--- com.group_a:module_c:1.0.0
          \--- com.group_a:module_d:1.0.0

如果都按这种方式展示,显然冗余信息太多了,特别对于一个大型项目,依赖关系复杂时,几乎是不可阅读的。所以为了简洁、方便理解,dependencies 命令会默认缩略重复的依赖关系子树,只在它第一次出现时,才完整展示;后续出现都以 (*) 符号代替。

这也解释了为什么我们在从上向下阅读一个 dependencies graph 时,会感觉越靠近开头,依赖关系越复杂、层级越深,越靠近末尾依赖关系越简单。其实并不是因为 gradle 对依赖关系做了排序,仅仅是因为靠近尾部,大部分子树都被缩略掉了

Dependency constraints

https://docs.gradle.org/current/userguide/dependency_constraints.html

(c) 这个标识对应 dependecy constraint,这部分逻辑的具体解释可以参考 这个章节,它对应 gradle 中的 constraints 闭包(如下),

dependencies {
    constraints {
        implementation('com.group_a:module_c:1.1.0') {
            because 'previous versions have a bug impacting this application'
        }
    }
}

constraints 闭包的作用可以简单解释成不通过直接依赖来升级某个间接依赖的版本,比如下面这个 demo,

// module A, tag 1.0.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.0.0'
    implementation 'com.group_a:module_d:1.0.0'
}

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.1.0'
    implementation 'com.group_a:module_d:1.1.0'
}

// module A, tag 1.2.0, build.gradle
dependencies {
    implementation 'com.group_a:module_d:1.2.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
}

假设,现在我们发现 module C 在 1.0.0 版本有一个 bug,需要升级 module C 到 1.1.0 版本来修复;但囿于种种原因我们不能直接使用 1.1.0 版本的 module A,比如我们暂时不希望升级 module D 到 1.1.0 版本。

面对这种问题时,我们可能会按下面这种写法来规避,

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
    implementation 'com.group_a:module_c:1.1.0'
}

此时依赖关系如下,

+--- com.group_a:module_a:1.0.0
|    +--- com.group_a:module_c:1.0.0 -> 1.1.0
|    \--- com.group_a:module_d:1.0.0
\--- com.group_a:module_c:1.1.0

但这种写法的缺点是:我们引入了一个不必要的依赖,在 app module 直接依赖了 module C。

假设当 module A 升级到 1.2.0 版本,此时 module A 不再依赖 module C 了,

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.2.0'
    implementation 'com.group_a:module_c:1.1.0'
}

但由于我们显式的依赖了 module C,导致 module C 不再是由 module A 来引入,依赖关系发生了错乱,这并不符合我们的预期,特别在复杂项目中会引入很多不必要的麻烦。

+--- com.group_a:module_a:1.2.0
|    \--- com.group_a:module_d:1.2.0
\--- com.group_a:module_c:1.1.0

如果换成使用 constraints 闭包来实现上面的 demo 就不同了,

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.0.0'
    constraints {
        implementation 'com.group_a:module_c:1.1.0'
    }
}

此时,就会产生 (c) 这个标识,

+--- com.group_a:module_a:1.0.0
|    +--- com.group_a:module_c:1.0.0 -> 1.1.0
|    \--- com.group_a:module_d:1.0.0
\--- com.group_a:module_c:1.1.0 (c)

当 module A 升级到 1.2.0 之后,

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.2.0'
    constraints {
        implementation 'com.group_a:module_c:1.1.0'
    }
}

对 module C 的依赖会自动失效,

+--- com.group_a:module_a:1.2.0
     \--- com.group_a:module_d:1.2.0

而如果将 demo 改成这样,继续升级 module A,

// module A, tag 1.3.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.3.0'
    implementation 'com.group_a:module_d:1.3.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.3.0'
    constraints {
        implementation 'com.group_a:module_c:1.2.0'
    }
}

此时输出如下,依然会展示 (c) 标识,但最终版本选取了更高的 1.3.0 版本

+--- com.group_a:module_a:1.3.0
|    +--- com.group_a:module_c:1.3.0 
|    \--- com.group_a:module_d:1.3.0
\--- com.group_a:module_c:1.2.0 -> 1.3.0 (c)

同时对于 constraints 闭包,也可以用来实现 dependency version alignment。

https://docs.gradle.org/current/userguide/dependency_version_alignment.html

以文章开头展示的那个结果为例,这里 kotlinx-coroutines-bom 顾名思义是 kotlin 协程的 bom(Bill Of Materials)模块,

\--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.0
  +--- org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.0
  |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0 (c)
  |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0 (c)
  |    +--- org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.0 (c)
  |    \--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.0 (c)
  +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.0 (*)
  \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.0

在这个 bom 库的 build.gradle 文件 中,有如下逻辑,

dependencies {
    constraints {
        rootProject.subprojects.each {
            if (rootProject.ext.unpublished.contains(it.name)) return
            if (it.name == name) return
            if (!it.plugins.hasPlugin('maven-publish')) return
            evaluationDependsOn(it.path)
            it.publishing.publications.all {
                ...
                api(group: it.groupId, name: it.artifactId, version: it.version)
            }
        }
    }
}

本质上就是通过 constraints 闭包,来保证 kotlinx-coroutines-android、kotlinx-coroutines-core、kotlinx-coroutines-jdk8、kotlinx-coroutines-core-jvm 这几个子模块的版本一致。

可见,类似协程这种一对多的库,可以通过抽取一个 bom 模块,利用 constraints 闭包来约束各子 module 版本一致,避免由于版本不一致而引发的问题

Downgrading versions

https://docs.gradle.org/current/userguide/dependency_downgrade_and_exclude.html

与 dependecy constraint 对应的方案是 downgrading versions,用来处理依赖版本的降级,这里不过多介绍它们的用法,只看它们对dependencies输出的影响。

这里重点看一下 force 与 strictly 关键字的区别,还是如下 demo,比如我们想降级 module C 的版本

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.1.0'
    implementation 'com.group_a:module_d:1.1.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.1.0'
    implementation('com.group_a:module_c') {
        version {
            strictly '1.0.0'
        }
    }
}

此时 dependencies 输出如下,

+--- com.group_a:module_a:1.1.0
|    +--- com.group_a:module_c:1.1.0 -> 1.0.0
|    \--- com.group_a:module_d:1.1.0
\--- com.group_a:module_c:{strictly 1.0.0} -> 1.0.0

可以看到,在 dependencies 中有一个 strictly 关键字。

但如果使用 force 属性,写一个类似的 demo

// module A, tag 1.1.0, build.gradle
dependencies {
    implementation 'com.group_a:module_c:1.1.0'
    implementation 'com.group_a:module_d:1.1.0'
}

// app, build.gradle
dependencies {
    implementation 'com.group_a:module_a:1.1.0'
    implementation('com.group_a:module_c:1.0.0') {
        force = true
    }
}

得到 dependencies 输出如下,

+--- com.group_a:module_a:1.1.0
|    +--- com.group_a:module_c:1.1.0 -> 1.0.0
|    \--- com.group_a:module_d:1.1.0
\--- com.group_a:module_c:1.0.0

可读性则不如 strictly 关键字,没有任何标识能够区分,并且在 gradle 的较高版本,force 关键字已经被标记废弃了

  • 拓展: 术语介绍

https://docs.gradle.org/current/userguide/dependency_management_terminology.html


(完)

;