仿Firebase的SDK BoM,实现自己的Android端SDK BoM版本统一管理
背景
大型的项目迭代个几年后框架底层功能模块的SDK一堆,各有各的版本号。而且sdk和sdk之间还相互依赖,很多时候配置不好还一堆冲突,简直一团乱麻。
这个时候发现项目中用的firebase,它也是很多sdk但是管理的就挺不错,使用起来非常清爽:
implementation platform('com.google.firebase:firebase-bom:xxx')
implementation 'com.google.firebase:firebase-messaging'
implementation 'com.google.firebase:firebase-analytics'
只要选好总的firebase-bom的版本号,子库需要哪个就选哪个,其版本号一概不需要关心。
查了查资料发现它使用的技术叫做BoM —— Bill of Materials。后知后觉发现很多地方有这个技术的影子,像Android的compose版本依赖管理也用到这个。
核心原理:
-
Maven BOM 机制: Firebase Android BoM 的底层机制是基于 Maven 的 BOM (Bill of Materials) 机制。BOM 本质上是一个特殊的 Maven POM 文件,它不包含实际的代码,而是定义了一组依赖项及其版本。
-
Gradle platform() 依赖约束: 在 Android 项目中,通过 Gradle 的 platform() 函数来引入 BOM 文件。platform() 函数的作用是导入 BOM 文件中定义的依赖版本约束。
-
版本统一管理: 当项目引入 BOM 后,在 dependencies 块中声明依赖项时,无需再指定版本号。Gradle 会自动从 BOM 文件中查找并使用预定义的版本,从而实现对多个库版本进行统一管理,确保版本兼容性
搞清楚原理后尝试给自己的sdk依赖管理也引入该技术。
实现步骤
准备工作
-
先创建Android 主工程
这一步没什么好说的常规操作,直接 new 一个 Android 工程出来即可
-
创建模拟用的 DNS 和 Share 两个library
结构如图所示,为了等会模拟调用,给每个 library 内部再定义一个 manager。
object EDnsManager { fun init() { Log.i("EDnsManager","init()") } } -
创建本地 Maven 仓库和 Aar Sdk 发布脚本
一般公司内部都有自己的 Maven 仓库,这里条件所限就直接用一个本地目录来模拟本地 Maven 仓库。
创建一个 gradle 脚本文件,用来将构建好的 sdk 发布到本地 Maven 仓库。
3.1 先使用 kts 格式的脚本:
创建文件publish-aar.gradle.kts,因为这个脚本功能其他模块都能用,所以防止在根目录下。
apply(plugin = "maven-publish") val GROUP_ID = project.findProperty("GROUP_ID") as String? ?: error("GROUP_ID not found") val ARTIFACT_ID = project.findProperty("ARTIFACT_ID") as String? ?: error("ARTIFACT_ID not found") val VERSION = project.findProperty("VERSION") as String? ?: error("VERSION not found") val DESCRIPTION = project.findProperty("DESCRIPTION") as String? ?: error("DESCRIPTION not found") val LOCAL_REPOSITORY_URL = project.findProperty("LOCAL_REPOSITORY_URL") as String? ?: error("LOCAL_REPOSITORY_URL not found") val REMOTE_RELEASE_REPOSITORY_URL = project.findProperty("REMOTE_RELEASE_REPOSITORY_URL") as String? ?: error("REMOTE_RELEASE_REPOSITORY_URL not found") val MAVEN_USERNAME = project.findProperty("MAVEN_USERNAME") as String? ?: error("MAVEN_USERNAME not found") val MAVEN_PASSWORD = project.findProperty("MAVEN_PASSWORD") as String? ?: error("MAVEN_PASSWORD not found") configure<PublishingExtension> { publications { create<MavenPublication>("maven") { groupId = GROUP_ID artifactId = ARTIFACT_ID version = VERSION artifact("$buildDir/outputs/aar/${project.name}-release.aar") pom.withXml { val dependenciesNode = asNode().appendNode("dependencies") configurations["implementation"].allDependencies.forEach { // 避免出现空节点或 artifactId=unspecified 的节点 if (it.group != null && it.name != null && it.name != "unspecified" && it.version != null) { println("dependency=${it.toString()}") val dependencyNode = dependenciesNode.appendNode("dependency") dependencyNode.appendNode("groupId", it.group) dependencyNode.appendNode("artifactId", it.name) dependencyNode.appendNode("version", it.version) dependencyNode.appendNode("scope", "implementation") } } } } } repositories { maven { name = "LocalMavenRepo" url = uri("file://${rootProject.projectDir}/$LOCAL_REPOSITORY_URL") } maven { name = "release" url = uri(REMOTE_RELEASE_REPOSITORY_URL) credentials { username = MAVEN_USERNAME password = MAVEN_PASSWORD } isAllowInsecureProtocol = true } } }3.2 再创建gradle.properties。 这里用来保持 sdk 的版本、group 等等信息,因为每个 sdk 的信息不一样,所以每个 sdk 工程防止一个。dns 工程的如下:
VERSION=1.0.1 GROUP_ID=com.evan ARTIFACT_ID=dns LOCAL_REPOSITORY_URL=local-maven-repo # 远端Maven的地址,直接填入http或者https的地址即可 REMOTE_RELEASE_REPOSITORY_URL= # 远端Maven仓库的密码和用户名 MAVEN_USERNAME= MAVEN_PASSWORD= # 项目描述,这里是本地仓库就没启用这个字段 DESCRIPTION= library #WEBSITE_URL= # Issue ???? #ISSUE_TRACKER_URL=要说明的几点是:
1)kts 格式和之前的 groovy 不一样,gradle.properties中的属性没法直接使用,需要处理一下:val GROUP_ID = project.findProperty("GROUP_ID") as String? ?: error("GROUP_ID not found");
2)记得补充脚本中的pom.withXml 部分,将sdk自身的依赖关系也打进去。要不然后期使用发布好的sdk时它自身需要的sdk没法自动获取。它在仓库中的pom文件就会显示成这样:
3)第三个就是isAllowInsecureProtocol = true, 这里的名称同旧版本相比多了个is。如果配置的远端Maven仓库使用的还是http协议记得加上这句
-
另外一个share 测试library 配置同dns。
4.1 发布sdk到本地的仓库目录需要先将aar构建出来,文章中的示例没有将构建和发布做关联。发布前可以在studio中执行构建:
也可以用命令行: ./gradlew :dnslibrary:assembleRelease 来构建(这里只发布release 版本sdk,所以构建release 版本)
4.2 使用 Android studio发布到本地 Maven 仓库,
命令行发布:./gradlew :dnslibrary:publishMavenPublicationToLocalMavenRepoRepository
正式开发,创建BoM部分
-
创建 BoM 目录和需要的文件
1.1 在项目根目录下创建一个目录,名为‘bom'。
1.2 创建发布脚本文件 publish-bom.gradle.kts 文件,内容如下:
apply(plugin = "maven-publish") val GROUP_ID = project.findProperty("GROUP_ID") as String? ?: error("GROUP_ID not found") val ARTIFACT_ID = project.findProperty("ARTIFACT_ID") as String? ?: error("ARTIFACT_ID not found") val VERSION = project.findProperty("VERSION") as String? ?: error("VERSION not found") val DESCRIPTION = project.findProperty("DESCRIPTION") as String? ?: error("DESCRIPTION not found") val LOCAL_REPOSITORY_URL = project.findProperty("LOCAL_REPOSITORY_URL") as String? ?: error("LOCAL_REPOSITORY_URL not found") val REMOTE_RELEASE_REPOSITORY_URL = project.findProperty("REMOTE_RELEASE_REPOSITORY_URL") as String? ?: error("REMOTE_RELEASE_REPOSITORY_URL not found") val MAVEN_USERNAME = project.findProperty("MAVEN_USERNAME") as String? ?: error("MAVEN_USERNAME not found") val MAVEN_PASSWORD = project.findProperty("MAVEN_PASSWORD") as String? ?: error("MAVEN_PASSWORD not found") configure<PublishingExtension> { publications { create<MavenPublication>("maven") { groupId = GROUP_ID artifactId = ARTIFACT_ID version = VERSION from(components.getByName("javaPlatform")) } } repositories { maven { name = "LocalMavenRepo" url = uri("file://${rootProject.projectDir}/$LOCAL_REPOSITORY_URL") } maven { name = "release" url = uri(REMOTE_RELEASE_REPOSITORY_URL) credentials { username = MAVEN_USERNAME password = MAVEN_PASSWORD } isAllowInsecureProtocol = true } } }内容和之前发布 aar 的大同小异,只是没有了 pom xml 部分,增加了一个 from(components.getByName("javaPlatform"))
1.3 创建 gradle.properties管理 BoM的版本号等信息,和刚才 sdk library的配置信息大同小异。
VERSION=1.0.0 GROUP_ID=com.evan ARTIFACT_ID=library-BoM LOCAL_REPOSITORY_URL=local-maven-repo REMOTE_RELEASE_REPOSITORY_URL= MAVEN_USERNAME= MAVEN_PASSWORD= DESCRIPTION= library #WEBSITE_URL= # Issue ???? #ISSUE_TRACKER_URL=1.4 创建入口 gradle 文件,build.gradle.kts,内容如下所示:
plugins { `java-platform` } apply(from = "publish-bom.gradle.kts") dependencies { constraints { api("com.evan:dns:1.0.1") api("com.evan:share:1.0.0") } }能做成BoM 版本依赖管理最核心的就是这个gradle脚本文件,尤其是这里的constraints。constraints 块的主要作用是声明版本约束而不实际引入依赖。这是 BoM 最核心的机制,它:1)仅定义版本号,不引入实际依赖;2)允许统一管理一组相关库的版本;3)提供版本建议,而不是强制版本。
我们的示例中只用到api 版本约束,直接定死每个library的版本人为的为这一组sdk控制好我们需要的版本。这样外部接入的时候只需要关注BoM的版本即可,内部的子library就不需要过多关注。
-
运行,将bom发布到本地仓库
2.1 发布前需要先将bom的gradle脚本勾起来,因为我们的工程是个Android示例工程。
在项目的根路径下找到 settings.gradle.kts,在其中加一句include(":bom") 即可。
如图:
2.2 同步项目,此时就可以将项目发布出去。
如上图,和之前一样可以在Android Studio中直接发布点击publishMavenPublicationToLocalMavenRepoRepository,也可以使用命令行来发布:./gradlew :bom:publishMavenPublicationToLocalMavenRepoRepository
在我们的本地仓库中就能看见发布过去的 BoM project:
到这一步发布环节就告一段落了。下面再试着将BoM 接入到 App 中,看是否真的和 Firebase BoM效果一样。
验证
-
先配置好仓库。找到项目的 settings.gradle.kts文件在dependencyResolutionManagement 部分增加本地仓库配置。(要不然我们在 App模块的 gradle 文件中配置 BoM引用是无法获取到的),完成后如下:
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url = uri("file:///${rootProject.projectDir}/local-maven-repo") } } } -
在 App 的 build.gradle 中引用 BoM,代码如下:
implementation(platform("com.evan:library-BoM:1.0.0")) implementation("com.evan:dns") implementation("com.evan:share")同步项目后发现External Libraries 中:
说明包的引入没问题,代码中我们也可以调用:
EDnsManager.init() EShareManager.init()
总结
- BoM 的核心作用在于为整个项目提供统一的依赖版本控制。通过创建一个专门用于管理依赖版本的 BoM 模块,并将其发布到本地仓库后引用,您可以确保所有模块在使用相同库时版本一致。这不仅提高了项目的稳定性和可维护性,还减少了手动更新和协调依赖版本的工作量。
- BoM 不仅是一个版本管理工具,更是架构治理的重要组成部分。它可以与模块化架构结合,形成更完善的依赖管理体系,使项目的版本控制更加清晰,降低维护成本,并提升开发协作的效率。