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
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>

通过 groupId / artifactId / version,来定位唯一的包。开发过程中只需要在 pom.xml 文件添加包相应的信息即可。

其中 version 中的版本 1.2.75 是有一个规约的,1表示主版本号,2表示此版本号,75表示修订号

  1. 主版本号:当你做了不兼容的 API 修改。
  2. 次版本号:当你做了向下兼容的功能性新增。
  3. 修订号:当你做了向下兼容的问题修正。

Maven 的传递性依赖以及包冲突

maven 传递性依赖的⾃动管理原则:绝对不允许最终的 classpath 出现同名不同版本的jar包

maven 解决传递性依赖

当我们引入相关的包时,maven 会帮我们下载这个包,并且把他的传递性依赖的包都下载下来。因此,在 maven 中我们不需要去管理包的传递性依赖问题,maven 都会帮我们处理好。

maven 解决传递性依赖带来的包冲突问题

假设你的项目有以下依赖。A 依赖 C,C 依赖 D 的0.2版本;B 依赖 D 的 0.1版本。
 image.png

如果按照图上所示,把所有的包都下载,势必会造成前面的 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 剔除了,这个时候项目启动就会报错了。

手动解决包冲突

当遇到上诉情况就需要我们手动来解决冲突问题。

  1. 首先我们要对比冲突包的区别,判断项目实际上所需要的包(maven 中央仓库找到冲突的包,通过查看源代码进行对比)
  2. 确定了所需要的版本之后,可以使用如下方法进行解决
    • 方法一:根据 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
2
3
4
5
6
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.8.RELEASE</version>
<scope>compile</scope>
</dependency>

其中 groupId、artifactId、version 来定位这个包。scope 则是用来声明这个包在项目的作用范围。通常有这三个值
test 、 compile 、 provided 。test 表示包作用在测试代码中,src/test 目录下。compile 作用在源代码和测试代码中,并且编译和运行都有效。procided 作用在代码的编译期间,代码运行期间不生效。


Java 包管理
http://wszzf.top/2021/08/26/Java 包管理/
作者
Greek
发布于
2021年8月26日
许可协议