【Maven】里有一个仓库的概念,这个概念和生活中的【仓库】理解起来一模一样:一大堆Jar包(货物)摆放在规则有序(坐标)的货架上等待人们来领取。
不知道大家有没有去【菜鸟驿站】领过快递包裹,一个货架有4排,每排可以存放25件快递,总共12个货架,所以一个快递的唯一坐标就确定了——【货架号-排】 (网图侵删)
本章主要学习摆放、存取物的规则,即【Maven】世界的坐标与依赖。
Maven坐标(规则)
Maven坐标总的来说由五个部分组成,其中加粗的三个是最重要且最常用的组成方式:
**groupId**
:表明 哪个组织发布的,通常使用公司域名的倒序写法,比如我的域名codeleven.com
,那么groupId
里就写com.codeleven
**artifactId**
:表明是 哪个项目,通常使用实际项目名称作为artifactId
的前缀。**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版本的时候 需要注意 可能存在各种各样的问题。packaging(可选)
:什么打包方式,比如jar包
、war包
等等classifier(可选)
: 附属构建属性。比如xxx-5.2.2.RELEASE.jar
、xxx-5.2.2.RELEASE-sources.jar
,这个-sources
就是附属构建的产物。这个属性不能人为指定,只能通过附加的插件产生。
依赖(货物)
我们知道了坐标后,就能根据坐标去查找相应的依赖了。使用坐标的语法如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version></version>
<scope>test</scope>
<type></type>
<optional></optional>
</dependency>
groupId
、artifactId
、version
:这三个是基本的坐标三要素type
:对应“坐标”概念中的“packaging”scope
:依赖的范围(在后面会具体讲到)optional
:标记依赖是否可选(在后面会具体讲到)
依赖范围scope
在工程开发过程中,肯定避免不了各种各样的测试和生产环境,测试环境会有测试环境的配置,生产环境会有生产环境的配置。对于编译构建工具来说也是类似的:有些库仅在测试时使用,有些库在生产环境中已经提供了(不需要再引入),有些库是必须存在的。不同的应用场景下延伸出不同的需求,Maven
也确实做到了这一点:
允许 编译、测试、运行 时使用不同的**classpath**
。
那么如何做呢?Maven
提供了几种不同的scope
:
compile
,这是默认的依赖范围。表明无论是 编译、测试还是运行都需要包含这个包。test
,这是仅在测试时生效的依赖范围。在编译主代码、运行项目时不会用到这个范围的依赖。provided
,已提供的依赖范围,这个可能不太好理解,简单来说就是运行环境已经提供了这个库了,不需要我们再次引入的意思。这个依赖范围仅再 编译、测试时生效。最典型的例子就是servlet-api.jar
。不知道会不会有小伙伴好奇,运行环境怎么提供库让我这个工程使用呢?我们拿
Tomcat
来说,我们把自己的war
包编译打包好后丢入webapp
目录,Tomcat
会去启动我们的工程,我们的工程会被载入到一个classpath
中,类似这样的效果: Tomcat的类加载方案(网图侵删) 咱们的项目会挂在一个新的WebAppClassLoader
上,然后都是**SharedClassLoader**
的子类加载器,所以根据双亲委派模式,如果咱们工程的代码想要使用servlet-api.jar
的接口,就会往上查找。然后Tomcat
自身会提供servlet-api.jar
在上层的类加载器中,咱们的工程就能找到了。runtime
,运行时依赖范围。对于测试和运行环境下才生效,编译主代码时无效,最常见的例子就是jdbc
驱动,只有在测试和运行时才需要用到具体的JDBC
驱动。正常来说都是对照着
java.sql
进行编写的,除非真用到了具体的JDBC
驱动的功能,才会使用compile
范围system
,系统依赖范围,作用范围和provided
一致,只不过使用时需要与<systemPath>
绑定,而<systemPath>
又与本机路径绑定,所以可能造成pom.xml
不能简单移植的问题。<systemPath>
是用来直接引入本地jar包
用的import
,导入依赖范围。这个在spring boot
中很常见,通常用于引入一堆依赖声明,只有在工程pom.xml
中再定义一遍(不需要指定版本),就能按照声明的依赖使用。(这个在后续会详细说明)
上述五种(除了import
)的依赖 范围与classpath
的关系:
传递性依赖
一个库可能依赖另外一个库,另外一个库又可能依赖其他库,如此往复。回想当初刚学SSM
的时候,那个找包找的痛苦哇,一个个放入到eclipse
的classpath
中然后根据错误再去百度、下载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=test
、scope=provided
的其他库,那么就不引入这些库。因为test
是测试环境用的,所以不引入;而provided
是运行环境提供的,并没有编译到这个包里来,不影响接口使用,所以也直接移除即可。其他的也是类似的原因。
依赖调节
- 有一个
jar包:B
,它的依赖链为:A->B->C->X(1.0)
- 有一个
jar包:D
,它的依赖链为:A->D->X(2.0)
如果项目:A
同时引入 jar包:B
和 jar包:D
,那么究竟使用哪个版本的jar包:X
呢?
这里有两个原则:
- 首先是就近原则,哪个链路最短,就用哪个链路的。这个例子中就使用
jar包:D
依赖的jar包:X(2.0
- 倘若链路都一样长,就用在
pom.xml
中最先声明的那个依赖的库。比如A->E->X(1.0)
和A->D->X(2.0)
,那么由最先声明的<dependency>
决定。
可选依赖 optional
如果 工程:B
对 jedis(一个Redis客户端的库)
使用了<optional>
,那么工程:A
在依赖 工程:B
的时候,【Maven】不会传递性依赖工程:B
的jedis包
,仅仅是不会传递依赖这个加了<optional>
的包。
这种做法给抽象通用平台提供了很大的灵活性,考虑一种场景:
我们开发了一套抽象的通用的数据框架,底层可以是Redis
,也可以是Elasticsearch
。但是具体使用哪个引擎来存储取决于使用人。比如使用人说我要用Redis
作为底层存储数据,那么使用人在自己的pom.xml
里引入Redis
即可;有人要用Elasticsearch
来作为底层存储数据,那么就引入Elasticsearch
的包即可。
如果无法想象就考虑Spring提供的
@ConditionalOnClass
,如果发现类路径中存在指定的类,就使用某一套Configuration来加载。那么结合optional机制,我们想使用Redis
就引入Redis
客户端的包,然后Spring
扫描到了Redis
包的类,就是用Redis
的那一套配置进行加载。
然鹅,在《Maven实战》这一书中更建议,为Redis
和Elasticsearch
(原著中是MySQL
和PostgreSQL
)分别各自起一个项目,原因是一个类应该只有一项职责。 ————《Maven实战》P68
依赖冲突
工作中应该少不了发生依赖冲突的情况,尤其是在搭建一个新项目的框架时:
Caused by: java.lang.ClassNotFoundException: A.B.C
Caused by: java.lang.NoSuchMethodError: X.method() not found
产生这个问题的原因主要是因为Maven
的依赖传递引入的 相同项目但不同版本的依赖,然后又根据 依赖调节 原则选择了低版本的依赖。
通常来说 高版本的依赖 都会去兼容 低版本 的依赖,使用高版本不会有问题。但是!笔者曾经遇到过一个现象,
okhttp3
和okhttp2
,腾讯某平台提供的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>