依赖管理基础:依赖类型、依赖树、传递性依赖
依赖管理这词,听起来挺唬人。
说白了就是帮你搞清楚:你的项目到底用了哪些第三方库?这些库之间会不会打架?我刚开始做跨平台SDK时,也踩过不少坑。有一次,两个SDK内部都引了同一个日志库的不同版本,结果运行时直接崩溃。嗯,从那以后,我再也不敢小看依赖管理了。
依赖的几种类型
先说说依赖的类型。不同的构建工具,叫法可能有点差异,但本质就那几类。我个人习惯把它们分成三大类:
- 直接依赖:你在配置文件里主动声明引入的库。比如你在
build.gradle里写了个implementation 'com.squareup.okhttp3:okhttp:4.9.0',那okhttp 4.9.0就是直接依赖。 - 间接依赖(也叫传递性依赖):你引入的库,它自己又依赖了其他库。比如
okhttp内部依赖了okio,那okio就是你的间接依赖。你并没有主动管它,但它确实存在。 - 编译依赖 vs 运行时依赖:有些库只在编译时需要,运行时就没了(比如
lombok)。有些则必须在运行时也存在(比如spring-core)。不同构建工具用不同的 scope 来区分,比如 Maven 的provided、runtime、test。
你想想看,要是没搞清楚这些类型,光靠手动拷贝 jar 包,那项目迟早变成一团乱麻。
exclude 或 transitive = false 来限制它的传递范围。
依赖树:一个可视化的依赖全景图
依赖树,就是把项目里所有依赖(直接 + 间接)按层级结构展示出来。我个人觉得,这是排查依赖冲突的利器。
举个例子,假设你的项目 A 依赖了库 B 和库 C,而 B 和 C 又分别依赖了库 D 的不同版本。在依赖树里,你会看到:
A (你的项目)
├── B (v1.0)
│ └── D (v2.0) ← B 要求用 2.0
└── C (v2.0)
└── D (v1.0) ← C 要求用 1.0
这时候就出问题了——同一个库 D,出现了两个不同版本。到底用哪个?构建工具会按“最近依赖优先”或“最先声明优先”的策略来裁决。
你可以用命令行查看依赖树。我常用的是:
// Gradle
gradle dependencies --configuration compileClasspath
// Maven
mvn dependency:tree
输出结果里,红色或带箭头的行,往往就是冲突的地方。碰到这种情况,我一般会手动指定一个兼容版本,或者用 force 强制锁定。
NoSuchMethodError。查了整整一天才发现,是两个子依赖引了不同版本的 Guava。从那以后,每次集成新 SDK ,我第一件事就是跑一遍依赖树。
传递性依赖:看不见的“附带物”
传递性依赖,就是依赖的依赖。这个东西,好处是你不用把库依赖的所有底层库都自己手动加一遍,省事。坏处是,你根本不知道它偷偷带进来了什么。
我举个例子。你在 Maven 里加了一个 spring-boot-starter-web,这玩意背后可能牵出了几十个间接依赖。你看:
| 依赖级别 | 库名 | 说明 |
|---|---|---|
| 直接依赖 | spring-boot-starter-web | 你显式引入的 |
| 第一层间接依赖 | spring-webmvc, spring-boot-starter-tomcat ... | 由 starter-web 引入 |
| 第二层间接依赖 | tomcat-embed-core, jackson-databind ... | 由 tomcat-starter 引入 |
| 更深层 | jackson-core, jackson-annotations ... | 由 jackson-databind 引入 |
你以为只加了一个依赖,实际项目里可能多了几十个 jar 包。这就是传递性依赖的力量。
那怎么管理它呢?我建议你:
- 保留必要的传递性依赖:只要不产生冲突,就让它正常工作。别为了减肥,把底层依赖全排除了。
- 排除冲突的传递性依赖:用
exclude来剔除那些版本冲突或引入安全漏洞的子依赖。Gradle 里这样写:implementation('com.example:some-library:1.0') { exclude group: 'com.unwanted', module: 'bad-dependency' } - 强制指定版本:当多个子依赖要求不同版本时,你可以强制项目里所有该库都使用指定版本。Maven 用
dependencyManagement,Gradle 用force = true或strictly。
ClassNotFoundException。一定要在排除后跑一遍单元测试和集成测试。
知识结构图:依赖管理的核心逻辑
为了让你更直观地理解,我画了一张依赖管理的核心逻辑图。它把依赖类型、树结构和传递性依赖串在了一起:
这张图其实把整个流程串起来了:你的项目拿到直接依赖后,直接依赖又会带入自己的子依赖(间接依赖)。这些依赖堆在一起,就构成了依赖树。只要树里出现同一个库的不同版本,就是冲突的信号。这时候,你就得借助构建工具提供的策略来“修剪”这棵树。
嗯,依赖管理这块,说白了就是三个字:看得见。只要你能看清依赖树,就成功了一大半。