依赖机制介绍
依赖管理是Maven众所周知的特性之一,也是Maven擅长的领域之一.管理单个项目的依赖并不是太困难,但是当你开始处理由数十个甚至上百个模块组成的多模块项目或者应用时,Maven将会很好的帮助你保持项目的高可控性和稳定性.
传递依赖
传递依赖是Maven 2.0 的新特性.它让你不再需要发现并指定你所需要的依赖库并自动包含它们.
这个特性是通过从你指定的远程仓库阅读你所依赖的项目文件来简化的.通常来讲,那些项目的所有依赖被使用在你的项目中,它们可能继承自它们的父项目或者来自它们的依赖等等.
依赖可以聚集的层级数量没有限制,只有在发现循环依赖时才会引发问题.
由于依赖的传递,包含库的图标可以迅速膨胀到相当大.鉴于这个原因,这里有一些额外的特性将会限制依赖的包含:
依赖调解 - 它决定了使用什么版本的依赖当遭遇到一个工件的多个版本时.一般的,Maven 2.0 只支持"最近定义",这意味着它会采用依赖树中最近的依赖版本提供给你的项目.你也可以通过在你的POM中显示声明一个依赖来确保它的版本.注意,如果两个依赖版本在依赖树中的相同层次,直到 Maven 2.0.8 它都没有准确定义哪一个将胜出,但是自从 Maven 2.0.9 开始宣布第一个声明将胜出.("最近定义"意味着被采用的依赖版本将会是在依赖树中离你项目最近的那个,例如:依赖被定义成A->B->C->D 2.0 和A->E->D 1.0,那么当你构建A时,D1.0将会被采用,因为它的线路比较短.你可以在POM中显示声明D 2.0来强制指定版本).
依赖管理 - 它允许项目作者直接指定工件的使用版本当他们在传递依赖或者在那些没有指定版本的依赖中遭遇时.在前面部分的例子中有一个依赖直接添加给了A尽管A没有直接使用它.取而代之的是,A能直接包含D作为依赖在依赖管理部分并且直接控制D要使用的版本.
依赖范围 - 这允许你只为当前构建阶段包含适当的依赖.后面会详细描述.
排除依赖 - 如果X依赖Y,Y依赖Z,那么X的所有者可以通过"exclusion"元素来显式排除对Z的依赖.
可选依赖 - 如果Y依赖Z,Y的所有者可以通过"optional"元素来标记Z作为可选依赖.当X依赖Y时,X就只会依赖Y而不会依赖Z.X的所有者可以显式添加Z作为依赖.(把可选依赖想象成"默认的依赖排除"有助于理解).
依赖范围
依赖范围用来限制依赖的传递,也影响各种构建任务使用的类路径.
这里有6种范围:
compile
这是默认范围,当没有指定范围的时候默认使用它.这些依赖在项目的所有类路径下都是可用的,另外,它们会传播到依赖它们的项目中.
provided
这个compile很像,但是它表明你期望在运行时提供给依赖的JDK或者一个容器.例如,当构建一个J2EE的Web应用的时候,你将会设置Servlet API和相关的J2EE API依赖范围为provided因为Web容器提供这些类.这个范围只有在编译和测试类路径可用,而且它不会传递.
runtime
这个范围表明编译的时候不需要这些依赖,但是执行的时候需要.它们会在运行时和测试类路径下,但是不会在编译类路径下.
test
这个范围表明通常的使用情况下都不需要这些依赖,它们只针对测试编译和执行阶段有用.
system
这个范围和provided类似除了你不得不显式提供包含它的JAR.这个工件总是可用的而且它不会在仓库中进行查找
import(Maven 2.0.9+)
这个范围只能被用在<dependencyManagement>部分中类型为pom的依赖.它表明指定的POM应该被替换成在那个POM中 <dependencyManagement>部分里的依赖.因为它们用于替换,所以范围是import的依赖它们实际不参与限制依赖的传递.
上述每一种范围(除了import)都以不同的方式来影响依赖传递,下面的表格演示了这种影响.第一列代表一个依赖的设置范围,第一行表示那个依赖传递经过的依赖范围,交汇处表示结果范围.如果没有结果范围被列出,表示这个依赖将被忽略.
compile | provided | runtime | test | |
compile | compile(*) | - | runtime | - |
provided | provided | - | provided | - |
runtime | runtime | - | runtime | - |
test | test | - | test | - |
请注意:(*)意指这应该被runtime范围取代,志抑郁于所有compile依赖必须被显式的罗列出来.但是,存在这样的情况,你所依赖的库继承自另一个库的类,会强制你在编译时期就可用.由于这个原因,在编译时期这些依赖保持compile范围甚至当它们是可传递的时候.
依赖管理
依赖管理部分是一个集中化依赖信息的机制.当你有一堆项目都继承了一个通用的父项目,那么这就可以把所有依赖信息放进父项目的POM从而让子项目的POM变得简单.可以通过几个例子来阐明这个机制.给出两个有相同继承的POM:
项目A:
<project>
...
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
项目B:
<project>
...
<dependencies>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
这两个示例POM共享了一个通用的依赖而且它们各自都有一个重要的依赖.这些信息可以被放进父POM:
<project>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
然后两个子POM就更简单了:
<project>
...
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>bar</type>
</dependency>
</dependencies>
</project>
<project>
...
<dependencies>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>war</type>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>bar</type>
</dependency>
</dependencies>
</project>
注意:这些依赖中有两个,我们必须指定<type>元素.这是因为在<dependencyManagement>部分用于匹配一个依赖引用的最小信息集合实际上是{groupId,artifactId,type,classifier}.在很多情况下,这些依赖将引用没有classifier的jar工件,这就允许我们可以将标识集合简写成{groupId,artifactId},因为type的默认值就是jar,而classifier的默认值是null.
第二点,也是非常重要的关于依赖管理的用处是用来在传递依赖中控制工件的版本.作为一个示例,考虑这些项目:
项目A:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>A</artifactId>
<packaging>pom</packaging>
<name>A</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
项目B:
<project>
<parent>
<artifactId>A</artifactId>
<groupId>maven</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
当在项目B上运行Maven的时候,a,b,c和d的1.0版本将会被使用不论在它们的pom中指定的版本是什么:
a和c都作为项目的依赖被声明所以依赖调解使用版本1.0是可以预见的.而且它们也都具有runtime的范围因为被直接指定了.
b定义在项目B的父项目中的依赖管理部分而且因为依赖管理对传递依赖的优先级高于依赖调解,
所以版本1.0将被选择,b也将会有compile范围.
最后,因为d被指定在项目B的依赖管理部分,d应该是a或c的一个依赖,版本1.0将会被再次选择,因为依赖管理的优先级高于依赖调解而且也因为当前pom的声明优先级高于它的父pom的声明.
可用的依赖管理标签的参考信息请查阅:https://maven.apache.org/ref/3.3.9/maven-model/maven.html#class_DependencyManagement
导入依赖
这个特性只有在Maven 2.0.9或者之后的版本可用.这意味着poms声明范围import在早期的Maven版本中不能被解析.在你决定使用它之前请仔细考量这个信息,如果你决定使用它,我们建议你使用强制插件来要求至少2.0.9的Maven版本.
之前的例子描述了如何通过继承来指定托管的依赖.然而,在大型项目中这基本上是不能实现的因为一个项目只能继承自一个父项目.为了实现这一点,项目可以从其他项目导入托管的依赖.这通过描述一个带有范围"import"的pom工件来作为依赖来实现.
项目B:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>maven</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
假设项目A是之前例子中定义过的,最终结果将是一样的.所有A管理的依赖都将被合并到B中除了d,因为d在这个POM中已经定义了.
项目X:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>X</artifactId>
<packaging>pom</packaging>
<name>X</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
项目Y:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>Y</artifactId>
<packaging>pom</packaging>
<name>Y</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
项目Z:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>Z</artifactId>
<packaging>pom</packaging>
<name>Z</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>maven</groupId>
<artifactId>X</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>maven</groupId>
<artifactId>Y</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在上面的例子中,Z从X和Y导入了它们托管的依赖.然而,X和Y都声明了依赖a.在这里,a版本1.1将被使用,因为X是首先声明的,而且a没有在Z的依赖管理中声明.
这个过程是递归的.例如,如果X导入了另一个pom,Q,当Z在处理的时候,你会发现Q的所有依赖被定义在X中.
导入是很有效的当用来定义由一系列相关的工件构成的一个"库"时.一个项目使用来自这些"库"的一个或多个工件是相当常见的.然而,有时候很难保持在项目中使用的工件版本和分布在那些库里的版本的同步一致.下面的模式阐明了如何创建一个材料清单(BOM)来供其他项目使用.
项目的根是BOM pom.它定义了所有将会被创建在库里的工件的版本.其他项目想要使用这些库应该导入这个pom到它们的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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>bom</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<project1Version>1.0.0</project1Version>
<project2Version>1.0.0</project2Version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
<version>${project1Version}</version>
</dependency>
<dependency>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
<version>${project1Version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>parent</module>
</modules>
</project>
子项目用BOM作为它的父项目.
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<version>1.0.0</version>
<artifactId>bom</artifactId>
</parent>
<groupId>com.test</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>project1</module>
<module>project2</module>
</modules>
</project>
接下来是真正的项目POMs
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<version>1.0.0</version>
<artifactId>parent</artifactId>
</parent>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
<version>${project1Version}</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
</dependencies>
</project>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<version>1.0.0</version>
<artifactId>parent</artifactId>
</parent>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
<version>${project2Version}</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</dependency>
</dependencies>
</project>
下面的项目展示了现在"库"如何能够被另一个项目使用而不需要指定依赖的项目版本.
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>use</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
</dependency>
<dependency>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
</dependency>
</dependencies>
</project>
最后,在创建项目导入依赖时当心下面几点:
不要试图导入一个定义在当前pom子模块中的pom.视图这么做的话将会导致构建失败因为它没法定位pom.
永远不要声明pom导入一个pom作为它的父pom.没法处理循环并将导致抛出异常.
当引用了那些pom中有传递依赖的工件时,项目将需要为那些工件指定版本作为托管依赖.不这么做的话将会导致构建失败因为工件没有指定的版本.(在任何情况下这都应该被认为是一个最佳实践,因为它防止工件的版本从一个构建到下一个构建的变化)
系统依赖
范围指定为"system"的依赖总是可用的而且它们不会在仓库中查找.它们通常用来告诉Maven那些由JDK或者VM提供的依赖.因此,系统依赖对于解决那些现在由JDK提供,但是又可以提前独立下载的依赖是很有用的.典型的示例是JDBC的标准扩展和JAAS.
简单的示例:
<project>
...
<dependencies>
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
</dependencies>
...
</project>
如果你的工件由JDK的tools.jar提供,系统路径应该被定义成这样:
<project>
...
<dependencies>
<dependency>
<groupId>sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
...
</project>