一:Java 的包原理

什么是包

JVM 的工作被设计地相当简单:

image.png

那么我们就会产生这样的疑惑:去哪里加载这些类呢?

答案就是:类路径(Classpath

回顾我们在上一章节所学习的内容:

对于 Main.java

  1. import org.apache.commons.lang3.StringUtils;
  2. public class Main {
  3. public static void main(String[] args) {
  4. System.out.println(StringUtils.isBlank(""));
  5. }
  6. }

我们首先要使用 javac 编译源文件:

  1. javac -classpath commons-lang3-3.9.jar Main.java

然后执行字节码文件,启动程序:

  1. java -cp commons-lang3-3.9.jar:. Main

我们使用了 -classpath/-cp 来告诉 JVM 去哪里加载我们需要的类文件

而这里面的 jar 包,实际上就是我们所说的“包”

jar 其实就是将很多类文件打包后的一个压缩包,我们导入 jar 后,可以直接使用里面的类或调用其中的功能。

什么是包管理

传递性依赖

我们依赖的包还依赖了别的类,这种依赖是具有传递性的,传递性依赖带来的最大的问题就是 jar hell

我们在 -classpath 后会添加项目依赖的各种各样的 jar 包,试想一下,如果两个仅仅不同版本的 jar 包被同时写进了 -classpath 参数里面,会出现什么问题?

首先,JVMclasspath 中寻找类文件的顺序是从前找到后的,也就是说如果有两个仅仅不同版本的 jar :demo-1.0.jardemo-2.0.jar ,哪个放在前面哪个就会被使用。如果 demo-1.0.jar 的顺序在 demo-2.0.jar 之前,就出现问题了。

我们知道全限定类名是类的唯一标识,在 demo-1.0.jardemo-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 是一个项目管理工具,它包含:

  1. 一个项目对象模型(Project Object Model
  2. 一组标准集合
  3. 一个项目生命周期(Project Lifecycle
  4. 一个依赖管理系统(Dependency Management System
  5. 以及用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑

Maven 是如何进行包管理的

Maven 包管理的做法是:Convention over configuration(约定优于配置原则,这一点体现在 POM

POMProject Object Model)是 Maven 工程的基本工作单元,是一个 XML 文件,该文件中包含了项目的基本信息,用于描述项目如何构建,声明项目依赖等等。

POM 文件示例:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>com.github</groupId>
  6. <artifactId>test</artifactId>
  7. <version>0.0.1-SNAPSHOT</version>
  8. <name>test</name>
  9. <description>Demo project for Spring Boot</description>
  10. <properties>
  11. <java.version>11</java.version>
  12. </properties>
  13. <dependencies>
  14. <dependency>
  15. <!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成,
  16. 如com.companyname.project-group,
  17. maven会将该项目打成的jar包放本地路径:/com/companyname/project-group -->
  18. <groupId>org.apache.commons</groupId>
  19. <!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 -->
  20. <artifactId>commons-lang3</artifactId>
  21. <!-- 版本号 -->
  22. <version>3.4</version>
  23. </dependency>
  24. </dependencies>
  25. </project>

如果我们需要在项目中引入第三方包,必须要遵守 POM 的约定,指定 groupIdartifactId 以及 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 依赖冲突的解决原则:最近的胜出

什么叫最近的胜出呢?

示例:

image.png

你的项目的依赖关系树如下:

项目依赖 A,B 两个 jar

A 依赖了 C,C 依赖了 Dversion 0.2 版本

B 依赖了 Dversion 0.1 版本

首先,根据 Maven 传递性依赖的管理原则:绝对不允许最终的 classpath 中出现同名不同版本的 jar 包 可知,最终添加到 classpath 中的 D 包只有一个,根据近者胜出的原则,我们可以知道,最终添加在 classpath 中的会是 Dversion 0.1 版本的 jar 包。

如果项目的依赖关系是这样的:

image.png

Maven 的原则是谁在前,谁胜出

解决 Maven 的包冲突

image.png

还是这个项目的依赖关系树,我们知道最终添加到 classpath 中的会是 Dversion 0.1 版本的 jar 包。如果说我的项目需要的是 Dversion 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)

项目的依赖关系如下:
image.png
我们看到引入的 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 插件,也可以查看到依赖冲突:

image.png

我们看到,项目应用的包实际上是 spring-web 5.1.8 版本,a 包 依赖的 spring-web 4.3.6 被解决掉了

知道引起问题的原因后,解决方法就很简单了,我们只需要将 POMspring-web 5.1.8 版本的依赖注释掉即可。