1. 坐标
Maven坐标为各种构件引入了秩序,任何一个构件都有必须明确定义自己的坐标,而一组Maven坐标是通过一些元素定义的,它们是groupId, artifactId, version, packaging, classifier。
下面简单介绍一下这几个标签
1) groupId
定义当前Maven项目隶属的实际项目,当然跟实际项目之间并不是一对一的关系。其命名方式与Java包名的方式类似,通常与域名反向一一对应。例如org.sonatyp.nexus。在后面的聚合里面可以看到,同一项目的不同模块,拥有相同的groupId
2)artifactId
定义实际项目中的一个Maven项目(或者模块),推荐的做法是使用实际项目名称作为artifactId的前缀,这样的作用是当不同的项目拥有同样的模块名称时方便区分,如foo-core-1.2.jar, bar-core-1.2.jar等。
3)version
定义Maven项目当前所处的版本,Maven定义了一整套版本规范,以及快照的概念。
上面三个元素是必须定义的
4)packaging
定义项目的打包方式,包括pom,war,jar等,默认为jar.
5)classifier
用来帮助定义构建输出的一些附属构件。附属构件与主构件对应,例如foo-core.1.2.jar,还可以通过插件的方式生成foo-core-1.2-javadoc.jar等。该标签不能直接定义,因为附属构件不是由项目直接默认生成的,而是由附加的插件生成。
2.依赖
pom.xml 根元素project下的dependencies可以配置一个或多个dependency元素,以声明一个或多个项目依赖。(何为依赖?其实就是引用……)
每个依赖可以包含的元素有:
1)groupId, artifactId, version。基本坐标,上面已描述
2)type 依赖的类型,对应构件的packaging.大部分情况下,该元素不必声明,默认值为jar.
3)scope 依赖的范围
首先要知道,Maven在编译项目主代码的时候,需要使用一套classpath(简称编译classpath);在编译和执行测试的时候会使用另外一套classpath(简称测试classpath);最后,在实际运行Maven项目时,又会使用另外一套classpath(简称运行classpath,其实不是很懂,运行期间还用到Maven么?)
依赖范围就是用来控制依赖与这三种classpath之间的关系的
compile:编译依赖范围,默认的范围,对于三种classpath都有效。
test:测试依赖范围,只对于测试classpath有效。
provided:已提供依赖范围,对于编译和测试classpath有效,在运行时无效。
runtime: 运行时依赖范围,对于测试和运行时classpath有效,在编译时无效。
system:系统依赖范围,该依赖与provided的范围一致,但其依赖必须通过systemPath元素显式地指定依赖文件的路径。由于此类依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此慎用。如下:
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
import:导入依赖范围。后续再介绍。
4)exclusions 排除传递性依赖
依赖是会传递的,例如A依赖了B,B依赖了C,那么A和C之间便是传递性依赖。如果A实际上不需要依赖,那么可以通过此项将两者之间的依赖切断。
依赖范围不仅可以控制依赖与三种classpath的关系,还可以对传递性依赖产生影响。
首先假设A依赖了B,B依赖了C,那么,A和B之间是第一直接依赖,B和C之间是第二直接依赖,A和C之间是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。具体范围如下图,左边一列是第一直接依赖,上面一行为第二直接依赖
第一直接依赖| 第二直接依赖 compile test provided runtime
compile compile ---- ---- runtime
test test ---- ---- test
provided provided ---- provided provided
runtime runtime ---- ---- runtime
5)optional 依赖是否可选
A->B->X(可选), A->B->Y(可选),根据传递性,假如所有这几个依赖范围都是compile,那么X Y就是A的compile范围传递性依赖,然而,由于这里X Y都是可选的,依赖将不会传递,也就是说,X Y将不会对A造成影响。
3.依赖调解
依赖传递机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关系项目直接依赖是什么。
假设有如下场景:A->B->C->X(1.0) ,A->D->X(2.0),现在X有两个版本,那么哪个版本会被Maven解析呢?
Maven依赖调解第一原则是:路径最近者会被解析,那么上例中,X(1.0)将会被解析。
再考虑一个场景:A->B->X(1.0),A->D->X(2.0),依赖的路径都是2,哪个版本会被解析呢?
Maven依赖调解第二原则是:第一声明者优先。在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用。在这个例子中,在A的POM文件,如果B比D先声明,那么X(1.0)将被解析使用。
4.最佳实践
1)排除依赖,不赘述
2)归类依赖
假设项目中有很多对Spring Framework的依赖(org.springframework:spring-core:2.5.6, org.springframework:spring-beans:2.5.6等),这些依赖都是来自同一项目的不同模块,因此版本是相同的,可以预见未来如果要升级,那么版本号也是要一起升级的。
可以通过Maven属性,定义一些变量,统一使用,类似代码中的常量。使用类似EL表示式的变量形式。
<project>
......
<properties>
<springframework.version>2.5.6</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>......</groupId>
<artifactId>......</artifactId>
<version>${springframework.version}</version>
</dependency>
......
</dependencies>
3)优化依赖
mvn dependency:list 查看项目已解析的构件的列表
mvn dependency:tree 查看项目依赖的构件的依赖树
mvn dependency:analyze 分析项目对依赖的使用情况。主要是两部分
首先是Used undeclared dependencies,项目中使用到的但没有显式声明的依赖。看似没有任何问题,实际上也隐藏风险。例如A->B->X(1.0),A用到了X(1.0)的接口,却又不显式声明,那么当B的依赖升级变成X(2.0),可能接口变更,A就有可能编译失败。
其次是Unused declared dependencies,未使用的,却声明了的依赖。由于这个命令只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它发现不了,所以不能草率删除,应进行分析。