【Android辟邪】之:gradle——在项目间共享依赖关系版本

翻译和简单修改自:https://docs.gradle.org/current/userguide/platforms.html#sec:sharing-catalogs

建议看原文(有能力的话)

现在 Gradle 脚本可以使用两种语法编写:Kotlin 和 Groovy
本文只使用kotlin脚本语法,更详细请查看Gradle官网(上面的网址)

在项目间共享依赖关系版本

  1. 集中声明依赖关系
  2. 共享目录
  3. 使用平台控制传递版本
  4. 导入 Maven BOM
  5. 我应该使用平台还是目录?

1. 集中声明依赖关系

1.1 使用版本目录

版本目录是一个以依赖关系坐标表示的依赖关系列表,用户在构建脚本中声明依赖关系时可从中挑选。

例如,可以从版本目录中选择依赖关系坐标,而不是使用字符串符号来声明依赖关系:

例 1. 使用版本目录中声明的库 build.gradle.kts

dependencies {
    implementation(libs.groovy.core)
}

在这里,libs 就是一个目录,而 groovy 则代表该目录中的一个依赖项。与直接在构建脚本中声明依赖关系相比,版本目录有很多优点:

对于每个目录,Gradle 都会生成类型安全的访问器,这样就可以在集成开发环境中通过自动补全轻松添加依赖关系。

每个目录对构建过程中的所有项目都可见。它是声明依赖项版本的中心位置,可确保对该版本的修改适用于每个子项目。

目录可以在多个依赖项之间共享版本声明。

目录可以将依赖项的组和名称与其实际版本分开,并使用版本引用来代替,这样就可以在多个依赖项之间共享一个版本声明。

使用 libs.someLib 符号添加依赖关系的效果与直接在构建脚本中硬编码组、工件和版本的效果完全相同。
依赖关系目录并不强制执行依赖关系的版本:就像普通的依赖关系符号一样,它声明所请求的版本或丰富的版本。该版本并不一定是冲突解决时选择的版本。

1.2 声明版本目录

版本目录可以在 settings.gradle(.kts) 文件中声明。在上面的例子中,为了让 groovy 可以通过 libs 目录使用,我们需要将别名与 GAV(组、工件、版本)坐标关联起来:

示例 2. 声明版本目录 settings.gradle.kts

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            library("groovy-core", "org.codehaus.groovy:groovy:3.0.5")
            library("groovy-json", "org.codehaus.groovy:groovy-json:3.0.5")
            library("groovy-nio", "org.codehaus.groovy:groovy-nio:3.0.5")
            library("commons-lang3", "org.apache.commons", "commons-lang3").version {
                strictly("[3.8, 4.0[")
                prefer("3.9")
            }
        }
    }
}

1.3 别名及其与类型安全访问器的映射

别名必须由一系列标识符组成,中间用破折号 (-,推荐使用)、下划线 (_) 或点 (.)隔开。标识符本身必须由 ascii 字符(最好小写)组成,最后跟上数字。

例如

guava 是一个有效别名

groovy-core 是一个有效的别名

commons-lang3 是一个有效的别名

androidx.awesome.lib 也是一个有效别名
但这 #is.not!

然后为每个子组生成类型安全访问器。例如,在名为 libs 的版本目录中给出以下别名:

guava、groovy-core、groovy-xml、groovy-json、androidx.awesome.lib

我们将生成以下类型安全的访问器:

libs.guava

libs.groovy.core

libs.groovy.xml

libs.groovy.j

libs.androidx.awesome.lib

其中 libs 前缀来自版本目录名称。

如果想避免生成子组访问器,我们建议使用大小写来区分。例如,别名 groovyCore、groovyJson 和 groovyXml 将分别映射到 libs.groovyCore、libs.groovyJson 和 libs.groovyXml 访问器。

在声明别名时,值得注意的是-、_和.字符都可以用作分隔符,但生成的目录将全部规范化为.:例如,foo-bar 作为别名会自动转换为 foo.bar。

有些关键字是保留的,因此不能用作别名。下一个词不能用作别名:

扩展

类

约定

下一个单词 c

捆绑包

版本

插件

因此,对于依赖项来说,别名 versions-dependency 是无效的,而 versionsDependency 或 dependency-versions 则是有效的。

1.4 版本号相同的依赖项

在声明版本目录的第一个例子中,我们可以看到,我们为 groovy 库的不同组件声明了 3 个别名,所有这些别名都共享同一个版本号。

我们可以声明一个版本并引用它,而不是重复相同的版本号:

例 3. 与库分开声明版本 settings.gradle.kts

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            version("groovy", "3.0.5")
            version("checkstyle", "8.37")
            library("groovy-core", "org.codehaus.groovy", "groovy").versionRef("groovy")
            library("groovy-json", "org.codehaus.groovy", "groovy-json").versionRef("groovy")
            library("groovy-nio", "org.codehaus.groovy", "groovy-nio").versionRef("groovy")
            library("commons-lang3", "org.apache.commons", "commons-lang3").version {
                strictly("[3.8, 4.0[")
                prefer("3.9")
            }
        }
    }
}

例 4. 使用版本目录中声明的版本 build.gradle.kts

单独声明的版本也可通过类型安全访问器获得,这使得它们比依赖版本适用于更多用例,尤其是工具:

checkstyle {
    // will use the version declared in the catalog
    toolVersion = libs.versions.checkstyle.get()
}

例 5. 在有更具体的别名时使用版本目录中的版本 build.gradle.kts

如果已声明版本的别名也是某些更具体别名的前缀,
如 libs.versions.zinc 和 libs.version.zinc.apiinfo,
则可通过类型安全访问器上的 asProvider() 获取更通用版本的值:

scala {
    zincVersion = libs.versions.zinc.asProvider().get()
}

例 6. 依赖关系符号对应build.gradle.kts

目录中声明的依赖关系会通过与其名称相对应的扩展名暴露给联编脚本。
在上例中,由于在设置中声明的目录名为 libs
因此在当前联编的所有联编脚本中,扩展名都是 libs
所以可以使用以下符号(x.x.x)声明依赖关系:

dependencies {
    implementation(libs.groovy.core)
    implementation(libs.groovy.json)
    implementation(libs.groovy.nio)
}

例 7. 依赖关系符号对应 build.gradle.kts

这个(x.x.x)与上面编写的效果完全相同:

dependencies {
    implementation("org.codehaus.groovy:groovy:3.0.5")
    implementation("org.codehaus.groovy:groovy-json:3.0.5")
    implementation("org.codehaus.groovy:groovy-nio:3.0.5")
}

目录中声明的版本是丰富版本。有关完整的版本声明支持文档,请参阅版本目录生成器 API。

1.5 依赖关系包

由于在不同的项目中经常会同时使用某些依赖包,因此版本目录提供了 "依赖包 "的概念。

例 8. 使用依赖关系包 build.gradle.kts

依赖包基本上是多个依赖项的别名。
例如,你可以这样写,而不是像上面那样声明 3 个单独的依赖项:

dependencies {
    implementation(libs.bundles.groovy)
}

例 9. 声明依赖包 settings.gradle.kts

目录中需要声明名为 groovy 的 bundle:

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            version("groovy", "3.0.5")
            version("checkstyle", "8.37")
            library("groovy-core", "org.codehaus.groovy", "groovy").versionRef("groovy")
            library("groovy-json", "org.codehaus.groovy", "groovy-json").versionRef("groovy")
            library("groovy-nio", "org.codehaus.groovy", "groovy-nio").versionRef("groovy")
            library("commons-lang3", "org.apache.commons", "commons-lang3").version {
                strictly("[3.8, 4.0[")
                prefer("3.9")
            }
            bundle("groovy", listOf("groovy-core", "groovy-json", "groovy-nio"))
        }
    }
}

语义也是等同的:添加一个单独的捆绑包相当于单独添加属于该捆绑包的所有依赖项

1.6 插件

外,版本目录还支持声明插件版本。

是通过组、工件和版本坐标(group, artifact and version)来表示的,

例 10. 声明插件版本 settings.gradle.kts

Gradle 插件只通过 id 和版本来标识。因此,它们需要单独声明:

提示:您不能在settings 文件或settings 插件中使用版本目录中声明的插件(因为目录本身是在settings 中定义的,这将是一个鸡和蛋的问题)。

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            plugin("versions", "com.github.ben-manes.versions").version("0.45.0")
        }
    }
}

例 11. 使用在目录中声明的插件 build.gradle.kts

然后,该插件可在插件块中访问,并可在构建过程中的任何项目中使用:

plugins {
    `java-library`
    checkstyle
    alias(libs.plugins.versions)
}

1.7 使用多个目录

除了传统的 lib 目录,您还可以通过设置 API 声明任意数量的目录。
这样,您就可以将多个源中的依赖关系声明分开,从而使您的编译过程更加高效。

例 12. 使用自定义目录 settings.gradle.kts

dependencyResolutionManagement {
    versionCatalogs {
        create("testLibs") {
            val junit5 = version("junit5", "5.7.1")
            library("junit-api", "org.junit.jupiter", "junit-jupiter-api").versionRef(junit5)
            library("junit-engine", "org.junit.jupiter", "junit-jupiter-engine").versionRef(junit5)
        }
    }
}

每个目录都会生成一个扩展名,应用于所有项目以访问其内容。因此,选择一个能减少潜在冲突的名称来降低碰撞的几率是有意义的。例如,可以选择以 Libs 结尾的名称。

1.8 libs.versions.toml 文件

除了上述的设置 API,Gradle 还提供了一个常规文件来声明目录。如果在根编译gradle 子目录中找到 libs.versions.toml 文件,那么目录就会根据该文件的内容自动声明。

声明 libs.versions.toml 文件并不能使其成为依赖关系的唯一来源:它只是一个可以声明依赖关系的常规位置。一旦开始使用目录,强烈建议在目录中声明所有依赖关系而不是构建脚本中硬编码组/工件/版本字符串。请注意,插件可能会添加依赖项,而这些依赖项是在此文件之外定义的。

就像 src/main/java 是查找 Java 源代码的约定一样,它并不妨碍其他源代码目录的声明(无论是在联编脚本中还是在插件中),libs.version.toml 文件的存在也不妨碍在其他地方声明依赖关系。

不过,该文件的存在表明,大多数依赖项(即使不是全部依赖项)都将在该文件中声明。因此,对于大多数用户来说,更新依赖版本只需修改该文件中的一行即可。

默认情况下,libs.version.toml 文件将作为 libs 目录的输入。您可以更改默认目录的名称,例如您已经有了一个同名的扩展名:

例 13. 更改默认扩展名 settings.gradle.kts

dependencyResolutionManagement {
    defaultLibrariesExtensionName = "projectLibs"
}

1.9 libs.versions.toml 文件的格式

TOML 文件由 4 个主要部分组成:

[版本]部分:用于声明可被依赖项引用的版本

[库]部分:用于声明坐标别名

[捆绑包]部分:用于声明依赖捆绑包

[插件]部分:用于声明插件

例如 libs.versions.toml 文件

[versions]
groovy = "3.0.5"
checkstyle = "8.37"

[libraries]
groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" } # 注释:version.ref = "groovy" 是解析 [versions] 下的 groovy 变量为 version
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]
groovy = ["groovy-core", "groovy-json", "groovy-nio"]

[plugins]
versions = { id = "com.github.ben-manes.versions", version = "0.45.0" }

版本既可以以单个字符串的形式声明(在这种情况下,它们被解释为必选版本),

也可以以丰富版本的形式声明:

[versions]
my-lib = { strictly = "[1.0, 2.0[", prefer = "1.2" }

版本声明的支持成员有

require:要求版本

strictly: 严格版本

prefer:首选版本

reject:被拒绝版本的列表

rejectAll:布尔值,用于拒绝所有版本

依赖关系声明可以是一个简单的字符串,在这种情况下,它们被解释为 group:artifact:version 坐标。

1.10 不同的依赖关系符号

或者将版本声明从组和名称中分离出来:

提示:对于别名,"别名及其与类型安全访问器的映射 "一节中描述的规则同样适用。

[versions]
common = "1.4"

[libraries]
my-lib = "com.mycompany:mylib:1.4"
my-other-lib = { module = "com.mycompany:other", version = "1.4" }
my-other-lib2 = { group = "com.mycompany", name = "alternate", version = "1.4" }
mylib-full-format = { group = "com.mycompany", name = "alternate", version = { require = "1.4" } }

[plugins]
short-notation = "some.plugin.id:1.4"
long-notation = { id = "some.plugin.id", version = "1.4" }
reference-notation = { id = "some.plugin.id", version.ref = "common" }

如果要引用 [versions] 部分声明的版本,应使用 version.ref 属性:

[versions]
some = "1.4"

[libraries]
my-lib = { group = "com.mycompany", name="mylib", version.ref="some" }

TOML 文件格式非常宽松,允许编写 "点 "属性作为完整对象声明的快捷方式。例如

a.b.c="d"
等同于:
a.b = { c = "d" }
或
a = { b = { c = "d" } }

详见 TOML 规范。

1.11 类型不安全 API

版本目录(Version catalogs)可通过类型不安全 API 访问。
该 API 在生成的访问器不可用的情况下可用。
它可通过版本目录扩展访问:build.gradle.kts

val versionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
println("Library aliases: ${versionCatalog.libraryAliases}")
dependencies {
    versionCatalog.findLibrary("groovy-json").ifPresent {
        implementation(it)
    }
}

查看版本目录 API,了解所有支持的方法。

2. 共享目录

版本目录用于单个构建(Build)(可能是多项目构建),但也可能在不同构建(Build)之间共享。

例如,一个组织可能希望创建一个依赖项目录,供来自不同团队不同项目使用。

2.1 从 TOML 文件导入目录

版本目录生成器 API 支持包含来自外部文件的模型
这样就可以在需要时为 buildSrc 重用主构建的目录。

例 14. 与 buildSrc 共享依赖关系目录settings.gradle.kts

例如,buildSrc/settings.gradle(.kts) 文件可以通过使用

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

使用 VersionCatalogBuilder.from(Object dependencyNotation) 方法时,只接受单个文件
这意味着,Project.files(java.lang.Object…) 等注释必须指向一个文件,否则构建将失败。

如果需要更复杂的结构(从多个文件导入版本目录),建议使用基于代码的方法而不是 TOML 文件

例 15. 声明其他目录 settings.gradle.kts

因此,这种技术可用于从不同文件中声明多个目录:

dependencyResolutionManagement {
    versionCatalogs {
        // declares an additional catalog, named 'testLibs', from the 'test-libs.versions.toml' file
        create("testLibs") {
            from(files("gradle/test-libs.versions.toml"))
        }
    }
}

2.2 版本目录插件(plugins)

虽然从本地文件导入目录很方便,但并不能解决在组织内部或为外部用户共享目录的问题。
共享目录的一种方法是编写一个设置插件,发布到 Gradle 插件门户或内部资源库,然后让用户在他们的设置文件中应用该插件。

另外,Gradle 还提供了版本目录插件,可以声明并发布目录。
要做到这一点,你需要应用版本目录插件:

例 16. 应用版本目录插件 build.gradle.kts

plugins {
    `version-catalog`
    `maven-publish`
}

然后,该插件将公开目录扩展,你可以用它来声明目录:

例 17. 目录定义 build.gradle.kts

catalog {
    // declare the aliases, bundles and versions in this block
    versionCatalog {
        library("my-lib", "com.mycompany:mylib:1.2")
    }
}

应用 maven-publish 或 ivy-publish 插件,并将发布配置为使用 versionCatalog 组件,就可以发布这样的目录:

例 18. 发布目录 build.gradle.kts

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["versionCatalog"])
        }
    }
}

发布此类项目时,会自动生成(并上传)libs.version.toml 文件,其他 Gradle 版本可以使用该文件。

2.3 导入已发布的目录

版本目录插件生成的目录可以通过设置 API 导入:

例 19. 使用发布的目录 settings.gradle.kts

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from("com.mycompany:catalog:1.0")
        }
    }
}

2.4 覆盖目录版本

如果目录声明了版本,则可以在导入目录时覆盖该版本:

例 20. 覆盖已发布目录中声明的版本 settings.gradle.kts

dependencyResolutionManagement {
    versionCatalogs {
        create("amendedLibs") {
            from("com.mycompany:catalog:1.0")
            // overwrite the "groovy" version declared in the imported catalog
            version("groovy", "3.0.6")
        }
    }
}

在上例中,任何使用 groovy 版本作为参考的依赖关系都将自动更新为使用 3.0.6。
再次重申,覆盖版本并不意味着实际解析的依赖关系版本也会相同:这只是改变了导入的版本,即声明依赖关系时使用的版本。如果有冲突,实际版本将采用传统的冲突解决方式。

3. 使用平台控制传递版本

平台是一种特殊的软件组件,可用于控制跨依赖关系版本。在大多数情况下,它完全由依赖关系约束组成,这些约束会建议依赖关系版本或强制执行某些版本。因此,在需要在项目间共享依赖版本时,这是一个完美的工具。在这种情况下,项目通常是这样组织的:

一个平台项目,为不同子项目中的各种依赖关系定义约束条件

若干依赖于平台并声明无版本依赖关系的子项目

在 Java 生态系统中,Gradle 为此提供了一个插件。

平台作为 Maven BOM 发布也很常见,Gradle 本身也支持这些平台。

使用 platform 关键字可以创建对平台的依赖关系:

例 21. 获取平台声明的版本 build.gradle.kts

dependencies {
    // get recommended versions from the platform project
    api(platform(project(":platform")))
    // no version required
    api("commons-httpclient:commons-httpclient")
}

这个平台符号是一个简写符号,实际上在引擎盖下执行了几个操作:

它将 org.gradle.category 属性设置为 platform,这意味着 Gradle 将选择依赖关系中的平台组件。

默认设置 endorseStrictVersions 行为,这意味着如果平台声明了严格的依赖关系,就会强制执行。

这意味着,默认情况下,对某个平台的依赖会触发对该平台定义的所有严格版本的继承,这对平台作者来说非常有用,可以确保所有用户都尊重他们在依赖版本方面的决定。可以通过显式调用 doNotEndorseStrictVersions 方法来关闭此功能。

4. 导入 Maven BOM

Gradle 支持导入物料清单(BOM)文件,这些文件实际上是 .pom 文件,使用 来控制直接依赖和传递依赖的依赖版本。Gradle 中的 BOM 支持与 Maven 中依赖 BOM 时使用 import类似。但在 Gradle 中,它是通过 BOM 上的常规依赖声明来实现的:

例 22. 依赖 BOM 导入其依赖约束 build.gradle.kts

dependencies {
    // import a BOM
    implementation(platform("org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE"))

    // define dependencies without versions
    implementation("com.google.code.gson:gson")
    implementation("dom4j:dom4j")
}

在示例中,gson 和 dom4j 的版本由 Spring Boot BOM 提供。这样,如果你是为 Spring Boot 这样的平台开发,就不必自己声明任何版本,而可以依赖平台提供的版本。

Gradle 对 BOM 的 块中所有条目的处理方式与 Gradle 的依赖约束类似。这意味着 块中定义的任何版本都会影响依赖关系的解析结果。要成为 BOM,.pom 文件需要设置 pom。

不过,BOM 通常不仅提供版本作为建议,还提供了一种覆盖图中任何其他版本的方法。您可以在导入 BOM 时使用 enforcedPlatform 关键字而不是 platform 来启用这一行为:

例 23. 导入 BOM,确保其定义的版本覆盖找到的任何其他版本 build.gradle.kts

dependencies {
    // import a BOM. The versions used in this file will override any other version found in the graph
    implementation(enforcedPlatform("org.springframework.boot:spring-boot-dependencies:1.5.8.RELEASE"))

    // define dependencies without versions
    implementation("com.google.code.gson:gson")
    implementation("dom4j:dom4j")

    // this version will be overridden by the one found in the BOM
    implementation("org.codehaus.groovy:groovy:1.8.6")
}

如果您的软件组件可被其他组件使用,则需要谨慎考虑使用 enforcedPlatform。该声明实际上是传递性的,因此将适用于用户的依赖关系图。不幸的是,如果他们不同意其中一个强制版本,就必须使用排除法。相反,如果你的可重用软件组件对某些第三方依赖版本有强烈的意见,可以考虑使用严格的富版本声明。

5. 我应该使用平台还是目录?

由于平台和目录都会涉及依赖版本,而且都可以用于在项目中共享依赖版本,因此在使用平台和目录时可能会出现混淆,不知道哪一种更好。

简而言之,你应该

使用目录为项目定义依赖关系及其版本,并生成类型安全的访问器

使用平台将版本应用于依赖关系图并影响依赖关系的解析

目录有助于集中化依赖关系版本,顾名思义,它只是一个可供挑选的依赖关系目录。我们建议在任何情况下都使用它来声明依赖关系的坐标。Gradle 会使用它来生成类型安全的访问器,为外部依赖关系提供简短的符号,并允许不同项目之间轻松共享这些坐标。使用目录不会对下游用户产生任何影响:这对他们来说是透明的。

平台是一个更重要的

平台中定义的限制不仅会影响项目的直接依赖关系,还会影响传递依赖关系。

平台是版本化的,图中的传递依赖关系可能依赖于平台的不同版本,从而导致各种依赖关系升级。

平台可以将组件绑定在一起,特别是可以用作调整版本的结构。

对平台的依赖关系会被依赖关系的用户 "继承":这意味着对平台的依赖关系会影响用户使用的库版本,即使用户并不直接或间接依赖平台引用的组件。

总之,使用目录总是一种很好的工程实践,因为它可以集中通用定义、共享依赖版本或插件版本,但它只是构建的 “实现细节”:消费者看不到它,目录中未使用的元素也会被忽略。

平台的目的是影响依赖关系解析图,例如通过添加对传递依赖关系的约束:这是一种结构化依赖关系图并影响解析结果的解决方案。

实际上,你的项目既可以使用目录,也可以声明一个使用目录的平台:

例 24. 在平台定义中使用目录 build.gradle.kts

plugins {
    `java-platform`
}

dependencies {
    constraints {
        api(libs.mylib)
    }
}

你可能感兴趣的:(android)