天天看点

教你依赖管理

你知道什么是依赖吗?

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

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

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

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

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

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

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

你见过几种依赖管理工具?

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

操作系统的依赖管理工具,比如CentOS的yum,Debian的apt,Arch的 Packman,macOS的Homebrew;

编程语言的依赖管理工具,比如Java的Maven,Net的nuget,Nodeis的 npm,Golang的goget,Python的pip,Ruby的Gem。

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

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

1.统一的命名规则,也可以说是坐标,在仓库中是唯一的,可以被准确定位到;

2.统一的中心仓库可以存储管理依赖和元数据;

3.统一的依赖配置描述文件;

4.本地使用的客户端可以解析上述的文件以及拉取所需的依赖。

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

Maven如何管理依赖?

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

Maven使用XML格式的文件进行依赖配置描述的方式,叫作POM(ProjectObjectModel),以下就是一段简单的pom.xml文件片段:

教你依赖管理

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

1.groupldartifactldversion:依赖的基本坐标;

2.type:依赖的类型,默认为jar;

3scope:依赖的范围;

4.optional:标记依赖是否可选;

5.exclusions:用来排除传递性依赖;

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

比如,junit只有在测试的时候有用,那么将其设为testscope就可以;

再比如,servletAPI这个jar包只需要在编译的时候提供接口,但是实际在运行时会有对应的servlet容器提供,所以没必要打到war包中去,这时候只需要指定在providedscope就可以了。通过指定providedscope的方式可以让每个依赖各司其职,不用弄成“一锅粥”。

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

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

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

教你依赖管理

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

教你依赖管理

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

Maven最佳实践

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

1.生产环境尽量不使用SNAPSHOT或者是带有范围的依赖版本,可以减少上线后的不确定性,我们必须保证,测试环境的包和生产环境是一致的。

2.将POM分成多个层次的继承关系,比如携程的POM继承树一般是这样:

教你依赖管理

这样做的好处是每一层都可以定义这一级别的依赖。

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

3.在父模块多使用dependencyManagement来定义依赖,子模块在使用该依赖时,就可以不用指定依赖的版本,这样做可以使多个子模块的依赖版本高度统一,同时还能简化子模块配置。

4.对于一组依赖的控制,可以使用BOM(BillofMaterials)进行版本定义。一般情况下,框架部门有一个统一的BOM来管理公共组件的版本,当用户引用了该BOM后,在使用框架提供的组件时无需指定版本。即使使用了多个组件,也不会有版本冲突的问题,因为框架部门的专家们已经在BOM中为各个组件配置了经过测试的稳定版本。

BOM是一个非常有用的工具,因为面对大量依赖时,作为用户你不知道具体应该使用它们的哪些版本、这些版本之间是否有相互依赖、相互依赖是否有冲突,使用BOM就可以让用户规避这些细节问题了。

5.对于版本相同的依赖使用properties定义,可以大大减少重复劳动,且易于改动。上面的pomxml片段,就是使用了properties来定义两个一样的版本号的依赖。

6.不要在在线编译环境中使用mvninstall命令,否则会埋下很多意想不到并且非常难以排查的坑:该命令会将同项目中编译产生的jar包缓存在编译系统本地,覆盖mvn仓库中真正应该被引用的jar包。

7.禁止变更了代码不改版本号就上传到中央仓库的行为。否则,会覆盖原有版本,使得一个版本出现二义性的问题。

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

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

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

1.确认开发操作系统,Java版本,Maven版本。通常情况下操作系统对Java编译的影响是最小的,但是偶尔也会遇到一些比如分隔符(冒号与分号)之类的问题。Java和Maven的版本应尽量与生产编译系统保持一致,以减少不必要的麻烦。

2.如果确认了开发操作系统没问题,那么你可以把用户的项目拉到自己的本地,并且删除本地依赖的缓存,也就是删除m2目录下的子目录,减少干扰,执行编译。若编译通不过,说明用户本地就有问题,让他也删掉自己本地的缓存找问题。如果可以编译通过,说明问题出在编译系统,进入第3步。

3.使用mvndependency命令对比生产编译系统与本地依赖树的区别,检查编译系统本地是否被缓存了错误的jar包,从而导致了编译失败。有时候这种错误会隐藏得比较深,非常难查,需要很大的耐心。

总结

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

1.了解到依赖管理的复杂度是如何产生的;