ZB-020-java包管理和maven

什么是包

  • JVM的⼯作被设计地相当简单:
    1. 执⾏⼀个类的字节码
    2. 假如这个过程中碰到了新的类,加载它,然后回到第一步,这样循环往复

那么,去哪⾥加载这些类呢?

  • 答案是 classpath类路径

类路径(Classpath)

  • 在哪⾥可以找到类
    • -classpath/-cp
  • 类的全限定类名(⽬录层级)唯⼀确定了⼀个类
  • 包就是把许多类放在⼀起打的压缩包 jar 包

每次 IDEA 运行java项目显示的命令行

程序运行时:会挨个在 classpath路径里找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=51272:
/Applications/IntelliJ IDEA.app/Contents/bin"
-Dfile.encoding=UTF-8
-classpath
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/deploy.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/dnsns.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/jaccess.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/localedata.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/nashorn.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunec.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/ext/zipfs.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/javaws.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfr.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jfxswt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/management-agent.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/plugin.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/dt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/javafx-mx.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/jconsole.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/packager.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/sa-jdi.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/lib/tools.jar:


# 当前目录的 target/classes 就是本项目里的 类文件

/Users/huangjiaxi/Desktop/java-fork-process/target/classes

com.github.hcsp.shell.Fork

什么是 jar 文件

  • 实际就是 跟zip一样压缩后的文件,只不过这里叫 jar包
每当 JVM 需要一个类时,它就会在当前的 classpath里 按照这个类的全限定类名挨个找,如果是jar包就解压缩然后在解压缩后的目录里找,如果是一个文件夹就直接按照目录找,找不到就抛出 classNotFound

如果你依赖的类中还依赖了别的包呢?

1
2
A 依赖 B , B 依赖 C , C 又依赖 D
D 从那里找呢?

这就是 传递性依赖 ,这就是为什么上面的命令行里 classpach 那么长的原因,要把所有依赖的path 添加进来

一个简单的项目命令行尚且如此之长,而一个真实项目可能有成百上千的依赖,如果你手动拼接,肯定又各种各样的问题,这就是为什么使用自动化工具的原因。

自动化的本质就是:帮我们把一些很啰嗦很累赘变成自动化

别急,还没完

没有maven时,你要手动导入jar包还有相关依赖,多人协作的时候非常麻烦。

  • 传递性依赖
    • 你依赖的类还依赖了别的类
  • Classpath hell
    • 全限定类名是类的唯⼀标识
    • 当多个同名类同时出现在Classpath中,就是噩梦的开始

当你的 classpath里包含了相同包,但是版本不同会发生什么

1
2
3
-classpath 
xxx.5.0.0.jar:
xxx.4.0.0.jar
  • 此时两个jar包里 有同名类 a
  • 在你程序运行时 会引用那个 类?

答案是 谁在前面用谁

问题升级,引入版本不同,但是同名的jar包

  • 版本不同的时候,代码可能是不一样的,以致于你的程序看上去可以运行。突然三天或三年后的某一天突然运行到一个地方出错了。你就很懵
    1
    2
    3
    4
    5
    6
    7
    8
    9
    -classpath 
    xxx.4.0.0.jar:
    xxx.5.0.0.jar

    # 4.0.0 是你实际使用的jar包
    # 5.0.0 是更新后的一个大版本修复了4.0.0的某些bug, 可能一些方法 API 已经变的面目全非了。
    但是在你看在程序还是正常的跑,只是没运行到出bug的地方
    你的项目如期上线,然后某天爆出个bug
    这个情况就叫 Classpath hell 依赖地狱

什么是包管理

  • 你要使⽤⼀些第三⽅类,总要告诉JVM从哪⾥找吧?
  • 包管理的本质就是告诉JVM如何找到所需的第三⽅类库
  • 以及成功地解决其中的冲突问题

没有Maven的蛮荒年代

以前怎么做的这些依赖

黑暗时代

  • ⼿动写命令进⾏编译运⾏
1
2
3
4
javac -cp  xx.jar xx2.jar Main.java
java -cp xx.jar xx2.jar Main

# 假设此时 jar包成百上千呢?

启蒙时代

  • Apache Ant
    • ⼿动下载jar包,放在⼀个⽬录中
    • 写XML配置,指定编译的源代码⽬录、依赖的jar包、输出
      ⽬录等
  • 缺点
    • 每个⼈都要⾃⼰造⼀套轮⼦
      1. 我的放在 libs
      2. 你的放在 sources
      3. 他的放在 libaray
    • 依赖的第三⽅类库都需要⼿动下载,费时费⼒
      • 假如你的应⽤依赖了⼀万个第三⽅的类库呢?都需要把依赖的库挨个下载下来
    • 没有解决Classpath地狱的问题,碰上重复的库你就完蛋了

Maven——划时代的包管理

  • Convention over configuration
  • 约定优于配置
  • 必须强调,Maven远远不⽌是包管理⼯具
  • Maven的中央仓库
    • 按照⼀定的约定存储包(坐标)
  • Maven的本地仓库
    • 默认位于~/.m2
    • 下载的第三⽅包放在这⾥进⾏缓存
  • Maven的包
    • 按照约定为所有的包编号,⽅便检索
    • groupId/artifactId/version
      • 扩展:语义化版本
    • SNAPSHOT快照版本

当你不知道一个包的坐标你就搜

1
2
3
4
5
6
7
8
xxx.xxx.xxx maven 通常第一个就是包的正确的名字

每个包有自己的版本
如 junit-jupiter-api 5.5.0
它会不停的升级。版本一旦发布,maven的约定是不能更改只能继续升级。
目的是 maven 想要实现一个可重现的构建,就是你机子上跑的和我机子上跑的 是一样的。

因为一旦同版本允许修改,就可能你跑的时候是好的,我跑的时候是坏的

maven 为什么知道包从哪里下载呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
当你引入依赖

<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>


maven 就会去它的主仓库 https://repo1.maven.org/maven2/
按照你的 groupId 里的路径
https://repo1.maven.org/maven2/org
https://repo1.maven.org/maven2/org/junit/
https://repo1.maven.org/maven2/org/junit/jupiter

然后按照 artifactId 找路径
https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/

然后按照版本号 5.4.2
https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/5.4.2/

里面有一堆文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
junit-jupiter-engine-5.4.2-javadoc.jar            2019-04-07 17:29    683506      
junit-jupiter-engine-5.4.2-javadoc.jar.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2-javadoc.jar.asc.md... 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-javadoc.jar.asc.sh... 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2-javadoc.jar.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-javadoc.jar.sha1 2019-04-07 17:29 40
# 源代码
junit-jupiter-engine-5.4.2-sources.jar 2019-04-07 17:29 110900
junit-jupiter-engine-5.4.2-sources.jar.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2-sources.jar.asc.md... 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-sources.jar.asc.sh... 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2-sources.jar.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2-sources.jar.sha1 2019-04-07 17:29 40
# 真正的 jar包
junit-jupiter-engine-5.4.2.jar 2019-04-07 17:29 177798
junit-jupiter-engine-5.4.2.jar.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2.jar.asc.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.jar.asc.sha1 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2.jar.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.jar.sha1 2019-04-07 17:29 40
# 当前包所依赖的包
junit-jupiter-engine-5.4.2.pom 2019-04-07 17:29 2395
junit-jupiter-engine-5.4.2.pom.asc 2019-04-07 17:29 821
junit-jupiter-engine-5.4.2.pom.asc.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.pom.asc.sha1 2019-04-07 17:29 40
junit-jupiter-engine-5.4.2.pom.md5 2019-04-07 17:29 32
junit-jupiter-engine-5.4.2.pom.sha1 2019-04-07 17:29 40

这样就实现了。你pom引入了一个junit-jupiter-engine-5.4.2的依赖,然后它下载后根据junit-jupiter-engine-5.4.2.pom文件去下载 当前包的依赖(传递性依赖)

这样就形成了一颗依赖树,你可以在 IDEA maven工具栏的Dependencies 里看到

pom.xml里的aliyun镜像,就是为了下载更快。也可以配置到你本地安装的maven里

1
2
3
4
5
6
7
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>

Maven的包

  • 按照约定为所有的包编号,⽅便检索
  • groupId/artifactId/version
  • SNAPSHOT快照版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    https://repo1.maven.org/maven2/org/junit/jupiter/junit-jupiter-engine/

    你开发的包可能不会立马上线,需要一直测一直改一直发布。但是总不能每次都发布一个版本吧!
    5.0.0/ 2017-09-10 18:09 -
    5.0.0-M1/ 2016-07-07 09:05 -
    5.0.0-M2/ 2016-07-23 18:14 -
    5.0.0-M3/ 2016-11-30 09:06 -
    5.0.0-M4/ 2017-04-01 19:23 -
    5.0.0-M5/ 2017-07-04 16:39 -
    5.0.0-M6/ 2017-07-18 19:27 -
    # RC叫做 发布准备版 但是它们都不是 SNAPSHOT快照版本,也基本不会在中央仓库里放置
    # 中央仓库都是放成熟的。
    5.0.0-RC1/ 2017-07-30 19:13 -
    5.0.0-RC2/ 2017-07-30 20:37 -

maven 默认会帮你下载字节码的包,如需看源码 可以在 IDEA 点击下载

包冲突

当你看到如下的异常的时候,包冲突发生了

  • AbstractMethodError
  • NoClassDefFoundError
  • ClassNotFoundException
  • LinkageError

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
你的项目里
依赖了 A
依赖了 D

A 又依赖了 B , B又依赖了 C
D 又依赖了 C

|A
--|B
--|--|C 1.0

|D
--|C 2.0

此时 两个 C 就导致了 包冲突

-classpath A:B:C1.0:D:C2.0

# 因为之前说了 classpath hell 依赖地狱
由于全限定类名是类的唯一标识,此时如果存在两个同名不同版本的库 同时出现在 classpath 中 那么噩梦开始了

如果有同名的库出现在 classpath 中 JVM 会选择靠前的那个 也就是 C1.0

但是 靠前的那个 C1.0 不一定是你想要的那个,有可能只是你忘了。

对此 maven 的原则是

  • 传递性依赖的⾃动管理
    • 原则:绝对不允许最终的classpath出现同名不同版本的jar包
  • 依赖冲突的解决:原则:最近的胜出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    你的项目里
    依赖了 A
    依赖了 D

    A 又依赖了 B , B又依赖了 C
    D 又依赖了 C

    |A
    --|B
    --|--|C 1.0

    |D
    --|C 2.0

    C2.0 更加离 依赖树的根更近
    也就是 C2.0 获胜,淘汰 C1.0
    -classpath A:B:D:C2.0
    • 这个过程绝大多数是可以正常工作的,但是
    • mvn dependency:tree
  • 依赖的scope

实战:解决包冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
你的项目依赖了 A / D

|A 0.1
--|B 0.1
--|--|C 0.2


|D 0.1
--|C 0.1

此时 maven 帮你淘汰了 C0.2 版本 因为它离得太远
-classpath A0.1:D0.1:B0.1:C0.1

此时就有 bug ,因为你想要的是 C0.2

为什么会干掉 C0.2,而不是C0.1

  • 答案是 maven就是这样设计的,它不分析语义版本,对于人类我们知道 0.2>0.1,但是maven在设计上的原则是谁离得更近,Gradle的策略是选择版本高的,但这也不是万能的。
  • 很多时候还是要人工介入

解决方案

  1. 强行引入 C0.2版本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    |A 0.1
    --|B 0.1
    --|--|C 0.2

    |D 0.1
    --|C 0.1

    |C0.2

    C0.2 离得比 C0.1近。所以淘汰 C0.1
  2. exclusion 排除指定依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    |A 0.1
    --|B 0.1
    --|--|C 0.2

    |D 0.1
    --|C 0.1
    通过 exclusion 来告诉 maven 忽略某个依赖 的子依赖
    <dependency>
    <groupId>xxx.xxx</groupId>
    <artifactId>D</artifactId>
    <version>0.1</version>
    <exclusions>
    <groupId>yyy.yyyy</groupId>
    <artifactId>C</artifactId>
    <version>0.1</version>
    </exclusions>
    </dependency>
  3. 一个IDEA 插件 maven helper

    • 能帮你分析当前的依赖树 和被淘汰的依赖标红显示
    • 你想排除指定版本只需要选中依赖 点击 Exclude
    • 一目了然帮你看到依赖树

如下依赖谁会被淘汰

1
2
3
4
5
6
7
8
9
|A 
--|B
--|--|C0.1

|D
--|E
----|C0.2

C0.1 获胜,因为C0.1 路径相同则选择靠前的那个

依赖的 scope

实现代码的隔离

  • <scope>test</scope> 代表只在 src/test目录可见
  • <scope>compile</scope> 在 src/main 和 src/test 都有效
  • <scope>provided</scope> 只在编译的时候有效
    1. 我们运行java的时候 右键 run 会先编译,此时会把依赖包在 -classpath 里体现
    2. 而在运行的时候 -classpath 不包含依赖包的路径
    3. 它的意思就是 编译的时候需要,运行的时候别人帮我提供,你不需要帮我加,防止包冲突。 最经典的就是 tomcat ,你的项目编译后会放在 tomcat容器里,tomcat 会帮你提供这些依赖包
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>5.4.2</version>
      <scope>test</scope>
      </dependency>
      <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>5.4.2</version>
      <scope>compile</scope>
      </dependency>

maven 不仅仅是包管理工具还是一个自动化构建工具

推荐一本书 Maven实战 google搜 Maven实战 site:github.com

  • Maven实战哪些章节是值得看的?
    • 1 简介可以看看
    • 3.1-3.4 代码是过时的
    • 4 可以看 背景案例也是过时的 看场景
    • 5
    • 6
    • 7
    • 8
    • 其他选看
  • Maven项⽬的基本结构(传世经典)
  • 基本概念:坐标和依赖/⽣命周期/仓库/聚合和继承
  • 使⽤Maven进⾏测试
  • 如果需要开发插件的话:
  • Maven的插件

真实世界中的Maven

  • 分析若⼲真实世界的Maven仓库
  • pom的含义 project object model 项目说明书

练习

  1. 修复commons-lang项目的pom文件
    我的pr
  2. 解决包冲突
    我的pr
  3. 实现语义化版本
    我的pr