Maven坐标详解:
Maven定义了这样一组规则:世界上任何一个构件都可以使用Maven坐标唯一标识,Maven坐标的元素包括groupId、artifactId、version、packaging、classifier。我们只需要提供正确的坐标元素,Maven就能找到对应的构件。比如当需要使用Java5平台上的TestNG的5.8版本时,就告诉Maven:”groupId=org.testng;
artifactId=testng; version=5.8; classifer=jdk15,maven就会从中央仓库(http://search.maven.org/#browse)中寻找相应的构件供我们使用。
先看一组坐标定义,如下:
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
nexus-indexer是一个对Maven仓库编纂索引并提供搜索功能的类库,它是Nexus项目中的一个子模块。后面会详细介绍Nexus。下面详细解释一下各个坐标元素:
groupId:定义当前Maven项目隶属的实际项目。首先,Maven项目和实际项目不一定是一对一的关系,比如SpringFramework这一实际项目,其对应的Maven项目会有很多,如spring-core、spring-context等。一个实际项目通常会划分成多个项目模块。groupId不应该只对应于项目隶属的组织或公司,原因是一个组织下会有很多实际项目,如果groupId只定义到组织级别,后面可以看到,artifactId只能对应Maven项目,那么实际项目这个层将难以定义。最后,groupId的表示方式与java包名的表示方式类似,通常与域名反向一一对应。
artifactId:该元素定义了实际项目中的一个Maven项目(模块),推荐的做法是使用实际项目名称作为artifactId的前缀。比如上例的artifactId是nexus-indexer,使用了实际项目名nexus作为前缀,这样做的好处是方便寻找实际构件。
version:该元素定义了Maven项目当前所处的版本。实际上,Maven定义了一套完整的版本规范,以及快照(SNAPSHOT)的概念。在后面的章节将详细讨论。
packaging:该元素定义Maven项目的打包方式。首先,打包方式通常与所生成构件的文件扩展名对应,如上例中packaging为jar,最终的文件名为nexus-indexer-2.0.0.jar。而是用war打包方式的Maven项目,最终生成的构件会有一个.war文件,但这不是绝对的。当不定义packaging时,Maven会是用默认值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,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。
项目构件的文件名是与坐标相对应的,一般的规则是artifactId-version[-classifier].packaging,[-classifier]表示可选。这里还要强调一点,packaging并非一定与构件扩展名对应,比如packaging为maven-plugin的构件扩展名为jar。
依赖的配置:
一个依赖声明可以包含如下的一些元素:
<project>
...
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
...
</exclusion>
...
</exlusions>
</dependency>
...
</dependencies>
...
</project>
根元素project下的dependencies可以包含一个或者多个denpendency元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
- groupId、artifactId和version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到需要的依赖。
- type:依赖的类型,对应于项目坐标定义的packaging。大部分情况下,该元素不必声明,其默认值为jar。
- scope:依赖的范围,请见后面小节
- optional:标记依赖是否可选,请见后面小节
- exclusions:用来排除传递性依赖,请见后面小节
Maven依赖范围:
在Maven中,依赖范围用元素scope表示。Maven在执行编译、测试、运行时执行的是三套不同的classpath。
依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系,Maven有以下几种依赖范围:
compile:编译依赖范围。如果没有指定,就会默认使用该依赖范围。该此依赖范围对于编译、测试、运行三种classpath都有效。典型的例子是spring-core,在编译、测试和运行的时候都需要使用该依赖。
test:测试依赖范围。该依赖范围只对于测试classpath有效,在编译主代码或者运行项目的时将无法使用此类依赖。典型的例子就是JUnit,它只有在编译测试代码及运行测试环境的时候才需要。
provided:已提供依赖范围。该依赖范围对于测试和运行class-path有效,但在运行时无效。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复引入一遍。
runtime:运行时依赖范围。该范围依赖,对于运行和测试class-path有效,但在编译主代码时无效。典型的例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体的JDBC驱动。
system:系统依赖范围。该依赖与三种classpath的关系,和provided依赖范围完全一致。但是,使用system范围的依赖时必须通过systemPath元素显式地指定依赖文件的路径。由于此类依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath元素可以引用环境变量,如:
<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: 导入依赖范围。该依赖范围不会对三种classPath产生实际的影响,我们将在后面的章节详细介绍该依赖。
传递性依赖:
何为传递性依赖:
传递性性依赖的意思是项目A依赖了B构件,而在B构件的pom.xml中又显式的依赖了C构件,那么A项目也就会依赖到C构件。在不使用Maven的项目当中,我们通常需要手动的去寻找所有直接使用和间接使用的构件(传递性依赖),以及解决版本冲突的问题,这将耗费很大的精力且意义不大。
Maven的传递性依赖机制可以很好的解决这一问题。在A项目下有一个org.springframework:spring-core:2.5.6的依赖,而实际上spring-core也有它自己的依赖,例如spring-core-2.5.6.pom该文件包含了一个commos-logging依赖,见下面代码:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifact>
<version>1.1.1</version>
</denpendency>
commons-logging没有声明依赖范围,那么其依赖范围就是默认的compile,而spring-core一般的依赖范围也是compile。
A项目有一个compile范围的spring-core依赖,spring-core有一个compile范围的commons-logging依赖,那么commons-logging就会成为A项目的compile范围依赖,commons-logging是account-email的一个传递性依赖。
有了传递性依赖机制,在使用Spring Framework的时候就不用去考虑它依赖了什么,也不用担心引入多于的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目之中。
传递性依赖和依赖范围:
假设A依赖与B,B依赖与C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。如下表所示,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围。
|
compile |
test |
provided |
runtime |
compile |
compile |
- |
- |
runtime |
test |
test |
- |
- |
test |
provided |
provided |
- |
- |
provided |
runtime |
runtime |
- |
- |
runtime |
仔细观察该表,可以发现如下的规律:当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖的范围是test的时候,依赖不会得以传递;当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,且传递性依赖的范围同样为provided;当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile例外,此时传递性依赖的范围为runtime。
依赖调解:
Maven的传递性依赖机制,一方面大大简化和方便了依赖声明。但有时候造成问题时,我们需要知道该传递性依赖是从哪条依赖路径引入的。
例如,项目A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被Maven解析使用呢?两个版本都被解析是不行的,因为会造成重复依赖。Maven依赖的第一原则是:路径最近者优先。该例中X(1.0)的路径长度为3,而X(2.0)的路径长度为2,因此X(2.0)会被解析使用。
Maven定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。
可选依赖:
假设有以下状态的依赖:A->B、B->X(可选)、B->Y(可选)。根据传递性依赖的定义,如果所有这三个依赖的范围都是compile,那么X、Y就是A的compile范围传递性依赖。然而,由于这里X、Y是可选依赖,依赖将不会得以传递。
使用可选依赖的原因可能是B实现了两个特性,其中的特性一依赖于X,特性二依赖于Y,而且这两个特性是互斥的,用户不可能同时使用这两个特性。比如B是一个持久层隔离工具包,它支持多种数据库,包括MySQL,PostgreSQL等,在构建工具包的时候,需要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一种数据库。
项目B的依赖声明见如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.10</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4-701.jdbc3</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>
在pom.xml中,使用<optional>元素表示mysql-connector-java和postgresql这两个依赖为可选依赖,它们只对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目A依赖于项目B的时候,如果其实际使用基于MySql数据库,那么在A项目中就想要显示地声明mysql-connector-java这一依赖,见下面A项目pom.xml。
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-a</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4-701.jdbc3</version>
</dependency>
</dependencies>
</project>
在理想情况中,是不应该使用可选依赖的。使用可选依赖的背景是一个项目实现了多个特性,在面向对象的设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。在上面的例子中,更好的做法是为MySql和PostgreSQL分别创建一个Maven项目,基于同样的groupId分配不同的artifactId,如com.juvenxu.mvnbook;project-b-mysql和com.juvenxu.mvnbook:project-b-postgresql,在各自的POM中声明对应的JDBC驱动依赖,而且不适用可选依赖,用户则根据需要选择使用project-b-mysql或者project-b-postgresql。由于传递性依赖的作用,就不再声明JDBC驱动依赖。
最佳实践:
排除依赖:
传递性依赖虽然简化了项目依赖的管理,但有时也会带来一些问题,需要我们排除一些传递性依赖。例如:当前项目有一个第三方依赖,而这个第三方依赖依赖了另一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,而SNAPSHOT的不稳定性会直接影响到当前的项目。这时候就需要排除掉该SNAPSHOT,并且在当前的项目中声明该类库的某个正式发布的版本。
还有一些情况,你也可能需要排除依赖,比如SUN JTA API,Hibernate依赖于这个JAR,但是由于版本的因素,该类库不在中央仓库中,而Apache Geronimo项目中有一个相应的实现。这时你就可以排除Sun JAT API,再声明Geronimo的JTA API实现。
排除依赖范例代码如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-a</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-c</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-c</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</project>
代码中使用exclusions元素声明排除依赖,exclusions可以包含一个或者多个exclusion子元素,因此可以排除一个或者多个传递性依赖。
归类依赖:
很多时候,我们会使用到来自同一项目下的不同模块,而且这些依赖的版本都是相同的。例如我们在使用spring framework时,分别引入的依赖为org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6、org.springframework:spring-support:2.5.6。如果将来需要升级Spring
Frame-work,这些依赖的版本会一起升级。
在Maven中可以使用归类依赖,这样可以避免重复,而且在修改值的时候,能够降低错误发生的几率。例子如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>account-email</artifactId>
<name>Account Email</name>
<version>1.0.0-SNAPSHOT</version>
<properties>
<springframework.version>2.5.6</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-support</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</project>
这里用到了Maven属性(后面会详细介绍Maven属性),Maven运行的时候会将POM中的所有的${springframework.version}替换成实际值2.5.6。也就是说,可以使用美元符号和大括弧环绕的方式引用Maven属性。然后,将所有Sping Framework依赖的版本值用这一属性引用表示。
优化依赖:
Maven会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为已解析依赖。可以运行如下的命令查看当前项目的已解析依赖:
mvn dependency:list
在此基础上,还能进一步了解已解析依赖的信息。将直接在当前项目POM声明的依赖定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,...当这些依赖经Maven解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条路径引入的。可以运行mvn dependency:tree来查看当前项目的依赖树。
我们还可以使用mvn dependency:analyze来分析项目中的依赖。使用该工具可以得出两类内容:
Used undeclared dependencies
意指项目中使用到的,但是没有显式声明的依赖。这种依赖意为着潜在的风险,当前项目直接在使用它们,例如有很多相关Java import声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本可能发生改变,接口就可能发生改变,那么就会导致当前项目中的相关代码无法编译。因此,一般应该显式声明任何项目中直接用到的依赖。
Unused declared dependencies
意指项目中未使用的,但是显式声明的依赖。对于这一类依赖,我们应该认真的分析,由于mvn dependency:analyze只会分析编译主代码和测试代码需要用到的依赖,一些执行测试盒运行时需要的依赖它发现不了。所以我们应该认真分析该依赖是否会被用到再决定对该依赖的取舍。