一:Java 的包原理
什么是包
JVM 的工作被设计地相当简单:
那么我们就会产生这样的疑惑:去哪里加载这些类呢?
答案就是:类路径(Classpath)
回顾我们在上一章节所学习的内容:
对于 Main.java
import org.apache.commons.lang3.StringUtils;
public class Main {
public static void main(String[] args) {
System.out.println(StringUtils.isBlank(""));
}
}
我们首先要使用 javac 编译源文件:
javac -classpath commons-lang3-3.9.jar Main.java
然后执行字节码文件,启动程序:
java -cp commons-lang3-3.9.jar:. Main
我们使用了 -classpath/-cp 来告诉 JVM 去哪里加载我们需要的类文件
而这里面的 jar 包,实际上就是我们所说的“包”
jar 其实就是将很多类文件打包后的一个压缩包,我们导入 jar 后,可以直接使用里面的类或调用其中的功能。
什么是包管理
传递性依赖
我们依赖的包还依赖了别的类,这种依赖是具有传递性的,传递性依赖带来的最大的问题就是 jar hell
我们在 -classpath 后会添加项目依赖的各种各样的 jar 包,试想一下,如果两个仅仅不同版本的 jar 包被同时写进了 -classpath 参数里面,会出现什么问题?
首先,JVM 在 classpath 中寻找类文件的顺序是从前找到后的,也就是说如果有两个仅仅不同版本的 jar :demo-1.0.jar 和 demo-2.0.jar ,哪个放在前面哪个就会被使用。如果 demo-1.0.jar 的顺序在 demo-2.0.jar 之前,就出现问题了。
我们知道全限定类名是类的唯一标识,在 demo-1.0.jar 和 demo-2.0.jar 中的类,对于 JVM 来说是完全一样的,但是因为在 classpath 的前后顺序,导致 JVM 会选择在 demo-1.0.jar 中寻找需要加载的类文件。可是项目开发者的意图很显然是采用升级版本的 jar 包。如果开发者并没有注意到这一点,在后期项目遇到了 bug,很难去查其根源。最要命的就是开发者需要手动管理成千上万个 jar 包!这种包管理的弊端被开发者调侃为 jar hell 并不是没有道理的。
黑暗岁月
在没有 Maven 的蛮荒年代,开发者是手动去编写命令,完成编译到运行的工作。
启蒙时代
Apache Ant
- 需要手动下载 jar 包,放在一个目录中
- 写 XML 配置,指定编译的源代码目录,依赖的 jar 包,输出目录等
缺点
- 每个人都是一套规范,没有一个标准
- 依赖的第三方类库都需要手动去下载,费时又费力
- 没有解决 jar hell
二:Maven 的包管理
Maven 是一个项目管理工具,它包含:
- 一个项目对象模型(Project Object Model)
- 一组标准集合
- 一个项目生命周期(Project Lifecycle)
- 一个依赖管理系统(Dependency Management System)
- 以及用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑
Maven 是如何进行包管理的
Maven 包管理的做法是:Convention over configuration(约定优于配置原则),这一点体现在 POM
POM(Project Object Model)是 Maven 工程的基本工作单元,是一个 XML 文件,该文件中包含了项目的基本信息,用于描述项目如何构建,声明项目依赖等等。
POM 文件示例:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成,
如com.companyname.project-group,
maven会将该项目打成的jar包放本地路径:/com/companyname/project-group -->
<groupId>org.apache.commons</groupId>
<!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
<artifactId>commons-lang3</artifactId>
<!-- 版本号 -->
<version>3.4</version>
</dependency>
</dependencies>
</project>
如果我们需要在项目中引入第三方包,必须要遵守 POM 的约定,指定 groupId,artifactId 以及 version。这三个标签就像是坐标一样,用来定位项目如何去寻找包的位置。
当我们在项目中添加了 commons-lang3 这个 jar 包的依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
我们的项目会从哪里寻找这个 jar 包呢?
Maven 本地仓库
Maven 首先会去本地仓库寻找 jar 包,对于我的 Mac 系统,Maven 本地仓库默认存放在 ~/.m2 目录。
该 jar 包定位在我本地仓库的 .m2/repository/org/apache/commons/commons-lang3/3.4 位置下,如果我们的本地仓库已经下载好该 jar 包,Maven 就会从本地导入 jar。
Maven 中央仓库
如果在本地仓库没有找到对应的 jar 包,Maven 就会从远程的中央仓库进行下载,然后放到本地仓库中。
Maven 中央仓库位置:https://repo1.maven.org/maven2/
示例中,如果在本地仓库没有找到 commons-lang3-3.4.jar ,对应地,Maven 就会去 https://repo1.maven.org/maven2/org/apache/commons/commons-lang3/3.4/ 该位置下载好需要引入的包,并存放在本地仓库。
三:包冲突及解决实战
Maven 是如何解决包冲突的
Maven 传递性依赖的管理原则:绝对不允许最终的 classpath 中出现同名不同版本的 jar 包
Maven 依赖冲突的解决原则:最近的胜出
什么叫最近的胜出呢?
示例:
你的项目的依赖关系树如下:
项目依赖 A,B 两个 jar 包
A 依赖了 C,C 依赖了 D 的 version 0.2 版本
B 依赖了 D 的 version 0.1 版本
首先,根据 Maven 传递性依赖的管理原则:绝对不允许最终的 classpath 中出现同名不同版本的 jar 包 可知,最终添加到 classpath 中的 D 包只有一个,根据近者胜出的原则,我们可以知道,最终添加在 classpath 中的会是 D 的 version 0.1 版本的 jar 包。
如果项目的依赖关系是这样的:
Maven 的原则是谁在前,谁胜出
解决 Maven 的包冲突
还是这个项目的依赖关系树,我们知道最终添加到 classpath 中的会是 D 的 version 0.1 版本的 jar 包。如果说我的项目需要的是 D 的 version 0.2 版本的 jar 包,或者是我们本来需要的就是 version 0.2 版本的 jar 包,结果这样的依赖关系导致项目引入了 D version 0.1 ,出现了包冲突的问题,我们该如何解决?
首先,当你看到如下的异常时:
- AbstarctMethodError
- NoClassDefFoundError
- ClassNotFoundException
- LinkageError
很有可能就遇到了包冲突的问题。
示例:
项目 POM
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>hcsp</groupId>
<artifactId>resolve-package-conflict</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<profiles>
<profile>
<id>aliyunMavenMirror</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<pluginRepositories>
<pluginRepository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</pluginRepository>
</pluginRepositories>
<repositories>
<repository>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>
</profile>
<profile>
<id>mavenCentral</id>
<pluginRepositories>
<pluginRepository>
<id>mavenCentral</id>
<name>mavenCentral</name>
<url>https://repo.maven.apache.org/maven2</url>
</pluginRepository>
</pluginRepositories>
<repositories>
<repository>
<id>mavenCentral</id>
<name>mavenCentral</name>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.8.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.hcsp</groupId>
<artifactId>test-library-a</artifactId>
<version>0.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<argLine>-Dfile.encoding=UTF-8</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
Main
public class Main {
public static void main(String[] args) {
new com.github.hcsp.a.A().a();
}
}
运行程序,我们会看到报如下错误:
Exception in thread "main" java.lang.NoSuchMethodError: org.springframework.http.converter.json.MappingJacksonValue.getJsonpFunction()Ljava/lang/String;
at com.github.hcsp.a.A.a(A.java:7)
at Main.main(Main.java:3)
项目的依赖关系如下:
我们看到引入的 a 包依赖了 spring-web 4.3.6 版本的包,但是根据 Maven 解决冲突的就近原则,该包并没有被添加到 classpath 中
使用 mvn dependency:tree
命令,可以查看 Maven 解决冲突后的依赖关系树:
[INFO] hcsp:resolve-package-conflict:jar:1.0-SNAPSHOT
[INFO] +- org.springframework:spring-web:jar:5.1.8.RELEASE:compile
[INFO] | +- org.springframework:spring-beans:jar:5.1.8.RELEASE:compile
[INFO] | \- org.springframework:spring-core:jar:5.1.8.RELEASE:compile
[INFO] | \- org.springframework:spring-jcl:jar:5.1.8.RELEASE:compile
[INFO] +- com.github.hcsp:test-library-a:jar:0.4:compile
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:5.6.0:test
[INFO] | +- org.apiguardian:apiguardian-api:jar:1.1.0:test
[INFO] | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | \- org.junit.platform:junit-platform-commons:jar:1.6.0:test
[INFO] \- org.junit.jupiter:junit-jupiter-engine:jar:5.6.0:test
[INFO] \- org.junit.platform:junit-platform-engine:jar:1.6.0:test
我们看到 a 包的依赖被 Maven 解决掉了。
通过 Maven Helper 插件,也可以查看到依赖冲突:
我们看到,项目应用的包实际上是 spring-web 5.1.8 版本,a 包 依赖的 spring-web 4.3.6 被解决掉了
知道引起问题的原因后,解决方法就很简单了,我们只需要将 POM 中 spring-web 5.1.8 版本的依赖注释掉即可。