1. 包和类路径
这里说的包是一个更宽的概念, 可以理解为把许多 Java 的 package 和其中的类放在一起打的压缩包(.jar
其实就是zip
)。
JVM 的工作被设计地相当简单:
执行一个类的字节码,假如过程中碰到了新的类,就加载它。
去哪里找呢?
根据类(搜索)路径(Classpath)查找,也就是 -classpath/-cp
参数。
但这存在一些问题:
- 传递性依赖:你依赖的类还以来了别的类,子子孙孙,无穷尽也。
- 类的全限定类名(目录层级)是类的唯一标识,唯一确定了一个类,当多个同名类同时出现在 Classpath 中,就会产生包冲突(如果两个类库 jar 包的不同版本),JVM 会选择靠前的,因为找到了这个新的类就开始进一步执行它了。
2. Maven 之前的年代
- 手动写命令进行编译运行
- 使用 Apache Ant,但还是要手动下载每个第三方类库,写 XML 配置,指定编译的源码目录、依赖的 jar 包和输出目录等,当有成百上千上万个第三方类库,依然是噩梦,还是没有解决 Classpath 地狱的问题。
2. 什么是包管理
你要使用的第三方类,要告诉 JVM 查找的路径,因此包管理的本质就是告诉 JVM 如何找到所需的第三方类库,以及成功地解决其中的冲突问题。
3. Maven—划时代的包管理
Convention over configuration 约定优于配置,Maven 远远不⽌是包管理⼯具。
Maven 的中央仓库,按照⼀定的约定存储包。Maven 的本地仓库默认位于~/.m2
,下载的第三⽅包放在这⾥进⾏缓存。
Maven 按照约定为所有的包编号,⽅便检索:
- groupId/artifactId/version 语义化版本
- SNAPSHOT 快照版本
Maven 会对传递性依赖进行自动管理,管理原则是:
绝对不允许最终的 classpath 出现同名不同版本的 jar 包
解决 Maven 的包冲突
当你看到如下异常时要考虑是否存在包冲突:
AbstractMethodError
NoClassDefFoundError
ClassNotFoundException
LinkageError
可以通过mvn dependency:tree
在命令行中查看依赖树,或者在 IDEA 的 Maven 栏目中查看。
Maven 自动解决包冲突的原则:
- 依赖树中离项目最近的(越浅的)胜出
- 若深度相同,则先声明的胜出
手动解决 Maven 不能解决的包冲突问题:
- 直接引入较新的版本
- 通过
<exclusions></exclusions>
声明强行干掉子依赖
<dependency>
<groupId>com.github.xxx<groupId>
<artifactId>test-library-a</artifactId>
<version>0.1</version>
<!-- test-library-c 被干掉了 -->
<exclusions>
<exclusion>
<groupId>com.github.xxx<groupId>
<artifactId>test-library-c</artifactId>
</exclusion>
</exclusions>
</dependency>
- 最后还可以使用 IDEA 的插件
maven helper
:
Maven 依赖中的 scope
scope 用于在不同代码目录中隔离第三方依赖,选项有compile/test/provided
等:
<dependencies>
<!-- testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
</dependencies>
- Maven 默认的依赖配置项中,scope 的默认值是
compile
(可以省略不写),会参与当前项目的编译,当然也包括测试,打包时候通常也需要包含进去,在main
和test
中都可见; -
test
表示依赖项目仅仅参与测试相关的工作,包括测试代码的编译,典型的如junit
,只在test
层级中可见; -
provided
表示只参与编译,不参与运行,使用 IDEA 尝试运行就能看到报错NoClassDefFoundError
,说明在运行时 IDEA 构建java
可执行程序时,并没有将这个声明为provided
的包的查找路径添加到-classpath
中,这印证了其不参与运行。
4. Maven—⾃动化构建⼯具
核心概念:坐标和依赖、生命周期、仓库、聚合和继承
《Maven实战》5、6、7、8章核心概念要看, 13、14、17、18章可以略读。
练习:实现语义化版本
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Version {
/**
* 请根据语义化版本的要求 https://semver.org/lang/zh-CN/ ,比较两个"语义化版本"
*
* <p>传入两个形如x.y.z的字符串,比较两个语义化版本的大小。如果version1小于version2,返回-1;如果version1大于
* version2,返回1。如果二者相等,返回0。
*
* <p>注意,如果传入的字符串形如x,则其等价于x.0.0。 如果传入的字符串形如x.y,则其等价于x.y.0。
*
* @param version1 传入的版本字符串1,支持x/x.y/x.y.z,你可以假定传入的字符串一定是合法的语义化版本
* @param version2 传入的版本字符串2,支持x/x.y/x.y.z,你可以假定传入的字符串一定是合法的语义化版本
* @return -1/0/1 当version1 小于/等于/大于 version2时
*/
private static List<String> stylizeVersion(String version) {
List<String> vList = new ArrayList<>(Arrays.asList(version.split("\\.")));
while (vList.size() < 3) {
vList.add("0");
}
return vList;
}
public static int compare(String version1, String version2) {
for (int i = 0; i < 3; i++) {
int v1 = Integer.parseInt(stylizeVersion(version1).get(i));
int v2 = Integer.parseInt(stylizeVersion(version2).get(i));
if (v1 < v2) {
return -1;
} else if (v1 > v2) {
return 1;
}
}
return 0;
}
}