来自《Maven实战》第五章
Maven
的一大功能是管理项目, 为了能自动化的解析任何一个java
构件,Maven
必须将它们唯一标识,这就是依赖管理的底层基础–坐标.
坐标为构件引入秩序
关于坐标最熟悉的应该是平面几何, 在平面几何坐标系中, 坐标(x,y)能够唯一标识平面中的一个点. 实际生活中, 地址也是一种坐标, 通过省市县区街道等一系列的信息,可以唯一标示城市中的任意地址, 邮局和快递正是基于这样一种坐标进行日常的工作.
对应于平面中的点和城市中的地址, Maven
的世界中都有数量非常巨大的构件,也就是平时
用的一些war
、jar
等文件。Maven
通过定义了一组规则,使世界上任何一个构
件都可以使用Maven
坐标进行唯一标识,这大大改善了开发中查找构件和依赖的过程。
Maven
坐标的元素包括goupld
、artifactld
、version
、packaging
、clasifier
。只要我们提供正确的坐标元素,Maven
就能找到对应的构件。
需要使用某个构件时,只需要告诉Maven
构件的坐标元素,Maven就会从仓库中寻找相应的构件供我们使用。Maven内置了一个中央仓库的地址
(htp://repol.maven.ang/mavern2),该中央仓库包含了世界上大部分流行的开源项目构件,Maven
会在需要的时候去那里下载。
在使用Maven
开发自己项目的时候,Maven
强制要求为其定义适当的坐标。在这个基础上,其他Maven
项目才能引用该项目生成的构件。
坐标详解
Maven
坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,它们是通过groupld
、artifaclld
、vesion
、packaging
、classifer
定义的。如下:
<!-- 这是nexus-indexer的坐标定义,nexus-indexer是一个对Maven仓库编集索引并提供搜索
功能的类库,它是Nexus项目的一个子模块 -->
<groupId>org.sonatype.nexus <向roup1d>
<artifactId>nexus-indexer</artifactld>
<version>2.0.0<version>
<packaging>jar<packaging>
项目构件的文件名是与坐标相对应的,一般的规则为artifactld-version [-classifier].packaging,[-clasifier]表示可选。
-
groupId
:必选,定义当前Maven项目隶属的实际项目。首先,Maven项目和实际项目不一定是一对一的关系。比如SpringFramework这一实际项目,其对应的Maven项目会有很多,如spring-core、spring-context等。这是由于Maven中模块的概念,因此,一个实际项目往往会被划分成很多模块。
其次,groupId不应该对应项目隶属的组织或公司。一个组织下会有很多实际项目,如果groupId只定义到组织级别,那么实际项目这个层将难以定义。
最后,groupId的表示方式与Java包名的表示方式类似,通常与域名反向一对应。上例中,groupId为org.sonatype.nexus,org.sonatype表示Sonatype公司建立的一个非盈利性组织,nexus 表示Nexus这一实际项目,该groupId与域名nexus.sonatype.org对应。 -
artifactId
:必选,该元素定义实际项目中的一个Maven
项目(模块),推荐的做法是使用实际项目名称作为前缀。比如上例中的
artifactld
是nexus-indexer
,使用了实际项目名nexus
作为前缀,这样做的好处是方便寻找实际构件。在默认情况下,Maven
生成的构件,其文件名会以artifactId
作为开头,如nesxus-indexer-2.0.0.jar
,使用实际项目名称作为前缀之后,就能方便从一个lib
文件夹中找到某个项目的一组构件。 -
version
: 必选,该元素定义Maven
项目当前所处的版本,如上例中nexus-indexer
的版本是2.0.0。 -
pakaging
: 非必选,该元素定义Mavcn
项目的打包方式,默认值为jar,可选值[war、jar、pom]打包方式通常与所生成构件的文件扩展名对应,如上例中packaging为jar,最终的文件名为nexus-indexer-2.0.0.jar,packaging并非一定与构件扩展名对应,比如packaging为maven-plugin的构件扩展名为jar。
-
classifier
: 不能直接定义,需要依赖插件,定义构建输出的一些附属构件。附属构件与主构件对应,如上例中的主构件是nexus-indexer-2.0.0.jar,还可使用一些插件生成如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-sources.jar这样的附属构件,其包含了Java文档和源代码。这时候,javadoc和sources就是这两个附属构件的classifier。这样,附属构件也就拥有了自己唯一的坐标。
注意,不能直接定义项目的classifier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。
依赖的配置
首先需要知道,Maven
在编译项目主代码的时候需要使用一套classpath
。其次,Maven
在编译和执行测试的时候会使用另外一套classpath
,不同的是这里的依赖范围是test
。最后,实际运行Maven
项目的时候,又会使用一套claspath
。依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系,Maven
有以下几种依赖范围:
compile
:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的Maven
依赖,对于编译、测试、运行三种classpath
都有效。test
:测试依赖范围。使用此依赖范围的Maven
依赖,只对于测试classpath
有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是JUnit
,它只有在编译测试代码及运行测试的时候才需要。provided
:已提供依赖范围。使用此依赖范围的Maven
依赖,对于编译和测试classpath
有效,但在运行时无效。典型的例子是servlet-api
,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复地引入.runtime
:运行时依赖范围。使用此依赖范围的Maven
依赖,对于测试和运行classpath
有效,但在编译主代码时无效。典型的例子是JDBC
驱动实现,项目主代码的编译只需要JDK
提供的JDBC
接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC
驱动。system
:系统依赖范围。该依赖与三种classpath
的关系,和provided
依赖范围完全一致。但是,使用system范围的依赖时必须通过systemPath
元素显式地指定依赖文件的路径。需要注意此类依赖不是通过Maven
仓库解析的。sysemPath
元素可以引用环境变量。
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.3</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/postgresql-9.3.jar</systemPath>
</dependency>
使用这种方式引入的JAR包在打包时不会一起打包,所以打包后找不到该JAR包
解决办法是,将jar包放到
resouces
目录下,使用system
依赖引入
- import:导入依赖范围,该依赖范围不会对三种
classpath
产生实际的影响,该依赖针对的是packaging
为pom
类型的。
依赖范围scope | 对于编译classpath有效 | 对于测试classpath有效 | 对于运行时classpath有效 | 例子 |
---|---|---|---|---|
compile | 是 | 是 | 是 | spring-core |
test | 否 | 是 | 否 | JUnit |
provided | 是 | 是 | 否 | servlet-api |
runtime | 否 | 是 | 是 | jdbc驱动实现 |
system | 是 | 是 | 否 | 本地的,Maven仓库之外的类库文件 |
import | - | - | - | - |
传递性依赖
概述
在没有Maven
的情况下,在项目中引入spring
,而spring
又依赖其他的开源类库,一般就只能等运行报错,在去查文档,去网站下载其他的开源类库引入进来,非常麻烦。Maven
的传递性依赖就是解决这个问题的。
在传递性依赖的机制下,Maven
会自动解析各个直接依赖的POM
,将必要的间接依赖,以传递性依赖的形式引入到当前项目中。
传递性依赖和依赖范围
依赖范围不仅可以控制依赖与三种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 |
- 当第二直接依赖范围是
compile
时,依赖范围和第一直接依赖范围相同. - 当第二直接依赖范围是
test
时,依赖范围不传递。 - 当第二直接依赖范围是
provided
时,只传递第一依赖范围是provided
的依赖,而且传递依赖范围同样是provided
。 - 当第二直接依赖范围是
runtime
的,依赖范围和第一直接依赖范围相同,但compile
除外,此时传递依赖范围是runtime
。
依赖调解原则
- 依赖调解第一原则:路径最近者优先。 当出现A->B->C->X(1.0), A->D->X(2.0), X是A的传递性依赖,但是两条依赖路径下,有两个版本的X,为避免造成依赖重复,必须选择一个,因为X1.0的依赖路径为3, 2.0的依赖路径为2,所以Maven会选择X2.0,
- 依赖调解第二原则:第一声明者优先。在依赖路径相等的情况下,在POM依赖声明中顺序靠前的依赖优胜。
可选依赖
可选依赖是通过dependency
的optional
属性声明的,理想情况下是不需要可选依赖的。
最佳实践
排除依赖
项目A依赖项目B,项目B依赖于项目C,由于一些原因,不想通过传递性依赖来引入C,而是自己显式的声明对项目C的依赖,这时候可以通过exclusions元素声明排除依赖。
<dependency>
<groupId>sample.b</groupId>
<artifactId>project-b</artifactId>
<version>1.0</version>
<scope>compile</scope>
<exclusions>
<!-- 排除依赖 -->
<exclusion> <!-- declare the exclusion here -->
<groupId>sample.c</groupId>
<artifactId>Project-c</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 自己依赖 -->
<dependency>
<groupId>sample.c</groupId>
<artifactId>project-c</artifactId>
<version>1.0</version>
</dependency>
优化依赖
- 查看当前项目已解析的依赖:
mvn dependency:list
可以看到下图中的已解析的依赖和每个依赖的范围
[INFO]
[INFO] The following files have been resolved:
[INFO] org.springframework:spring-beans:jar:2.5.6:compile
[INFO] javax.activation:activation:jar:1.1:compile
[INFO] javax.mail:mail:jar:1.4.7:compile
[INFO] org.slf4j:slf4j-api:jar:1.7.21:test
[INFO] commons-logging:commons-logging:jar:1.1.1:compile
[INFO] org.springframework:spring-context-support:jar:2.5.6:compile
[INFO] aopalliance:aopalliance:jar:1.0:compile
[INFO] junit:junit:jar:4.7:test
[INFO] com.icegreen:greenmail:jar:1.5.5:test
[INFO] com.sun.mail:javax.mail:jar:1.5.6:test
[INFO] org.springframework:spring-core:jar:2.5.6:compile
[INFO] org.springframework:spring-context:jar:2.5.6:compile
- 将在
pom
中声明的依赖定义为顶层依赖,顶层依赖的依赖位第二层,以此类推,会形成一个依赖树,通过依赖树,就可以了很清楚的看到依赖路径。
mvn dependency:tree
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ account-email ---
[INFO] com.pudding.mvnstudy.account:account-email:jar:1.0-SNAPSHOT
[INFO] +- junit:junit:jar:4.7:test
[INFO] +- org.springframework:spring-core:jar:2.5.6:compile
[INFO] | \- commons-logging:commons-logging:jar:1.1.1:compile
[INFO] +- org.springframework:spring-beans:jar:2.5.6:compile
[INFO] +- org.springframework:spring-context:jar:2.5.6:compile
[INFO] | \- aopalliance:aopalliance:jar:1.0:compile
[INFO] +- org.springframework:spring-context-support:jar:2.5.6:compile
[INFO] +- javax.mail:mail:jar:1.4.7:compile
[INFO] | \- javax.activation:activation:jar:1.1:compile
[INFO] \- com.icegreen:greenmail:jar:1.5.5:test
[INFO] +- com.sun.mail:javax.mail:jar:1.5.6:test
[INFO] \- org.slf4j:slf4j-api:jar:1.7.21:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
-
分析依赖:
mvn dependency:analyze
该结果包括两个部分:
Unused undeclared dependencies 指项目中使用到的,但没有显式声明的依赖,存在风险,应该显式声明任何项目中直接用到的依赖
Unused declared dependencies 指项目中显式声明了,而没有用到的依赖,对于这样的依赖应该仔细分析,而不是直接删除依赖