依赖管理:依赖冲突的根源分析、使用Gradle/Maven管理依赖、依赖隔离与瘦身策略、BOM的应用

依赖管理,说白了就是管好你项目里那些“拿来就用”的代码包。我做了这么多年SDK,见过太多因为依赖搞砸的项目。有的编译报错,有的运行时崩溃,有的包体积大得离谱。今天咱们就把这些坑一个个说清楚。

依赖冲突的根源分析

依赖冲突是怎么来的?其实就一句话:同一个库,不同版本,同时出现。

举个例子。你的SDK依赖了A库的1.0版本,而A库又依赖了C库的2.0。同时,你的SDK还依赖了B库,B库依赖了C库的3.0。这时候C库的2.0和3.0就撞车了。Gradle或Maven会选一个,但选错了就可能出问题。

我在项目中遇到过最典型的一次:某个网络库的2.0版本改了内部API签名,结果我们的SDK和宿主App各自依赖了不同版本。编译没问题,运行时直接NoSuchMethodError。查了两天才定位到。

依赖冲突的本质:传递依赖导致的版本不一致。你直接依赖的库可能只有几个,但传递依赖会像树一样扩散,冲突概率指数级上升。

为什么会这样?因为Java生态里,每个库都有自己的依赖树。你没法控制别人怎么依赖。你想想看,一个大型项目里,依赖树深度可能达到5层以上。每一层都可能引入冲突。

常见的冲突类型有三种:

  • 版本冲突:同一个库不同版本,API签名变了
  • 传递冲突:A依赖B1.0,C依赖B2.0,你同时用了A和C
  • 实现冲突:同一个接口有多个实现,比如SLF4J绑定多个日志框架

注意:不要以为编译通过就万事大吉。运行时冲突才是最恶心的。我曾经遇到过一个案例,编译完全正常,但App启动时因为类加载器加载了错误版本的类,直接闪退。这种问题排查起来特别痛苦。

使用Gradle/Maven管理依赖

Gradle和Maven是Java生态的两大依赖管理工具。我个人习惯用Gradle,因为它的DSL写起来更灵活。但Maven的XML结构更清晰,适合大型团队统一规范。

先说说Gradle怎么声明依赖:

// build.gradle
dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'
    api 'com.google.code.gson:gson:2.8.9'
    compileOnly 'org.projectlombok:lombok:1.18.24'
    runtimeOnly 'ch.qos.logback:logback-classic:1.2.11'
}

这里有个关键点:implementation和api的区别。implementation的依赖不会暴露给调用方,api会。我建议能用implementation就用implementation,能减少很多不必要的传递依赖。

Maven的写法类似:

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.3</version>
    <scope>compile</scope>
</dependency>

scope有compile、provided、runtime、test等。和Gradle的implementation、api、compileOnly、runtimeOnly一一对应。

我的习惯:每次加依赖前,先查一下这个库的传递依赖树。用gradle dependencies命令或者mvn dependency:tree。看一眼就知道会不会引入冲突。别偷懒,这一步能省你后面很多调试时间。

依赖隔离与瘦身策略

依赖隔离,说白了就是让你的SDK不“污染”宿主App的依赖环境。瘦身,就是去掉那些没用的依赖。

先说隔离。我常用的方法有几种:

  • 使用provided/compileOnly:如果某个库宿主App肯定会提供,就用compileOnly。比如Android的support库,宿主App肯定有,你就不用打包进去。
  • 类加载器隔离:对于极端情况,可以用自定义ClassLoader加载你的SDK依赖。但这种方式复杂度高,不推荐轻易使用。
  • 重命名依赖:用Shadow插件(Gradle)或maven-shade-plugin(Maven)把依赖的包名改掉。比如把com.google.gson改成com.yourcompany.shaded.gson。这样就不会和宿主App的Gson冲突了。

瘦身方面,我分享几个实战经验:

  • 去掉未使用的依赖:用gradle lint或mvn dependency:analyze检查哪些依赖声明了但没用。我见过一个项目,依赖了20多个库,实际用到的不到一半。
  • 使用更轻量的替代库:比如用OkHttp代替Apache HttpClient,用Gson代替Jackson(如果功能够用的话)。
  • 按需引入:有些库支持模块化引入,比如AndroidX的各个子模块。别一股脑全引进来。

避坑指南:我曾经接手过一个SDK,打包后体积有8MB。一查发现,里面打包了三个不同的JSON解析库:Gson、Jackson、Fastjson。问原因,说是不同模块各自为政。最后统一成Gson,体积直接降到3MB。所以,团队内部一定要统一技术栈。

BOM(Bill of Materials)的应用

BOM是什么?说白了就是一个“版本清单”。你定义一个BOM,里面列好各个库的版本号。其他模块引用时,只需要声明库的坐标,不用写版本号。版本号统一由BOM管理。

举个例子。你的SDK依赖了多个Spring Boot的库:

// 定义BOM
dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:2.7.0')
    // 下面这些库的版本号由BOM统一管理
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
}

Maven的BOM用法:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.7.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

BOM的好处很明显:

  • 版本统一:所有模块用同一个版本,不会出现A模块用2.0、B模块用3.0的情况
  • 升级方便:只需要改BOM里的版本号,所有模块自动更新
  • 减少冲突:BOM里已经帮你配好了兼容的版本组合

我的建议:如果你的SDK依赖了多个同生态的库(比如Spring全家桶、AndroidX全家桶),一定要用BOM。我自己维护的SDK,每个大版本都会发布一个对应的BOM。这样用户只需要依赖一个BOM,就能拿到所有兼容的版本。省心又省力。

嗯,这里要注意一点:BOM不是万能的。它只能管理你显式声明的依赖。如果某个库的传递依赖和BOM里的版本冲突,还是需要手动排除。所以,BOM + 依赖树检查,才是最佳实践。

警告:不要在一个项目里同时使用多个BOM。比如同时用Spring Boot的BOM和Spring Cloud的BOM。这两个BOM可能对同一个库指定了不同版本。这时候Gradle/Maven会随机选一个,结果可能不是你想要的。我见过有人因为这个折腾了一整天。

依赖管理知识体系 🔴 依赖冲突根源分析 • 版本冲突:同一库不同版本 • 传递冲突:依赖树多层嵌套 • 实现冲突:同一接口多实现 • 运行时冲突:编译通过但运行报错 💡 本质:传递依赖导致版本不一致 🛠️ Gradle/Maven管理依赖 • implementation vs api 区别 • scope/compileOnly/runtimeOnly • 依赖树检查命令 • 传递依赖控制策略 💡 优先用implementation减少暴露 📦 依赖隔离与瘦身策略 • compileOnly:宿主已提供则不用打包 • 类加载器隔离(极端情况) • Shadow插件重命名包名 • 去掉未使用依赖、统一技术栈 💡 瘦身后体积可减少50%以上 📋 BOM(Bill of Materials) • 版本清单:统一管理版本号 • 升级方便:改一处全更新 • 减少冲突:预配兼容版本组合 • 注意:不要同时用多个BOM 💡 同生态库建议必用BOM 核心目标:版本统一 · 体积最小 · 冲突为零

依赖管理这件事,说难不难,说简单也不简单。核心就是三句话:搞清楚依赖树,控制好传递依赖,用BOM统一版本。做到这三点,你的SDK在依赖这块基本不会出大问题。

可可精品资料库