仿Firebase的SDK BoM,实现自己的Android端SDK BoM版本统一管理

483 阅读3分钟

仿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依赖管理也引入该技术。

实现步骤

准备工作

  1. 先创建Android 主工程

    这一步没什么好说的常规操作,直接 new 一个 Android 工程出来即可

  2. 创建模拟用的 DNS 和 Share 两个library

    结构如图所示,为了等会模拟调用,给每个 library 内部再定义一个 manager。

    object EDnsManager {
        fun init() {
            Log.i("EDnsManager","init()")
        }
    }
    
  3. 创建本地 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协议记得加上这句

  4. 另外一个share 测试library 配置同dns。

    4.1 发布sdk到本地的仓库目录需要先将aar构建出来,文章中的示例没有将构建和发布做关联。发布前可以在studio中执行构建:

    也可以用命令行: ./gradlew :dnslibrary:assembleRelease 来构建(这里只发布release 版本sdk,所以构建release 版本)

    4.2 使用 Android studio发布到本地 Maven 仓库,

    命令行发布:./gradlew :dnslibrary:publishMavenPublicationToLocalMavenRepoRepository

正式开发,创建BoM部分

  1. 创建 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就不需要过多关注。

  2. 运行,将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效果一样。

验证

  1. 先配置好仓库。找到项目的 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")
            }
        }
    }
    
  2. 在 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()
    

总结

  1. BoM 的核心作用在于为整个项目提供统一的依赖版本控制。通过创建一个专门用于管理依赖版本的 BoM 模块,并将其发布到本地仓库后引用,您可以确保所有模块在使用相同库时版本一致。这不仅提高了项目的稳定性和可维护性,还减少了手动更新和协调依赖版本的工作量。
  2. BoM 不仅是一个版本管理工具,更是架构治理的重要组成部分。它可以与模块化架构结合,形成更完善的依赖管理体系,使项目的版本控制更加清晰,降低维护成本,并提升开发协作的效率。