Java 包管理
java 程序的本质就是在拼接命令行,如何拼接的细节,都由 ide 帮我们实现了。假如我们在程序中添加一个包,我们只需要 java -cp 后面补上包的位置,以及这个包依赖的其他包的位置。 当程序中引用的包越来越多时,带来传递性依赖也会越来越多,要在 Java -cp 后面一个个补上包的位置,并且保证不遗漏传递性依赖的包,并且还要保证包不能同名。这将是一比巨大的工作量,费时费力,还容易出错。
这个时候就需要使用包管理了。它的本质就是告诉JVM如何找到所需的第三⽅类库,以及成功地解决其中的冲突问题。
JVM 加载包
首先,JVM 的工作被设计地相当简单:执行一个类的字节码,假如这个过程中碰到了新的类,就去加载它。
既然碰到新的类就会去加载,那么就存在一个问题:去哪里加载呢?
类路径(Classpath)
当 jvm 去找一个新的类时,就会到类路径(Classpath)中挨个去找,碰到 jar 包就会解压缩再去查找。
由于类的全限定类名(⽬录层级)唯⼀确定了⼀个类,因此 jvm 可以找到这个类。其中 jar 包本质上就是把许多类放在一起打的压缩包。
包加载存在的一些问题
- 传递性依赖。简单的解释就是,你依赖的类还依赖了别的类。例如:A -> B -> C,A 依赖 B,B 依赖 C。
- Classpath hell。因为全限定类名是类的唯⼀标识,所以当多个同名类同时出现在Classpath中,就会出现问题。例如:当 classpath 中存在同名的但是不同版本的 jar 包,A-1.0.jar 和 A-1.2.jar。jvm 会根据声明顺序去选择执行,假如 A-1.0.jar 声明在前,jvm 就会加载 A-1.0.jar,而不使用 A-1.2.jar。如果 A-1.0.jar 中是存在安全风险,那么到时候程序运行到安全风险时,就会导致灾难性后果。
包管理工具
Apache Ant
Apache Ant 解决了部分包管理的问题。通过⼿动下载 jar 包,放在⼀个⽬录中。然后写XML配置,指定编译的源代码⽬录、依赖的jar包、输出目录等。
这样做带来的缺点是什么呢?
- 每个人都要自己造一套轮子库
- 依赖的第三⽅类库都需要⼿动下载,费时费力。依赖的第三方类库越多,越麻烦。
- 没有解决 Classpath hell 问题。即,还是可能存在包同名的问题。
Maven
Maven 包管理
Maven 是划时代的包管理工具,但是 Maven 能做的远不止包管理。
Maven 的理念是约定优于配置,默认的 Maven 项目结构都是一样的。Maven 具有中央仓库,包都是按照一定约定存储的。Maven 还有本地仓库,默认是位于 ~/.m2
。当我们引入一个依赖时,Maven 就会根据填写的信息找到对应的包,并把它下载到本地仓库,下载到本地仓库之后,下次再有相同的包,则直接从本地仓库找。
Maven 的包按照约定为所有的包编号,方便检索。例如要在项目中引入 fastjson.jar
1 |
|
通过 groupId / artifactId / version,来定位唯一的包。开发过程中只需要在 pom.xml 文件添加包相应的信息即可。
其中 version 中的版本 1.2.75 是有一个规约的,1表示主版本号,2表示此版本号,75表示修订号
- 主版本号:当你做了不兼容的 API 修改。
- 次版本号:当你做了向下兼容的功能性新增。
- 修订号:当你做了向下兼容的问题修正。
Maven 的传递性依赖以及包冲突
maven 传递性依赖的⾃动管理原则:绝对不允许最终的 classpath 出现同名不同版本的jar包
maven 解决传递性依赖
当我们引入相关的包时,maven 会帮我们下载这个包,并且把他的传递性依赖的包都下载下来。因此,在 maven 中我们不需要去管理包的传递性依赖问题,maven 都会帮我们处理好。
maven 解决传递性依赖带来的包冲突问题
假设你的项目有以下依赖。A 依赖 C,C 依赖 D 的0.2版本;B 依赖 D 的 0.1版本。
如果按照图上所示,把所有的包都下载,势必会造成前面的 Classpath hell 问题,因为两个包 D 同名了。首先,maven 会根据包的 groupId 和 artifactId 来判断是否为同一个。存在相同的包 maven 就会帮我们自动解决。
maven 解决的原则就是:离项目最近的胜出,如果一样近,则靠前声明的胜出。回到图中,D-0.2 离项目的距离是3,而 D-0.1 离项目的距离是2,因此 D-0.2就会被 maven 剔除,classpath 路径中只存在 D-0.1。maven 解决冲突的做法,在大部分情况下是可行的,但也有不行的时候。假设项目中使用了 D-0.2 中的 API,由于 D-0.1 是旧版,没有相同的 API,maven 根据原则,帮我们把 D-0.2 剔除了,这个时候项目启动就会报错了。
手动解决包冲突
当遇到上诉情况就需要我们手动来解决冲突问题。
- 首先我们要对比冲突包的区别,判断项目实际上所需要的包(maven 中央仓库找到冲突的包,通过查看源代码进行对比)
- 确定了所需要的版本之后,可以使用如下方法进行解决
- 方法一:根据 maven 的解决原则,最近的胜出,我们可以直接在项目中引入一个 D-0.2 版本,此时 D-0.2 离项目的距离是1,所以会使用 D-0.2。
- 方法二:指定 maven 排除不需要的包,把不需要的依赖排除掉,剩下需要的依赖就可以了。使用exclusions,在 B 的 dependency 中排除掉 D 的依赖。
1
2
3
4
5
6<exclusions>
<exclusion>
<groupId></groupId>
<artifactId></artifactId>
</exclusion>
</exclusions>
**tips: ** 可以使用Maven helper
查看依赖树,也可以通过 mvn dependency:tree
命令查看依赖树。
依赖的 scope
我们经常可以在 pom 文件中看到以下依赖
1 |
|
其中 groupId、artifactId、version 来定位这个包。scope 则是用来声明这个包在项目的作用范围。通常有这三个值test
、 compile
、 provided
。test 表示包作用在测试代码中,src/test 目录下。compile 作用在源代码和测试代码中,并且编译和运行都有效。procided 作用在代码的编译期间,代码运行期间不生效。