07.maven依赖管理

Maven的依赖管理十分强大,单个项目的依赖管理非常简单,但是,当应用由多个模块组成,并且应用有数十到数百个模块组成时,依赖管理变得非常困难。此时,maven可以保证高度的依赖控制和稳定

maven的项目依赖主要配置在项目pom.xml的dependencies节点中。依赖是使用Maven坐标来定位的,因此,使用任何一个依赖之间,你都需要知道它的Maven坐标。

maven仓库搜索服务的站点 上,每个组件都有maven依赖的配置内容,只需要将内容复制到项目pom.xml中的dependencies节点即可。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

完整的依赖节点包含以下节点:

  • groupId,必选,实际隶属项目
  • artifactId,必选,其中的模块
  • version必选,版本号
  • type可选,依赖类型,默认jar
  • scope可选,依赖作用域,默认compile
  • optional可选,标记依赖是否可选,默认false
  • exclusion可选,排除传递依赖性,默认空

上面的示例是编写文档时,junit的maven依赖。

依赖传递

依赖传递是maven2.0的新特征。用于避免手动发现和添加需要的组件的依赖组件,maven会自动添加。

这个新特殊通过从远程仓库读取项目依赖的工程文件实现,一般而言,组件的传递性依赖包括继承自父项目的依赖组件,以及组件依赖的组件,等等。

因为没有限制依赖关系的层次。所以会产生一个问题:循环依赖(后面会有解决办法)

因为依赖传递,包含库可能会快速增长,出于这个原因,一些特征将用于限制依赖关系。

  • 依赖调解

当遇到一个组件的多个版本依赖时,通过依赖调解决定使用哪个版本的组件。 maven2.0只支持“就近原则”,即选择离依赖树中最近一个版本。当然,你也可以选择显示的在项目pom文件中指定一个版本。 在maven2.0.9版本及之后的版本中,如果两个版本的组件在项目依赖树的在同一深度,那么哪个依赖先声明,用哪个版本。

举例说明:

在项目A中,依赖B与C组件,B组件依赖D1.0与E1.0,而组件C依赖于F与E2.0,F依赖与D2.0。此时,产生了依赖冲突,即对组件D和组件E的依赖。此时,因为组件D1.0更靠近项目,所以使用组件D1.0,对于E组件,因为距离项目的距离相同,所以POM中组件B与C在谁先声明,就用谁的依赖版本。

如果在POM中显示的声明D与E的版本,则使用指定版本。

  • 依赖管理

即允许开发者在遇到传递性依赖,直接指定组件版本。

  • 依赖作用域

见下节

  • 依赖排除

假设X组件依赖于Y组件,Y组件依赖于Z组件,在引用X时,可以使用exclusion节点,排队项目对Z组件的引用。 这常常用于解除依赖调解的“就近原则” ,使用离项目较远的版本。

  • 可选依赖

假设组件Y依赖于组件Z,在组件Y中可以通过optional节点将Z定义为可选依赖。X引用Y组件依赖时,Z组件将不参与依赖传递。X组件可以选择性的显示引用Z组件。

maven依赖作用域

scope节点表示依赖作用域。

依赖作用域是用来限制依赖关系的传递性,同时影响各类组件的作用域。

maven依赖有以下几种:

  • compile

此作用域这是默认的作用域,如果依赖没的指定作用域,则使用此作用域。此依赖作用域的依赖可用于项目的所有类路径。此外,此依赖作用域参与依赖传递。

  • provided

此作用域和compile很像,只是在运行时此依赖由JDK或者容器提供。举个例子,当构建一个J2EE web应用时,你就要将Servlet API和Java EE API的依赖作用域设置为provided,因为容器提供了这些类。此依赖作用域只作用于编译和测试的类路径。不参与依赖传递

  • runtime

此作用域表示依赖在编译时不需要,但是执行时需要,作用于运行和测试时,但是不在编译的类路径中。

  • test

此作用域表示依赖在项目一般使用中不需要,只在测试编译和测试执行期间使用。

  • system

此作用域与provided作用域相似,只是你必须提供显示包含的jar。组件必须可用,maven不会在repository里查找组件。

  • import(只在Maven 2.0.9及以后的版本中可用)

此作用域只用于pom依赖,

这说明指定的pom会被POM的节点中的dependencies替换。因为会被替换,所以带有import的依赖不参与实际的依赖传递。 Since they are replaced, dependencies with a scope of import do not actually participate in limiting the transitivity of a dependency.

每个不同的作用域以不同的方式影响依赖传递。如下表所示: 如果一个项目依赖设置为左列的作用域,本依赖的传递依赖的作用域为第一行的值,那么传递依赖作用于本项目的的作用域将取交叉单元格的值,未列出的作用域,将被忽略

compileprovidedruntimetest
compilecompile(*)-runtime-
providedprovided-provided-
runtimeruntime-runtime-
testtest-test-

system作用域

system作用域的依赖组件应该总是可用,maven用不会去仓库中查找,system作用域通常用于告诉 maven 由JDK与虚拟机提供的依赖。如果你将一个依赖范围设置成系统范围,你必须同时提供一个 systemPath 元素。注意该范围是不推荐使用的

经典的使用例子就是JDBC标准扩展,Java身份验证和授权服务(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>

可选依赖

使用可选依赖的场景:一个项目A依赖于B,但是引用A的项目可能并不需要B,此时,A项目对B项目的依赖就可以配置成可选依赖,如果引用A的项目需要使用B的功能,可以手动引用B的依赖。

为什么使用可选依赖?

  1. 节约磁盘、内存等空间;
  2. 避免license许可问题;
  3. 避免类路径问题,等等。
<project>
  ...
  <dependencies>
    <!-- declare the dependency to be set as optional -->
    <dependency>
      <groupId>sample.ProjectA</groupId>
      <artifactId>Project-A</artifactId>
      <version>1.0</version>
      <scope>compile</scope>
      <optional>true</optional> <!-- value will be true or false only -->
    </dependency>
  </dependencies>
</project>

配置可靠依赖只需要在dependency节点中添加true即可。

依赖排除

依赖排除的使用场景:当一个项目A依赖项目B,而项目B同时依赖项目C,如果项目A中因为各种原因不想引用项目C,在配置项目B的依赖时,可以排除对C的依赖。

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>sample.ProjectA</groupId>
      <artifactId>Project-A</artifactId>
      <version>1.0</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>  <!-- declare the exclusion here -->
          <groupId>sample.ProjectB</groupId>
          <artifactId>Project-B</artifactId>
        </exclusion>
      </exclusions> 
    </dependency>
  </dependencies>
</project>

配置依赖排除时,只需要在节点dependency添加exclusions节点,在exclusions节点中添加一个或者多个exclusion即可。exclusion节点包含groupIdartifactId节点,用于定义排队的组件。

多重依赖

Demo:

对于如下依赖结构:

Project-A
   -> Project-B
        -> Project-D 
              -> Project-E
              -> Project-F
   -> Project C

A对于E相当于有多重依赖,如果想在项目A中排除对E的依赖,只需要在配置B的依赖中进行即可:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>sample.ProjectA</groupId>
  <artifactId>Project-A</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  ...
  <dependencies>
    <dependency>
      <groupId>sample.ProjectB</groupId>
      <artifactId>Project-B</artifactId>
      <version>1.0-SNAPSHOT</version>
      <exclusions>
        <exclusion>
          <groupId>sample.ProjectE</groupId> <!-- Exclude Project-E from Project-B -->
          <artifactId>Project-E</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
</project>

maven处理循环依赖

循环依赖

如果工程A依赖工程B,工程B又依赖工程A,就会形成循环依赖。或者A依赖B,B依赖C,C依赖A,也是循环依赖,总的来说,在画出工程依赖图之后,如果发现工程间的依赖连线形成了一个有向循环图,则说明有循环依赖的现象。

解决循环依赖

第一个办法是用build-helper-maven-plugin插件来规避。比如A依赖B,B依赖C,C依赖A的情况。这个插件提供了一种规避措施,即临时地将工程A、B、C合并成一个中间工程,编译出临时的模块D。然后A、B、C再分别依赖临时模块D进行编译

第二个办法是通过重构

重构的思路

第一个思路是平移重构,例如项目A和B互相依赖,那么可以将B依赖A的那部分平移到工程B中,或者将A依赖B的部分平移到A项目中,这样就能消除循环依赖。    第二个思路应对如下情况,比如A和B互相依赖,同时它们都依赖C,那么可以将B和A相互依赖的那部分移动到工程C里,这样一来,A和B相互之间都不依赖,只继续依赖C,也可以消除循环依赖。