依赖坐标
在一个平面上,坐标(x,y)可以定位一个具体的位置。
在一个空间里,坐标(x,y,z)也可以定位一个具体的位置。
同理,在Maven仓库里面,同样有一套规则可以定位到一个具体的依赖包。
Maven坐标元素有五个:groupId、artifactId、version、packaging、classifier。
坐标元素 | 坐标含义 | 必须 |
---|---|---|
groupId | 开发依赖包的组织机构,通常采用域名反写 | 必须 |
artifactId | 依赖包的具体名称 | 必须 |
version | 依赖包的版本 | 必须 |
packaging | 依赖包的打包方式 | 默认值jar |
classifier | 依赖包插件输出的一些附属构建 | 不能直接定义 |
仓库里的(groupId,artifactId,version)就好比是三维世界里的(x,y,z)坐标。
可以把Maven仓库当成五维空间来理解。
依赖范围
Java编程有个非常重要的概念:classpath,它的含义是指明class字节码文件的位置!
在Maven里面,classpath细分为三种场景:
- 编译项目主代码(main)的时候,叫做“编译classpath”
- 运行项目主代码(main)的时候,叫做“运行classpath”
- 编译或运行测试代码(test)的时候,叫做“测试classpath”
依赖包在这三种classpath中是否有效可用,就是依赖范围的概念。
Maven提供了六种依赖范围:
依赖范围 | 编译classpath | 测试classpath | 运行classpath | 示例 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | Y | junit | ||
provided | Y | Y | servlet-api | |
runtime | Y | Y | jdbc驱动 | |
system | Y | Y | 仓库之外的,本地依赖包 |
表格中只有五种依赖范围,都会对classpath会产生影响,单元格中的Y表示依赖范围对应classpath是有效可用的,空白单元格表示无效不可用。
举例,引入依赖包junit并声明依赖范围为test:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
该依赖包只在编译和运行测试代码的时候有效,编译、运行主代码的时候无效。
还有一个依赖范围是import,导入依赖范围,它不会影响到classpath,就暂且不提它。
依赖传递性
A依赖B,B依赖C,那么,A和C之间就有了间接的联系。
我们说,A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。
传递性依赖的范围取决于第一直接依赖和第二直接依赖的范围。
Maven的官方文档给出了依赖性依赖范围的表格总结:
compile | provided | runtime | test | |
---|---|---|---|---|
compile | compile(*) | - | runtime | - |
provided | provided | - | provided | - |
runtime | runtime | - | runtime | - |
test | test | - | test | - |
第一列表示:第一直接依赖的范围
第一行表示:第二直接依赖的范围
中间的单元格:传递性依赖的有效范围,-表示传递性依赖无效。拿上面的例子来说,就是A无法通过传递性依赖关系引用到C,如果需要,就直接引入C的依赖坐标并定义依赖范围。
我自己有个疑问未解决:传递性依赖的作用结果为什么是这样的?这里面有什么样的逻辑含义呢?
依赖调解
有这样一个依赖关系:A -> B -> C -> X(1.0)
还有另一个依赖关系:A -> D -> X(2.0)
两条依赖路径,引入了两个不同版本的依赖包X,到底哪个会被Maven解析使用呢?
两个版本的X都解析肯定不对,同样的类存在两个以上,JVM就会随机加载其中一个,这样多半会出错的。
依赖调解就是解决这个问题的,它遵循两个原则:
- 路径最近者优先
- 第一声明者优先
在上面的例子中,第一个依赖路径长度是3,第二个依赖路径长度是2,所以X(2.0)会被解析使用,X(1.0)直接被抛弃。
当依赖路径长度相同时,调解的第一原则就不能解决问题了。
比如,两个依赖路径是:A -> B -> X(1.0) 和 A -> C -> X(2.0)
在Maven 2.0.8及之前的版本中,会解析哪个X依赖是不确定的。
从Maven 2.0.9 版本开始,调解的第二原则诞生了,在pom.xml文件中,谁先声明就用谁。
Maven在pom.xml中查找依赖的规则是自上而下递归查询。
在这个例子中,如果A的pom.xml文件里先声明了B依赖,就会先找到X(1.0),反之,就会找到X(2.0)。
有了这两个调解原则,重复依赖的管理就变成了一个确定的事情。
可选依赖
声明可选依赖的方法:
<dependency>
<groupId>org.example</groupId>
<artifactId>maven-a</artifactId>
<version>1.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
假如有这样的依赖关系:A -> B,B -> X,B -> Y,他们的依赖范围都是comiple。
如果X和Y不是可选依赖,那么通过传递性依赖关系,A会同时引入X和Y两个依赖。
如果X和Y是可选依赖,那么传递性依赖关系不再起作用,A不会引用到X和Y两个依赖包。
为什么Maven提供了可选依赖这个特性呢?
构建个场景:项目B实现了对数据库MySQL和Oracle的封装,并构建成依赖包,被项目A引用,但是A只用到了MySQL,不会用到Oracle。MySQL和Oracle就对应例子里的X和Y。
如果B对MySQL和Oracle的依赖声明不是可选的,那么A就会同时引入MySQL相关依赖和Oracle相关依赖,变得臃肿起来。
如果B对MySQL和Oracle的依赖声明是可选的,那么A就会缺少MySQL相关依赖,在A的pom.xml文件中显示引入MySQL就解决了这个问题,并且不会带入没用的Oracle依赖。
其实,在理想情况下,不应该使用可选依赖。因为面向对象编程中有个单一职责性原则,意思是代码职责应该单一,不要将太多职责糅合在一起。
单一职责性原则对Maven的项目规划应该同样适用。
考虑到单一职责性原则,我们将项目B拆分未两个项目B1和B2,一个实现对MySQL封装,一个实现对Oracle封装。A需要MySQL就引入B1依赖包。
如此一来,A就不用在自己的pom.xml文件中显示引入MySQL的相关依赖了。这也就避开了可选依赖的问题,而且从设计模式的角度来看,项目设计也会更加的合理些。
排除依赖
传递性依赖的好处是Maven帮我们隐式地引入了很多的依赖,极大的简化了依赖的管理。
但是,这样的好处可能也会带来一些问题:
- 可能会引入很多根本用不到的依赖包
- 引入的依赖包版本可能不是我们想要的
针对第一个问题,我们可以排除不想要的依赖包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
排除依赖包还有通配符的写法,这里就不展开了,理解意思就行。
针对第二个问题,排除掉版本不正确依赖包,然后显示引入版本正确依赖包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.example</groupId>
<artifactId>xxx</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>xxx</artifactId>
<version>2.0.0</version>
</dependency>
这里就是个举例,假如spring-boot-starter引入了依赖xxx的1.0.0版本,我们需要的是2.0.0的版本,就可以这么做。
依赖分析
Maven给出了三个依赖分析相关的命令:
- mvn dependency:list 查看当前项目解析出的所有依赖包
- mvn dependency:tree 查看当前项目的依赖树关系
- mvn dependency:analyze Maven对依赖结果的分析结果,可以参考,不可轻信!