【Maven】里有一个仓库的概念,这个概念和生活中的【仓库】理解起来一模一样:一大堆Jar包(货物)摆放在规则有序(坐标)的货架上等待人们来领取。

不知道大家有没有去【菜鸟驿站】领过快递包裹,一个货架有4排,每排可以存放25件快递,总共12个货架,所以一个快递的唯一坐标就确定了——【货架号-排】 🚚坐标与依赖 - 图1 (网图侵删)

本章主要学习摆放、存取物的规则,即【Maven】世界的坐标与依赖

Maven坐标(规则)

Maven坐标总的来说由五个部分组成,其中加粗的三个是最重要且最常用的组成方式:

  1. **groupId**:表明 哪个组织发布的,通常使用公司域名的倒序写法,比如我的域名codeleven.com,那么groupId里就写com.codeleven
  2. **artifactId**:表明是 哪个项目,通常使用实际项目名称作为artifactId的前缀。
  3. **version**:什么版本的,这版本号定义有个规范,如下所示

    约定 < 主版本 >.< 次版本 >.< 增量版本 >-< 里程碑版本 >
    1 、表示该版本的主版本号
    2 、表示该版本的次版本号
    3 、表示该版本的增量版本号; 主版本:表示项目的重大架构变更。例如: Maven2 和 Maven1 相去甚远; Struts1 和 Struts2 采用了 不同的架构。 次版本:表示较大范围的功能增加和变化,及 Bug 修复。例如 Nexus 1.5 较 1.4 添加了 LDAP 的支持,并且修复了很多 Bug, 但是从总体架构来说,没有什么变化。 增量版本:顾名思义,这往往指某一个版本的里程碑。例如, Maven3 已经发布了很多里程碑版本,如:3.0-alpha-1 、 3.0-alpha-2 、 3.0-bata-1 等。这里的版本与正式版本 3.0 相比,往往表示不是非常稳定,还需要很多测试。我们用spring 或者开源框架 都是会选择 release稳定版本,这样版本通常在线上运行了一段时间,很稳定了。所以大家如果在引用别人SNAPSHOT版本的时候 需要注意 可能存在各种各样的问题。

  4. packaging(可选):什么打包方式,比如jar包war包等等

  5. classifier(可选): 附属构建属性。比如xxx-5.2.2.RELEASE.jarxxx-5.2.2.RELEASE-sources.jar,这个-sources就是附属构建的产物。这个属性不能人为指定,只能通过附加的插件产生。

依赖(货物)

我们知道了坐标后,就能根据坐标去查找相应的依赖了。使用坐标的语法如下所示:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-test</artifactId>
  4. <version></version>
  5. <scope>test</scope>
  6. <type></type>
  7. <optional></optional>
  8. </dependency>
  • groupIdartifactIdversion:这三个是基本的坐标三要素
  • type:对应“坐标”概念中的“packaging”
  • scope:依赖的范围(在后面会具体讲到)
  • optional:标记依赖是否可选(在后面会具体讲到)

依赖范围scope

在工程开发过程中,肯定避免不了各种各样的测试和生产环境,测试环境会有测试环境的配置,生产环境会有生产环境的配置。对于编译构建工具来说也是类似的:有些库仅在测试时使用,有些库在生产环境中已经提供了(不需要再引入),有些库是必须存在的。不同的应用场景下延伸出不同的需求,Maven也确实做到了这一点:
允许 编译、测试、运行 时使用不同的**classpath**

那么如何做呢?Maven提供了几种不同的scope

  1. compile,这是默认的依赖范围。表明无论是 编译、测试还是运行都需要包含这个包
  2. test,这是仅在测试时生效的依赖范围。在编译主代码、运行项目时不会用到这个范围的依赖。
  3. provided已提供的依赖范围,这个可能不太好理解,简单来说就是运行环境已经提供了这个库了,不需要我们再次引入的意思。这个依赖范围仅再 编译、测试时生效。最典型的例子就是servlet-api.jar

    不知道会不会有小伙伴好奇,运行环境怎么提供库让我这个工程使用呢?我们拿Tomcat来说,我们把自己的war包编译打包好后丢入webapp目录,Tomcat会去启动我们的工程,我们的工程会被载入到一个classpath中,类似这样的效果: 🚚坐标与依赖 - 图2 Tomcat的类加载方案(网图侵删) 咱们的项目会挂在一个新的WebAppClassLoader上,然后都是**SharedClassLoader**的子类加载器,所以根据双亲委派模式,如果咱们工程的代码想要使用servlet-api.jar的接口,就会往上查找。然后Tomcat自身会提供servlet-api.jar在上层的类加载器中,咱们的工程就能找到了。

  4. runtime运行时依赖范围。对于测试和运行环境下才生效,编译主代码时无效,最常见的例子就是jdbc驱动,只有在测试和运行时才需要用到具体的JDBC驱动。

    正常来说都是对照着java.sql进行编写的,除非真用到了具体的JDBC驱动的功能,才会使用compile范围

  5. system系统依赖范围,作用范围和provided一致,只不过使用时需要与<systemPath>绑定,而<systemPath>又与本机路径绑定,所以可能造成pom.xml不能简单移植的问题。

    <systemPath>是用来直接引入本地jar包用的

  6. import导入依赖范围。这个在spring boot中很常见,通常用于引入一堆依赖声明,只有在工程pom.xml中再定义一遍(不需要指定版本),就能按照声明的依赖使用。(这个在后续会详细说明)

上述五种(除了import)的依赖 范围与classpath的关系:
image.png

传递性依赖

一个库可能依赖另外一个库,另外一个库又可能依赖其他库,如此往复。回想当初刚学SSM的时候,那个找包找的痛苦哇,一个个放入到eclipseclasspath中然后根据错误再去百度、下载jar包再放入,恶心的是有些包还放在CSDN上,要积分才能下载。而Maven也解决了这个问题,它会自动去解析咱们依赖的第一个库,然后如果这个库还依赖其他库,那么Maven自动给我们下载其他所需的依赖。 咱们还是举个例子来说明:
点击查看【processon】
咱们工程的pom.xml里直接依赖了下面两个包:

  • org.mockito:mockito-core
  • com.fasterxml.jackson.dataformat:jackson-dataformat-xml

Maven在引入时会帮我们去解析这两个包还会依赖哪些库,然后帮我们找到并下载过来。这就是传递性依赖。但是如果 org.mockito:mockito-core 引入时的作用范围是test,那么它的依赖的是什么作用范围呢?这里有一张表供参考:

compile test provided runtime
compile compile / / runtime
test test / / test
provided provided / provided provided
runtime runtime / / runtime

表示直接依赖
表示依赖的依赖

如果我们用 scope=compile 引入库,那么Maven在解析这个库时,若发现这个库依赖了scope=testscope=provided的其他库,那么就不引入这些库。因为test是测试环境用的,所以不引入;而provided是运行环境提供的,并没有编译到这个包里来,不影响接口使用,所以也直接移除即可。其他的也是类似的原因。

依赖调节

  • 有一个jar包:B,它的依赖链为:A->B->C->X(1.0)
  • 有一个jar包:D,它的依赖链为:A->D->X(2.0)

如果项目:A同时引入 jar包:Bjar包:D,那么究竟使用哪个版本的jar包:X呢?
这里有两个原则:

  1. 首先是就近原则,哪个链路最短,就用哪个链路的。这个例子中就使用jar包:D依赖的jar包:X(2.0
  2. 倘若链路都一样长,就用在pom.xml中最先声明的那个依赖的库。比如A->E->X(1.0)A->D->X(2.0),那么由最先声明的<dependency>决定。

可选依赖 optional

如果 工程:Bjedis(一个Redis客户端的库) 使用了<optional>,那么工程:A在依赖 工程:B 的时候,【Maven】不会传递性依赖工程:Bjedis包,仅仅是不会传递依赖这个加了<optional>的包。

这种做法给抽象通用平台提供了很大的灵活性,考虑一种场景:
我们开发了一套抽象的通用的数据框架,底层可以是Redis,也可以是Elasticsearch。但是具体使用哪个引擎来存储取决于使用人。比如使用人说我要用Redis作为底层存储数据,那么使用人在自己的pom.xml里引入Redis即可;有人要用Elasticsearch来作为底层存储数据,那么就引入Elasticsearch的包即可。

如果无法想象就考虑Spring提供的 @ConditionalOnClass ,如果发现类路径中存在指定的类,就使用某一套Configuration来加载。那么结合optional机制,我们想使用Redis就引入Redis客户端的包,然后Spring扫描到了Redis包的类,就是用Redis的那一套配置进行加载。

然鹅,在《Maven实战》这一书中更建议,为RedisElasticsearch(原著中是MySQLPostgreSQL)分别各自起一个项目,原因是一个类应该只有一项职责。 ————《Maven实战》P68

依赖冲突

工作中应该少不了发生依赖冲突的情况,尤其是在搭建一个新项目的框架时:

Caused by: java.lang.ClassNotFoundException: A.B.C 
Caused by: java.lang.NoSuchMethodError: X.method() not found

产生这个问题的原因主要是因为Maven依赖传递引入的 相同项目不同版本的依赖,然后又根据 依赖调节 原则选择了低版本的依赖。

通常来说 高版本的依赖 都会去兼容 低版本 的依赖,使用高版本不会有问题。但是!笔者曾经遇到过一个现象,okhttp3okhttp2,腾讯某平台提供的SDK是 okhttp2 的,然后 spring-cloud链路追踪 用的是okhttp3的,而okhttp3不兼容okhttp2,导致无论使用哪种依赖都不对!最后是选择将腾讯平台的SDK(开源的)下下来,手动改成okhttp3然后使用。

那么倘若真发生了这些异常情况,我们该怎么去找到是哪个包有冲突呢?使用mvn dependency:tree
其次,找到了包有冲突该如何解决呢?在对应的<dependency>下加入<exclusions>元素:

<dependency>
    <groupId>A</groupId>
    <artifactId>A</artifactId>
    <version>1.0</version>
    <exclusions>
            <exclusion>
                <groupId>C</groupId>
                <artifactId>C</artifactId>
            </exclusion>
    </exclusions>
</dependency>