Maven坐标和依赖的实现示例
Maven的一大功能是管理项目依赖。为了能自动化地解析任何一个Java构件,Maven就必须将它们唯一标识,这就依赖管理的底层基础——坐标。将详细分析Maven坐标的作用,解释其每一个元素;在此基础上,再介绍如何配置Maven,以及相关的经验和技巧,以帮助我们管理项目依赖。
1、何为Maven坐标
关于坐标(Coordinate),大家最熟悉的定义应该来自于平面几何。在一个平面坐标系中,坐标(x,y)表示该平面上与x轴距离为y,与y轴距离为x的一点,任何一个坐标都能够唯一标识该平面中的一点。
在实际生活中,我们也可以将地址看成是一种坐标。省、市、区、街道等一系列信息同样可以唯一标识城市中的任一居住地址和工作地址。邮局和快递公司正是基于这样一种坐标进行日常工作的。
对应于平面中的点和城市中的地址,Maven的世界中拥有数量非常巨大的构件,也就是平时用的一些jar、war等文件。在Maven为这些构件引入坐标概念之前,我们无法使用任何一种方式来唯一标识所有这些构件。因此,当需要用到Spring Framework依赖的时候,大家会去Spring Framework网站寻找,当需要用到log4j依赖的时候,大家又会去Apache网站寻找。又因为各个项目的网站风格迥异,大量的时间花费在了搜索、浏览网页等工作上面。没有统一的规范、统一的法则,该工作就无法自动化。重复地搜索、浏览网页和下载类似的jar文件,这本就应该交给机器来做。而机器工作必须基于预定义的规则,Maven定义了这样一组规则:世界上任何一个构件都可以使用Maven坐标唯一标识,Maven坐标的元素包括groupId、artifactId、version、packaging、classifier。现在,只要我们提供正确的坐标元素,Maven就能找到对应的构件。比如说,当需要使用Java5平台上TestNG的5.8版本时,就告诉Maven:“groupId=org.testng;artifactId=testng;version=5.8;classifier=jdk15”,Maven就会从仓库中寻找相应的构件供我们使用。也许你会奇怪,“Maven是从哪里下载构件的呢?”答案其实很简单,Maven内置了一个中央仓库的地址(http://repo1.maven.org/maven2),该中央仓库包含了世界上大部分流行的开源项目构件,Maven会在需要的时候去那里下载。
在我们开发自己项目的时候,也需要为其定义适当的坐标,这是Maven强制要求的。在这个基础上,其他Maven项目才能引用该项目生成的构件,见下图:
2、坐标详解
Maven坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组Maven坐标是通过一些元素定义的,它们是groupId、artifactId、version、packaging、classifier。先看一组坐标定义,如下:
<groupId>org.sonatype.nexus</groupId> <artifactId>nexus-indexer</artifactId> <version>2.0.0</version> <packaging>jar</packaging>
这是nexus-indexer的坐标定义,nexus-indexer是一个对Maven仓库编纂索引并提供搜索功能的类库,它是Nexus项目的一个子模块。后面会详细介绍Nexus。上述代码片段中,其坐标分别为groupId:org.sonatype.nexus、artifactId:nexus-indexer、version:2.0.0、packaging:jar,没有classifier。下面详细解释一下各个坐标元素:
这是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,org.sonatype表示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这样的文件,加上实际项目名前缀之后,便能很容易区分foo-core-1.2.jar、bar-core-1.2.jar……
- version:该元素定义Maven项目当前所处的版本,如上例中nexus-indexer的版本是2.0.0。需要注意的是,Maven定义了一套完整的版本规范,以及快照(SNAPSHOT)的概念。
- packaging:该元素定义Maven项目的打包方式。首先,打包方式通常与所生成构件的文件扩展名对应,如上例中packaging为jar,最终的文件名为nexus-indexer-2.0.0.jar,而使用war打包方式的Maven项目,最终生成的构件会有一个.war文件,不过这不是绝对的。其次,打包方式会影响到构建的生命周期,比如jar打包和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的典型例子是TestNG,TestNG的主构件是基于Java 1.4平台的,而它又提供了一个classifier为jdk5的附属构件。注意,不能直接定义项目的classifier,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。
上述5个元素中,groupId、artifactId、version是必须定义的,packaging是可选的(默认为jar),而classifier是不能直接定义的。
同时,项目构件的文件名是与坐标相对应的,一般的规则为artifactId-version[-classifier].packaging,[-classifier]表示可选。比如上例nexus-indexer的主构件为nexus-indexer-2.0.0.jar,附属构件有nexus-indexer-2.0.0-javadoc.jar。这里还要强调的一点是,packaging并非一定与构件扩展名对应,比如packaging为maven-plugin的构件扩展名为jar。此外,Maven仓库的布局也是基于Maven坐标。
理解清楚城市中地址的定义方式后,邮递员就能够开始工作了;同样地,理解清楚Maven坐标之后,我们就能开始讨论Maven的依赖管理了。
3、解析一个Maven项目的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>com.juvenxu.mvnbook.account</groupId> <artifactId>account-email</artifactId> <name>Account Email</name> <version>1.0.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> <scope>test</scope> </dependency> <dependency> <groupId>com.icegreen</groupId> <artifactId>greenmail</artifactId> <version>1.3.1b</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin> </plugins> </build> </project>
先观察该项目模块的坐标,groupId:com.juvenxu.mvnbook.account;artifactId:account-email;version:1.0.0-SNAPSHOT。由于该模块属于账户注册服务项目的一部分,因此,其groupId对应了account项目。紧接着,该模块的artifactId仍然以account作为前缀,以方便区分其他项目的构建。最后,1.0.0-SNAPSHOT表示该版本处于开发中,还不稳定。
再看dependencies元素,其包含了多个dependency子元素,这是POM中定义项目依赖的位置。以第一个依赖为例,其groupId:artifactId:version为org.springframework:spring-core:2.5.6,这便是依赖的坐标,任何一个Maven项目都需要定义自己的坐标,当这个Maven项目成为其他Maven项目的依赖的时候,这组坐标就体现了其价值。本例中的spring-core,以及后面的spring-beans、spring-context、spring-context-support是Spring Framework实现依赖注入等功能必要的构件,由于我们的关注点在于Maven,只会涉及简单的Spring Framework的使用,不会详细解释Spring Framework的用法,如果大家有不清楚的地方,请参阅Spring Framework相关的文档。
在spring-context-support之后,有一个依赖为javax.mail:mail:1.4.1,这是实现发送必须的类库。
紧接着的依赖为junit:junit:4.7,JUnit是Java社区事实上的单元测试标准,详细信息请参阅http://www.junit.org/,这个依赖特殊的地方在于一个值为test的scope子元素,scope用来定义依赖范围。这里我们暂时只需要了解当依赖范围是test的时候,该依赖只会被加入到测试代码的classpath中。也就是说,对于项目主代码,该依赖是没有任何作用的。JUnit是单元测试框架,只有在测试的时候才需要,因此使用该依赖范围。
随后的依赖是com.icegreen:greenmail:1.3.1b,其依赖范围同样为test。这时也许你已经猜到,该依赖同样只服务于测试目的,GreenMail是开源的邮件服务测试套件,account-email模块使用该套件来测试邮件的发送。关于GreenMail的详细信息可访问http://www.icegreen.com/greenmail/。
最后,POM中有一段关于maven-compiler-plugin的配置,其目的是开启Java 5的支持。
3.1、构建项目
使用mvn clean install构建account-email,Maven会根据POM配置自动下载所需要的依赖构件,执行编译、测试、打包等工作,最后将项目生成的构件account-email-1.0.0-SNAP-SHOT.jar安装到本地仓库中。这时,该模块就能供其他Maven项目使用了。
4、依赖的配置
依赖会有基本的groupId、arti-factId和version等元素组成。其实一个依赖声明可以包含如下的一些元素:
<project> …… <dependencies> <dependency> <groupId>……</groupId> <artifactId>……</artifactId> <version>……</version> <type>……</type> <scope>……</scope> <optional>……</optional> <exclusions> <exclusion>……</exclusion> …… </exclusions> </dependency> …… </dependencies> …… </project>
根元素project下的dependencies可以包含一个或者多个dependency元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
- groupId、artifactId和version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到需要的依赖。
- type:依赖的类型,对应于项目坐标定义的packaging。大部分情况下,该元素不必声明,其默认值为jar。
- scope:依赖的范围。
- optional:标记依赖是否可选。
- exclusions:用来排除传递性依赖。
大部分依赖声明只包含基本坐标,然而在一些特殊情况下,其他元素至关重要。
5、依赖范围
上一节提到,JUnit依赖的测试范围是test,测试范围用元素scope表示。本节将详细解释什么是测试范围,以及各种测试范围的效果和用途。
首先需要知道,Maven在编译项目主代码的时候需要使用一套classpath。在上例中,编译项目主代码的时候需要用到spring-core,该文件以依赖的方式被引入到classpath中。其次,Maven在编译和执行测试的时候会使用另外一套classpath。上例中的JUnit就是一个很好的例子,该文件也以依赖的方式引入到测试使用的classpath中,不同的是这里的依赖范围是test。最后,实际运行Maven项目的时候,又会使用一套classpath,上例中的spring-core需要在该classpath中,而JUnit则不需要。
依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系,Maven有以下几种依赖范围:
- compile:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效。典型的例子是spring-core,在编译、测试和运行的时候都需要使用该依赖。
- test:测试依赖范围。使用此依赖范围的Maven依赖,只对于测试classpath有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是JUnit,它只有在编译测试代码及运行测试的时候才需要。
- test:测试依赖范围。使用此依赖范围的Maven依赖,只对于测试classpath有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是JUnit,它只有在编译测试代码及运行测试的时候才需要。
- provided:已提供依赖范围。使用此依赖范围的Maven依赖,对于编译和测试class-path有效,但在运行时无效。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复地引入一遍。
- runtime:运行时依赖范围。使用此依赖范围的Maven依赖,对于测试和运行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(Maven 2.0.9及以上):导入依赖范围。该依赖范围不会对三种classpath产生实际的影响,后面将介绍Maven依赖和dependencyManagement的时候详细介绍此依赖范围。
上述除import以外的各种依赖范围与三种classpath的关系如下表所示:
6、传递性依赖
6.1、何为传递性依赖
考虑一个基于Spring Framework的项目,如果不使用Maven,那么在项目中就需要手动下载相关依赖。由于Spring Framework又会依赖于其他开源类库,因此实际中往往会下载一个很大的如spring-framework-2.5.6-with-dependencies.zip的包,这里包含了所有Spring Framework的jar包,以及所有它依赖的其他jar包。这么做往往就引入了很多不必要的依赖。另一种做法是只下载spring-framework-2.5.6.zip这样一个包,这里不包含其他相关依赖,到实际使用的时候,再根据出错信息,或者查询相关文档,加入需要的其他依赖。很显然,这也是一件非常麻烦的事情。
Maven的传递性依赖机制可以很好地解决这一问题。以account-email项目为例,该项目有一个org.springframework:spring-core:2.5.6的依赖,而实际上spring-core也有它自己的依赖,我们可以直接访问位于中央仓库的该构件的POM:http://repo1.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom。该文件包含了一个commons-logging依赖,代码清单如下所示:
<dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency>
该依赖没有声明依赖范围,那么其依赖范围就是默认的compile。同时回顾一下account-email,spring-core的依赖范围也是compile。
account-mail有一个compile范围的spring-core依赖,spring-core有一个compile范围的commons-logging依赖,那么commons-logging就会成为account-email的compile范围依赖,commons-logging是account-email的一个传递性依赖,如下图所示:
有了传递性依赖机制,在使用Spring Framework的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。
6.2、传递性依赖和依赖范围
依赖范围不仅可以控制依赖与三种classpath的关系,还对传递性依赖产生影响。上面的例子中,account-email对于spring-core的依赖范围是compile,spring-core对于commons-logging的依赖范围是compile,那么account-email对于commons-logging这一传递性依赖的范围也就是compile。假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围,如下表所示,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围:
为了能够帮助读者更好地理解上表,这里再举个例子。account-email项目有一个com.icegreen:greenmail:1.3.1b的直接依赖,我们说这是第一直接依赖,其依赖范围是test;而greenmail又有一个javax.mail:mail:1.4的直接依赖,我们说这是第二直接依赖,其依赖范围是compile。显然javax.mail:mail:1.4是account-email的传递性依赖,对照上表可以知道,当第一直接依赖范围为test,第二直接依赖范围是compile的时候,传递性依赖的范围是test,因此javax.mail:mail:1.4是account-email的一个范围是test的传递性依赖。
仔细观察一下上表,可以发现这样的规律:当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致;当第二直接依赖的范围是test的时候,依赖不会得以传递;当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,且传递性依赖的范围同样为provided;当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile例外,此时传递性依赖的范围为runtime。
7、依赖调解
Maven引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。
Maven引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。
依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A->B->Y(1.0)、A->C->Y(2.0),Y(1.0)和Y(2.0)的依赖路径长度是一样的,都为2。那么到底谁会被解析使用呢?在Maven 2.0.8及之前的版本中,这是不确定的,但是从Maven 2.0.9开始,为了尽可能避免构建的不确定性,Maven定义了依赖调解的第二原则:第一声明者优先。在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。该例中,如果B的依赖声明在C之前,那么Y(1.0)就会被解析使用。
8、可选依赖
假设有这样一个依赖关系,项目A依赖于项目B,项目B依赖于项目X和Y,B对于X和Y的依赖都是可选依赖:A->B、B->X(可选)、B->Y(可选)。根据传递性依赖的定义,如果所有这三个依赖的范围都是compile,那么X、Y就是A的compile范围传递性依赖。然而,由于这里X、Y是可选依赖,依赖将不会得以传递。换句话说,X、Y将不会对A有任何影响,如下图所示:
为什么要使用可选依赖这一特性呢?可能项目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>
上述XML代码片段中,使用<optional>元素表示mysql-connector-java和postgresql这两个依赖为可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目A依赖于项目B的时候,如果其实际使用基于MySQL数据库,那么在项目A中就需要显式地声明mysql-connector-java这一依赖,见代码如下所示:
<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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.10</version> </dependency> </dependencies> </project>
最后,关于可选依赖需要说明的一点是,在理想的情况下,是不应该使用可选依赖的。前面我们可以看到,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。这个原则在规划Maven项目的时候也同样适用。在上面的例子中,更好的做法是为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驱动依赖。
9、最佳实践
Maven依赖涉及的知识点比较多,在理解了主要的功能和原理之后,最需要的当然就是前人的经验总结了,我们称之为最佳实践。本小节归纳了一些使用Maven依赖常见的技巧,方便用来避免和处理很多常见的问题。
9.1、排除依赖
传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的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>
上述代码中,项目A依赖于项目B,但是由于一些原因,不想引入传递性依赖C,而是自己显式地声明对于项目C 1.1.0版本的依赖。代码中使用exclusions元素声明排除依赖,exclusions可以包含一个或者多个exclusion子元素,因此可以排除一个或者多个传递性依赖。需要注意的是,声明exclusion的时候只需要groupId和artifactId,而不需要version元素,这是因为只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。换句话说,Maven解析后的依赖中,不可能出现groupId和artifactId相同,但是version不同的两个依赖。该例的依赖解析逻辑如下图所示:
9.2、归类依赖
,有很多关于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-context-support:2.5.6,它们是来自同一项目的不同模块。因此,所有这些依赖的版本都是相同的,而且可以预见,如果将来需要升级Spring Frame-work,这些依赖的版本会一起升级。
对于account-email中这些Spring Framework来说,也应该在一个唯一的地方定义版本,并且在dependency声明中引用这一版本。这样,在升级Spring Framework的时候就只需要修改一处,实现方式见代码清单如下:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.juven.mvnbook.account</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-context-support</artifactId> <version>${springframework.version}</version> </dependency> </dependencies> </project>
这里简单用到了Maven属性,首先使用properties元素定义Maven属性,该例中定义了一个springframework.version子元素,其值为2.5.6。有了这个属性定义之后,Maven运行的时候会将POM中的所有的${springframework.version}替换成实际值2.5.6。也就是说,可以使用美元符号和大括弧环绕的方式引用Maven属性。然后,将所有Spring Framework依赖的版本值用这一属性引用表示。
9.3、优化依赖
在软件开发过程中,程序员会通过重构等方式不断地优化自己的代码,使其变得更简洁、更灵活。同理,程序员也应该能够对Maven项目的依赖了然于胸,并对其进行优化,如去除多余的依赖,显式地声明某些必要的依赖。
通过阅读前面的内容,我们应该能够了解到:Maven会自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后,最后得到的那些依赖被称为已解析依赖(Resolved Dependency)。可以运行如下的命令查看当前项目的已解析依赖:
mvn dependency:list
上图显示了所有shardingsphere-5.3.0的已解析依赖,同时,每个依赖的范围也得以明确标示。
在此基础上,还能进一步了解已解析依赖的信息。将直接在当前项目POM声明的依赖定义为顶层依赖,而这些顶层依赖的依赖则定义为第二层依赖,以此类推,有第三、第四层依赖。当这些依赖经Maven解析后,就会构成一个依赖树,通过这棵依赖树就能很清楚地看到某个依赖是通过哪条传递路径引入的。可以运行如下命令查看当前项目的依赖树:
mvn dependency:tree
从上图中能够看到,虽然我们没有声明org.slf4j:slf4japi:1.3这一依赖,但它还是经过com.icegreen:greenmail:1.3成为了当前项目的传递性依赖,而且其范围是test。
使用dependency:list和dependency:tree可以帮助我们详细了解项目中所有依赖的具体信息,在此基础上,还有dependency:analyze工具可以帮助分析当前项目的依赖。
为了说明该工具的用途,先将spring-context这一依赖删除,然后构建项目,你会发现编译、测试和打包都不会有任何问题。通过分析依赖树,可以看到spring-context是spring-context-support的依赖,因此会得以传递到项目的classspath中。现在再运行如下命令:
mvn dependency:analyze
该结果中重要的是两个部分。首先是Used undeclared dependencies,意指项目中使用到的,但是没有显式声明的依赖,这里是spring-context。这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的Java import声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译。这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相。因此,显式声明任何项目中直接用到的依赖。
结果中还有一个重要的部分是Unused declared dependencies,意指项目中未使用的,但显式声明的依赖,这里有spring-core和spring-beans。需要注意的是,对于这样一类依赖,我们不应该简单地直接删除其声明,而是应该仔细分析。由于dependency:analyze只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。很显然,该例中的spring-core和spring-beans是运行Spring Framework项目必要的类库,因此不应该删除依赖声明。当然,有时候确实能通过该信息找到一些没用的依赖,但一定要小心测试。
到此这篇关于Maven坐标和依赖的实现示例的文章就介绍到这了,更多相关Maven坐标和依赖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
SpringBoot(JAVA)整合微信公众号消息推送完整步骤(文本、图片/视频推送)
微信公众号消息推送包括文本推送和图文/视频推送两类,文本推送通过模板消息或自定义消息实现,而图文/视频推送需先上传素材至临时/永久素材库,再上传图文消息,最后进行消息推送,文中将实现的方法介绍的非常详细,需要的朋友可以参考下2024-09-09
最新评论