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仅仅会分析编译主代码和測试代码须要用到的依赖。一些执行測试盒执行时须要的依赖它发现不了。所以我们应该认真分析该依赖是否会被用到再决定对该依赖的取舍。
版权声明:本文博客原创文章。博客,未经同意,不得转载。