Java包管理的那些事——Maven与Ivy

Posted by Bo on May 10, 2019

前几篇文章我们提到了,JVM和JVM的包管理机制相当简单:

  • JVM一辈子只干一件事,读字节码文件,执行字节码。
  • 每当JVM看到一个全限定类名,就尝试从classpath中加载。

只要你铭记这两个原则,一切的问题就都能迎刃而解。

只是,你要如何把JVM所需的字节码完整、正确地塞到classpath中呢?

完整——意思是说,任何时候,JVM都不会哭丧着脸说,咋办哥,有个类我找不着啊(NoClassDefFoundError)。 正确——意思是说,classpath中的字节码正好是你想要的那个版本,JVM不会舔着脸说,咋办哥,类A的说明书里让我调用类B的方法,但是类B的说明书里没这个方法啊,你是不是拿错说明书了(NoSuchMethodError)?

Java问世的最初几年,业界一直在探索,究竟怎么才能科学合理地满足JVM这个小妖精的要求?

1

我们之前提到过,JVM这个二傻子只认类的说明书(字节码),你要使用一个库,你就得想办法把这份说明书以及它引用的说明书(传递性依赖)找到,然后喂给JVM。

看上去不是非常困难是不是?想象一下你的程序运行需要一万份说明书吧(传递性依赖的传递性依赖的传递性依赖)。

2004年,Maven问世了。必须说明的是,包管理只是Maven的一个功能,或者说子系统/模块。作为传世经典,Maven能做的事情远不止于此,不过我们现在只研究它的包管理功能。

Maven念了两句诗,说,很惭愧,我只做了两点微小的工作。

1

第一,我给每叠说明书都编了三个号码:品牌(groupId)、型号(artifactId)以及版本(version)。你不用费劲吧啦地去满世界下载说明书了,告诉我品牌型号版本,我自己去仓库(repository)里翻。 第二,我在每叠说明书旁边都放了一个清单,指明了这叠说明书里面引用的别的说明书的号码。

因此,从现在开始,你不再需要亲自管理classpath了——不再需要自己找全所有的正确的说明书,也不再需要自己拼装classpath字符串喂给JVM了。

你只需要告诉Maven,我要用这个牌子这个型号的东西,还有那个牌子那个型号的东西,你给我安排一下。

Maven说:

1

在这个过程中,发生了什么呢?

  • Maven首先按照你的要求,去仓库中按照编号找到你需要的那叠说明书。
  • Maven查看了一下说明书旁边放的清单,发现这叠说明书依赖了另外几个牌子型号的说明书,于是继续在仓库里翻找。
  • 重复这个过程,直到找全所有需要的说明书。
  • 如果这些说明书之间存在冲突,解决之(稍后会讲到)。
  • 将所有用到的说明书下载到本地,然后拼装一个很长的classpath字符串,启动JVM(或者javac)。

你看,Maven在这里体现出了自己的强大,却也屏蔽了所有的细节。你得到了一份便利,却失去了掌控全局的能力。

我们来用一个例子向你展示这一切的细节。假设你现在需要使用JUnit 4。你写了一个测试,其中使用了org.junit.Test类。JVM需要读取这个类的说明书(字节码)才知道怎么工作。

于是你熟练的搜索junit maven,在pom.xml里加入了:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

我们的故事就从这里开始。现在,你告诉了Maven,我要用junit(groupId)牌的junit(artifactId)的版本4.12(version)的那么一叠说明书。

于是Maven开始在仓库里翻找。这个翻找包括两个仓库:本地仓库与中央仓库。本地仓库就是你本机的~/.m2目录,中央仓库由你的pom.xml或者~/.m2/settings.xml定义,说白了就是个链接。

Maven首先根据型号在本地仓库里翻找,很快就找到了你要的junit牌的junit4.12版本的那叠说明书(~/.m2/repository/junit/junit/4.12/junit-4.12.jar)。如果你有兴趣可以把它解压缩来看看,其中就包含了你需要的org.junit.Test类的说明书(org/junit/Test.class字节码)。

别着急,还没完。Maven在这叠说明书旁边还看到了一个清单:~/.m2/repository/junit/junit/4.12/junit-4.12.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>

    <!-- 省略啰里八嗦的一堆内容 -->
    <dependencies>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
            <version>1.3</version>
        </dependency>
    </dependencies>

换句话说,junit:junit:4.12的清单指明,自己依赖org.hamcrest牌的hamcrest-core1.3版本的那叠说明书。这次不巧Maven在本地仓库没找到这叠说明书,于是它访问了远程仓库,开心地找到了说明书和清单:

  • http://central.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar
  • http://central.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.pom
    • 这个清单中不包含传递性依赖,因此Maven知道,到此为止吧。

之后,Maven将它们下载回本地仓库,方便下次使用。现在,Maven就可以拼装出一个classpath启动JVM(或者javac了):

-classpath ~/.m2/repository/junit/junit/4.12/junit-4.12.jar:~/m2/repository/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar

可以看到,Maven所做的一切相当平铺直叙,什么黑科技也没有,仅仅是:

  • 按照你指定的品牌型号和版本拼装磁盘路径或者URL,查找指定的jar包,并反复重复这个过程。
  • 用查找到的所有jar包拼装出来一个classpath,启动JVM。