Maven and Classpath Hell

  • 前言

        之前其实一直在用到Maven,但却没有怎么下功夫了解。但了解Maven还是非常有必要的。我一直很认同ber哥的一句话。

强大的IDE会割裂你和这些底层的运作。但你还是要了解这些底层运行的原理,因为总会遇到各种各样的问题让你不得不和这些底层打交道,对于工程师来说,生疏的表现是不合适的。了解这些细节,有助于你解决问题。工作就是在解决各种各样的问题。

        通过了解Maven所为我们做的,我们可以更深刻的理解整个JAVA世界的历史变革,对我们现在使用的一切有一定认知性。虽然不能讲得很深,但也能大致对Maven有一个感性的认识了。

什么是Maven? 什么是包????

        首先,Maven是一个包管理工具,也有人叫它项目管理工具。但此包非彼包,并不是初学JAVA时接触的那个Package包。那到底是什么包?我们不妨先引入一个概念。JVM的运行机制

JVM的工作只有一个:

执行这个类的字节码,过程中若碰到了新的类,加载它。(循环往复直到把事情做完)

那么,它是如何找到这些类的呢?我们知道,命令行中的那些可执行程序是在哪儿找到的呢,答案是在当前的PATH环境变量中找到的。相应的,我们找类也有一个路径,叫Classpath(类路径), 和命令行业寻找可执行程序一样。JVM会挨个在PATH里面找,是压缩包就解压来找(这是全自动的),文件夹就直接开找,找不到就下一个,找到为止。让我们随便看一个JAVA小案例的Classpath。

1
-classpath "C:\Program Files\Java\jdk1.8.0_221\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_221\jre\lib\rt.jar;F:\shape-polymorphism\target\classes" com.github.hcsp.polymorphism.Main

由于存在包依赖关系,如果是真正的项目,这个Classpath会长得超出显示限制。。。。

类的全限定类名(目录层级)确定了一个类
包就是把许多类放在一起打的压缩包啊!
jar包就是一堆类的集合

介绍完包了之后,需要说的是,我们知道JVM懂得看到了方法去找类,找类也就是在找包(通过Classpath),但包并不会凭空出来,很多第三方包都需要我们Down到项目中,添加到Classpath中,这也是Maven诞生的主要原因,包管理的本质就是告诉JVM如何找到所需的第三⽅类库。 开发过程更爽!生产出来的软件更棒!

让我们看一看没有Maven之前的年代,JAVA是如何走过来的。

蛮荒时代

这个时候,全世界的JAVA工程师还没有形成包管理这样的概念,

一个一个手写命令进行编译运行,just like that

1
javac -cp ./commons-lang3-3.9.jar StringIsBlank.java

想象一下这里需要不仅仅是一个包,而是上面成千上百个。。。真正的编程到昏厥。苦不堪言(如果以现在的眼光看的话,无论什么情况当事人一般都会觉得还行哈哈哈哈哈哈哈哈哈哈)

启蒙时代

Apache Ant 提出了XML配置文件来规范化所谓包管理,制定变异的源代码目录,依赖的JAR包,输出目录等。虽说有了点规范的意思,但还是跳不过一个一个手动下载的命,也没有解决JVM傻呆呆的处理重名JAR包的问题。(Classpath hell),且大家的编写各不相同,配置并不通用。你写你的libs,我写的library,他写他的依赖jar包文件夹,她写她的输出目录。大家经常拿Ant 和 Maven 作比较,现在Ant似乎不行了,大家就拿Gradle和 Maven 作比较。 由此可见Maven还是很坚挺的 :)

Maven时代

Maven是一款跨时代的包管理工具,它使用了一套相当先进的项目结构及约定征服了全世界的工程师。首先它存在一个中央仓库,还制定了一些规则,让全世界的工程师不用再一个一个手写命令行编译运行,而是变为了导包。我们只需要在pom.xml文件按照相应的约定格式导入即可,就下载到了你的本地仓库。下载的第三方包放在这里进行缓存。它还会把你下载的第三方包所以来的其他包一并下载进来。

中央仓库的构建,对于包管理方面按照约定为所有的包编号,⽅便检索

1
2
3
4
5
6
7

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>

Maven这一严谨的规定折服了所有的工程师,以至于后面诞生的包管理工具相关规定也有这样的影子。除此之外,Maven还解决了一个JAVA历史性难题。Classpath Hell (类路径地狱)。

什么是Classpath Hell,也就是包冲突?

如果你的classpath或项目里面不幸地引入了两个版本不一样,但同名的jar包。就是噩梦的开始,JVM可不会想人一样会斟酌选择,它只会找到哪个用哪个。用不到这个包自然不报错,它会让你错误地认为一切顺风顺水,然而,它也会在某一天毫无征兆的暴毙,然后你只得大半天赶到开始接受地狱般的bug挑战。

常见的包冲突的一些报错:

  • AbstractMethodError
  • NoClassDefFoundError
  • ClassNotFoundException
  • LinkageError

包管理有一个原则:绝对不允许最终的classpath出现同名不同版本的JAR包。

只有能够得知包冲突,才能解决。

maven 是 如何得知 两个包冲突呢

因为maven把这些包按照名字格式把它们归类起来了。
梯子恢复后自行

依赖冲突的解决:最近的胜出

这个机制让maven能够解决绝大多数的情况,但在现实生活中,我们还是会遇到需要亲自手动去解决的情况。为什么?
因为mvn解决掉的是0.2版本,保留0.1版本。但我们要用的确是更新版本的0.2版本!!!!

你可以打开ide点击maven窗口,用肉眼目测整个项目的包依赖关系。

也可以在终端输入

1
mvn dependency:tree

来观察。

如何手动解决?

  1. 我们手动介入让我想要的版本的jar包离的最近,从让它在JVM的包冲突的比较中胜出,自然JVM用的就是这个包。
  1. 通过exclusions声明强行干掉子依赖
  1. 如果想让ide来帮助你的话,你可能需要maven-helper插件,当然非人的调试还是可能出问题。

拓展一下,如果是这样呢?

  • A->B->C:0.1

  • D->E->C:0.2

谁赢,答案是0.1版本获胜,maven还是会选择前面的那一个。

maven总是会选择一个,因为绝对不允许最终的classpath出现同名不同版本的JAR包。

那Maven还有一个关键属性 scope ,它有什么用呢?

其实很简单,它的作用就是 让依赖 可以隔离化。换言之就是让 依赖不是全盘默认,而是可以人为的决定这个是否可以依赖。

scope的常用属性为 compile test provided

  • test: 只在测试代码中可以拿到依赖包
  • compile: 在测试和生产环境中都可以拿到依赖包
  • provided: 在编译的时候可以拿到,在运行时就拿不到了

Maven的伟大之处——它不仅仅是项目管理工具!

Maven项目构建,版本控制,库依赖三大特性。若想真正了解Maven

建议阅读 Maven实战