坐标:
坐标最熟悉的定义应该来源于平面几何,在任何一个平面坐标系中,任何坐标(x,y)都可以唯一标识该平面中的一个点。
在生活中,我们可以把地址看成一个坐标。国家、省、市、区、街道、等一系列信息同样可以唯一标识城市中任一地址。
邮局和快递公司就是基于这样一种坐标进行日常工作的。
Maven的世界中拥有数量非常巨大的构件,也就是平时常用的jar和war等文件。在Maven为这些构件引入坐标的概念之前,我们无法使用任何一种方式来唯一标识这些构件。
因此,需要spring的时候,就要去spring的官网去找,需要log4j的时候,又需要去apache官网去找。又因为每个网站的风格都不同,大量的时间都花费在了搜索和下载上。
没有统一的规范、统一的法则,该工作就无法自动化。这些重复的事情应该交给机器来做,但是机器工作必须有标准的协议,Maven制定了这样一组规则:世界上任何一个构件
都可以使用Maven坐标唯一标识、Maven坐标系的元素包括groupId、artifactId、version、package、classifier。我们只需要提供正确的坐标元素,Maven就能找到对应的构件。
比如说需要使用junit的4.7版本时,就告诉Maven:"groupId=junit,artifactId=junit,version=4.7",Maven就会自动的从仓库中寻找相应的构件供我们使用。
这些构件从哪来的呢?答案很简单,Maven内置了一个中央仓库的地址(http://repo1.maven.org/maven2),该中央仓库包含了世界上大部分流行的开源项目构件。
Maven需要时会去这里下载。
在我们开发自己项目的时候,也需要为其定义适当的坐标,这是Maven强制要求的。在和基础上,其他Maven项目才能引用该项目生成的构件。
先看一组坐标的定义:
<groupId>org.sonatype.nexus</groupId> <artifactId>nexus-indexer</artifactId> <version>2.0.0</version> <packaging>jar</packaging>
这是nexus-indexer的坐标定义,nexus-indexer是一个对Maven仓库编纂索引并提供搜索功能的类库,他是Nexus项目的一个子模块。
上面的代码中,其坐标分别为groupId:org.sonatype.nexus,artifactId:nexus-indexer,version:2.0.0,packaging:jar。
没有classifier。下面解释一下各个坐标元素:
groupId:定义当前Maven项目隶属的实际项目。首先,Maven项目和实际项目不一定是一对一的关系。比如springframework这是一个实际项目,其对应的Maven项目会有很多,比如spring-core、spring-context等。这是Maven中模块的概念,因此,一个实际项目往往会被划分成很多模块。其次,groupId不应该对应项目隶属的组织或公司。原因很简单,一个组织下面会有很多实际项目,如果groupId只定义到组织级别,后面的artifactId只能定义Maven项目(模块),那个实际项目这个层将很难定义。最后,groupId的表示方式和Java包名的表示方式类似,通常与域名反向一一对应。上例中,groupId为org.sonatype.nexus表示Sonatype公司建立了一个非盈利性组织,nexus表示Nexus这一实际项目,该groupId与域名nexus.sonatype.org对应。
artifactId:该元素定义实际项目中的一个Maven项目(模块),推荐的做法是使用实际项目名称作为artifactId的前缀。比如上例的artifactId是nexus-indexer,使用了实际项目名称nexus作为前缀,这样做的好处是方便寻找实际构件。在默认情况下,Maven生成的构件,其文件名会以artifactId作为开头,如nexus-indexer-2.0.0.jar,使用实际项目最为前缀之后, 就能方便从一个lib文件夹中找到某个项目的一组构件。如果有5个项目,每个项目都有一个core模块,如果没有前缀就会看到很多core-1.2.jar这样的文件,加上实际项目名称之后,就能很容易的区分apple-core.1.2.jar、banana-core.1.2.jar...
version:该元素定义了Maven项目当前的版本,如上例所示版本是2.0.0。需要注意的是,Maven定义了一套完成的版本规范,以及快照(SNAPSHOT)的概念。
packaging:该元素定义了Maven项目的打包方式。首先,打包方式通常与所生成的构件的文件扩展名对应,如上例的packaging为jar,最终文件名为nexus-ndexer-2.0.0.jar,而使用war打包方式的Maven项目,最终生成的构件会有一个.war的文件,不过这不是绝对的。其次,打包方式会影响到构建的生命周期,比如jar打包和war打包回使用不同的命令。最后,当不定义packaging的时候,Maven会默认使用jar值。打包时默认名称是[artifactId-version.packaging],也可以通过finalName指定打包名称。
classfier:该元素用来帮助定义构建输出的一些附属构件。附属构件与主构件对应,如上例中的是nexus-indexer-2.0.0.jar,该项目可能还会通过使用一些插件生成如nexus-indexer-2.0.0-javadoc.jar、nexus-indexer-2.0.0-source.jar这样一些附属构件的classfier。这样,附属构件也有了自己唯一的坐标。注意,不能直接定义项目的classfier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助完成。
依赖:
根元素project下的dependencies可以包含也或多个denpendency元素,用来声明一个或者多个依赖,每个依赖可以包含的元素有:
groupId、artifactId、version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到依赖。
type:依赖的类型,对于项目坐标定义的packaging。大部分情况下,该元素不必声明,默认为jar。
scope:依赖范围,compile、test、provided、runtime、system
optional:标记依赖是否可选 true|fals
exclusions:用来排除传递性依赖。(正在查资料。。。)
大部分依赖声明只包含基本坐标,然而在一些特殊的情况下,其他元素至关重要。
依赖范围:
在一般情况下,JUnit依赖的测试范围是test,测试范围用scope表示。首先需要知道,Maven在编译项目主代码的时候需要使用一套classpath(编译classpath、测试classpath、运行classpath)。依赖范围就是用来控制依赖于这三种classpath的关系。Maven有以下几种依赖范围:
compile:编译依赖范围。如果没有指定,就会默认使用这个依赖范围。使用此依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效。
test:测试依赖范围。使用此依赖范围的Maven依赖,只对于测试classpath有效,在编译主代码或者运行项目的使用时都将无法使用此类依赖。典型的例子就是JUnit,它只有在编辑测试代码的时候才需要。
provided:以提供依赖范围。使用此依赖范围的Maven依赖,对于编译和测试classpath有效,但在运行时无效。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复的引入一遍。
runtime:运行时依赖范围。使用此依赖范围的Maven依赖,对于测试和运行classpath有效,但在编译主代码时无效。典型的例子是JDBC驱动的实现,项目主代码的编译只需要JDK提供的接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
system:系统以来范围。该依赖与三种classpath的关系和provided完全一致。
传递性依赖:
考虑一个基于Spring Framework的项目,如果不使用Maven,那么在项目中就需要手动下载相关依赖。由于Spring Framework又会依赖其他开源类库,因此实际中往往会下载一个很大的如spring-framework-3.0.5-with-dependencies.zip的包,这里包含了Spring Framework所有的包,以及所有它依赖的其他jar包,这里不包含其他相关的依赖,到实际使用的时候,再根据出错信息,或者查询相关的文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事。
Maven的传递性依赖机制很好的解决了这个问题。以一个基于Spring的项目为例(hello),该项目有一个org.springframework:spring-core:3.0.5-RELEASE的依赖,依赖范围是compile,而实际上spring-core也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的POM:http://search.maven.org/remotecontent?filepath=org/springframework/spring-core/3.0.5.RELEASE/spring-core-3.0.5.RELEASE.pom。代码如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <packaging>jar</packaging> <version>3.0.5.RELEASE</version> <parent> <groupId>org.springframework</groupId> <artifactId>spring-parent</artifactId> <relativePath>../org.springframework.spring-parent</relativePath> <version>3.0.5.RELEASE</version> </parent> <dependencies> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2</version> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-asm</artifactId> <version>${project.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <scope>compile</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>xmlunit</groupId> <artifactId>xmlunit</artifactId> <version>1.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.codehaus.woodstox</groupId> <artifactId>wstx-asl</artifactId> <version>3.2.7</version> <scope>test</scope> </dependency> </dependencies> </project>
该文件包含了一个commons-logging依赖,该依赖没有声明依赖范围,那么其依赖范围就是默认的compile,hello有一个compile范围的spring-core依赖,spring-core有一个compile范围的commons-logging依赖,那么commons-logging就会成为hello的compile范围依赖,commos-logging是hello的传递性依赖。有了传递性依赖机制,在使用Spring Framework的时候就不用去考虑他依赖了什么,也不用担心引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,已传递性依赖的形式引入到当前项目中。
传递性依赖和依赖范围:
依赖范围不仅可以控制依赖与三种classpath的关系,还对传递性依赖产生影响。上例中,hello对于spring-core的依赖范围是compile,spring-core对于commons-logging的依赖范围是compile,那么hello对于commons-logging这一传递性依赖的范围就是compile,假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对C是第二直接依赖,A对C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围觉得了传递性依赖的范围,如图:
左边第一列表示第一直接依赖的范围,上边第一行表示第二直接依赖的范围,中间交叉处表示传递性依赖的范围,---表示没有依赖。
观察图片发现:当第二直接依赖的范围是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)会被解析使用。
依赖调解第一原则不能解决所有问题,比如:A→B→X(1.0),A→C→X(2.0),X(1.0)和X(2.0)的长度都是2,到底谁会被解析使用呢?在Maven2.0.8及之前的版本,这是不确定的,但从2.0.9之后,为了避免构建的不确定性,Maven定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的情况下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序靠前的优胜。