<audio id="audio" title="05 | 手把手教你依赖管理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/7f/836151335836db3e6e2c02bc77a37a7f.mp3"></audio>

软件工程是多人合作的结果，我们在开发软件的时候经常会使用一些别人编写好的，比较成熟的库。

比如，早期的前端开发用到了 jQuery 库，那么通常的做法是去官网下载一个最新版本的 jQuery，然后放在自己本地的项目中。对于简单的前端项目来说，这样可以简单粗暴地达到目的。

但当项目越来越庞大，除了 jQuery 之外，你还会依赖一些其他的第三方库。比如 Bootstrap 与 Chosen，这两个流行的前端库也都依赖 jQuery，如果这些第三方库依赖的jQuery 版本一致还好，但大多数情况并没有这么乐观：

> 
你的项目依赖的 jQuery 版本是 1.0.0 ，Bootstrap 依赖的版本是 1.1.0，而 Chosen 依赖的版本是 1.2.0，看上去都是小版本不一致，一开始并没有发现任何问题，但是如果到后期发现不兼容，可能就为时已晚了。


所以，你需要在确定依赖之前，就把整个系统的依赖全部梳理一遍，保证每个依赖都不会有冲突问题。

你可能会质疑，这个前端工程师一定是初级的，事先都不会确认 Bootstrap 和 Chosen 依赖的版本吗，直接选择依赖 jQuery 1.0.0 版本的不就行了？

这么说有一定道理，但是手工维护这些依赖是相当麻烦且容易出错的。随便找一个比较流行的开源软件，你都会发现它依赖了大量的第三方库，而这些第三方库又依赖着其他的第三方库，形成了一条十分复杂的依赖链。靠人工去解决这个依赖链一定会让你怀疑人生，因此你需要一些工具去管理项目的依赖。

## 你见过几种依赖管理工具？

其实，各大平台早已有一套自己的手段来解决上述的问题，仔细看看你常用的软件，你会发现其实工作当中已经充斥着各种各样的依赖管理工具，没有它们你将寸步难行。

> 
<p>操作系统的依赖管理工具，比如CentOS 的 yum，Debian 的 apt，Arch 的 Packman，macOS 的 Homebrew；<br>
编程语言的依赖管理工具，比如Java 的 Maven， .Net 的 nuget，Node.js 的 npm，Golang 的 go get，Python 的 pip，Ruby 的 Gem  。</p>


这些平台的解决思路都是将依赖放到共同的仓库，然后管理工具通过依赖描述文件去中央仓库获取相应的包。

一个典型的依赖管理工具通常会有以下几个特性：

<li>
统一的命名规则，也可以说是坐标，在仓库中是唯一的，可以被准确定位到；
</li>
<li>
统一的中心仓库可以存储管理依赖和元数据；
</li>
<li>
统一的依赖配置描述文件；
</li>
<li>
本地使用的客户端可以解析上述的文件以及拉取所需的依赖。
</li>

接下来我以 Maven 为例，跟你一起探究一下 Maven 会如何管理 Java 项目的依赖。

## Maven 如何管理依赖？

Maven 是 Java生态系统里面一款非常强大的构建工具，其中一项非常重要的工作就是对项目依赖进行管理。

Maven 使用 XML格式的文件进行依赖配置描述的方式，叫作 POM（Project Object Model ），以下就是一段简单的 pom.xml 文件片段：

<img src="https://static001.geekbang.org/resource/image/b3/b5/b37b7cb7e75517bf64695bcfa63529b5.png" alt="">

在POM中，根元素 project 下的 dependencies 可以包含一个或多个 dependency 元素，以声明一个或者多个项目依赖。每个依赖可以包含的元素有：

<li>
groupId、artifactId、version： 依赖的基本坐标；
</li>
<li>
type： 依赖的类型，默认为jar；
</li>
<li>
scope： 依赖的范围；
</li>
<li>
optional： 标记依赖是否可选；
</li>
<li>
exclusions： 用来排除传递性依赖；
</li>

要想用好Maven管理依赖，你必须理解每一项的含义，而新手通常傻傻分不清楚。举个例子，依赖范围这一项，Maven 在不同的时期会使用不同的 classpath ：

- 比如，junit 只有在测试的时候有用，那么将其设为 test scope 就可以；
- 再比如 ，servlet API这个 jar 包只需要在编译的时候提供接口，但是实际在运行时会有对应的 servlet 容器提供，所以没必要打到 war 包中去，这时候只需要指定在 provided scope 就可以了。通过指定 provided scope的方式可以让每个依赖各司其职，不用弄成“一锅粥”。

包管理工具还解决了依赖传递的问题，比如你的项目 A 依赖了 B，而 B 依赖了 C和D，那么在获取依赖的时候会把 B、C、D 都一起拉下来，这样可以节省大量的时间。

再让我们回到最开始的问题：依赖不一致该如何处理？通常，每个包管理工具都有一套自己的原则，Maven 的依赖仲裁原则如下。

**第一原则： 最短路径优先原则。** 比如，A 依赖了 B和C，而 B 也依赖了 C，那么 Maven 会使用 A 依赖的 C 的版本，因为它的路径是最短的。

<img src="https://static001.geekbang.org/resource/image/a6/38/a64e8f8816eb5f91538c70b117e8e938.png" alt="">

**第二原则： 第一声明优先原则。** 比如，A 依赖了 B和C，B 和 C 分别依赖了 D，那么 Maven 会使用 B 依赖的 D 的版本，因为它是最先声明的。

<img src="https://static001.geekbang.org/resource/image/75/58/75c0189d4c6d3f28074b315928a36d58.png" alt="">

**根据这两个原则，Maven 就可以确定一个项目所有依赖的列表，但它处理依赖的方式还是有些简单粗暴。有时 Maven 的决定结果并不是你想要的，所以我们在使用 Maven 的时候还是要多加小心。**

## Maven 最佳实践

接下来，我跟你分享下，我平时使用Maven 时总结的一些经验。

<li>
生产环境尽量不使用 SNAPSHOT 或者是带有范围的依赖版本，可以减少上线后的不确定性，我们必须保证，测试环境的包和生产环境是一致的。
</li>
<li>
将 POM分成多个层次的继承关系，比如携程的POM继承树一般是这样：
</li>

```
corp pom
	ctrip pom/qunar pom
		bu pom
			product pom
				project parent pom
					project sub module pom

```

这样做的好处是每一层都可以定义这一级别的依赖。<br>
其中 ctrip pom/qunar pom 我们叫它为公司的 super-pom，每个项目必须直接或间接的继承其所在公司的 super-pom。这样做的好处是每一层都可以定义这一级别的依赖，便于各个层次的统一管理。

<li>
在父模块多使用 dependencyManagement 来定义依赖，子模块在使用该依赖时，就可以不用指定依赖的版本，这样做可以使多个子模块的依赖版本高度统一，同时还能简化子模块配置。
</li>
<li>
<p>对于一组依赖的控制，可以使用BOM（Bill of Materials） 进行版本定义。一般情况下，框架部门有一个统一的BOM 来管理公共组件的版本，当用户引用了该BOM后，在使用框架提供的组件时无需指定版本。即使使用了多个组件，也不会有版本冲突的问题，因为框架部门的专家们已经在BOM中为各个组件配置了经过测试的稳定版本。<br>
BOM是一个非常有用的工具，因为面对大量依赖时，作为用户你不知道具体应该使用它们的哪些版本、这些版本之间是否有相互依赖、相互依赖是否有冲突，使用BOM 就可以让用户规避这些细节问题了。</p>
</li>
<li>
对于版本相同的依赖使用 properties 定义，可以大大减少重复劳动，且易于改动。上面的 pom.xml 片段，就是使用了 properties 来定义两个一样的版本号的依赖。
</li>
<li>
不要在在线编译环境中使用 mvn install 命令，否则会埋下很多意想不到并且非常难以排查的坑：该命令会将同项目中编译产生的jar包缓存在编译系统本地，覆盖mvn仓库中真正应该被引用的jar包。
</li>
<li>
禁止变更了代码不改版本号就上传到中央仓库的行为。否则，会覆盖原有版本，使得一个版本出现二义性的问题。
</li>

归根结底，这些经验都是为了两件事：减少重复的配置代码，以及减少不确定的因素发生。

有时候，你会听到来自业务开发部门同事传来报障的声音：“为什么我本地可以编译通过，而你们编译系统编译通不过？”难道 Maven 在工作的时候还看脸？ 当然不是!

遇到这样的情况不要急，处理起来通常有如下“三板斧”：

<li>
确认开发操作系统，Java 版本，Maven 版本。通常情况下操作系统对 Java 编译的影响是最小的，但是偶尔也会遇到一些比如分隔符（冒号与分号）之类的问题。Java 和 Maven 的版本应尽量与生产编译系统保持一致，以减少不必要的麻烦。
</li>
<li>
如果确认了开发操作系统没问题，那么你可以把用户的项目拉到自己的本地，并且删除本地依赖的缓存，也就是删除 .m2 目录下的子目录，减少干扰，执行编译。若编译通不过，说明用户本地就有问题，让他也删掉自己本地的缓存找问题。如果可以编译通过，说明问题出在编译系统，进入第3步。
</li>
<li>
使用 mvn dependency 命令对比生产编译系统与本地依赖树的区别，检查编译系统本地是否被缓存了错误的 jar 包，从而导致了编译失败。有时候这种错误会隐藏得比较深，非常难查，需要很大的耐心。
</li>

## 总结

今天，我跟你聊了聊依赖的问题。你可以从中：

<li>
了解到依赖管理的复杂度是如何产生的；
</li>
<li>
学习到依赖管理的一些常规思路；
</li>
<li>
初步掌握通过Maven进行依赖管理的方式方法，及一些最佳实践。
</li>

同时我也与你一起分享了一些我的实际经验，希望能够对你在实际工作中有所帮助。

## 后续

理想是美好的，然而现实却很骨感，在实际过程中我们也遇到了一些问题，比如用户不遵守我们推荐的命名规则，或者不继承公司提供的 Super POM，或者框架组件升级而用户不愿意升级等等。

为了能够统一管理，我们在构建系统上增加了一些强制手段来做统一的约束，使用 Maven Enforcer 插件以及其他方式对构建过程实行大量检查，欲知详情，请听下回分解。

## 思考题

你可以对比一下 Maven 其他语言的依赖管理工具，如 npm、pip、gem 等等，看看它们各自都有什么样的优缺点。

欢迎你给我留言。


