8.3. 优化依赖

如果你仔细看一下Chapter 7, 多模块企业级项目中创建的不同POM,就会注意到几种重复模式。我们能看到的第一种模式是:一些依赖如springhibernate-annotations在多个模块中被声明。每个hibernate依赖都重复排除了javax.transaction。第二种重复模式是:有一些依赖是关联的,共享同样的版本。这种情况通常是因为某个项目的发布版本中包含了多个紧密关联的组件。例如,看一下依赖hibernate-annotationshibernate-commons-annotations,两者的版本都是3.3.0.ga,而且我们可以预料这两个依赖的版本只会一起向前改变。hibernate-annotationshibernate-commons-annotations都是JBoss发布的同一个项目的组件,当有新的版本发布的时候,两个依赖都会改变。最后的重复模式是:兄弟模块依赖和兄弟模块版本的重复。Maven提供的简单机制能让你将所有这些依赖重构到一个父POM

就像你项目的源码一样,任何时候你在POM中有重复,你就开启了通往麻烦之路的大门。重复依赖声明使得很难保证一个大项目中的版本一致性。当你只有两个或者三个模块的时候,可能这不是一个大问题,但当你的组织正使用一个大型的,多模块Maven构建来管理分布于很多部门的数百个组件,一个依赖间的版本不匹配能够造成混乱。项目中一个对于名为ASM的字节码操作包的依赖版本不一致,即使处于项目层次的三层以下,如果该模块被依赖,还是可以影响到由另一个完全不同的开发组维护的web应用。单元测试会通过因为它们是基于一个版本的依赖运行的,但产品可能会灾难性的失败,原因是包(比如这里是war)里存在有不同版本的类库。如果你拥有数十个项目使用比如Hibernate这样的东西,每个项目重复那些依赖和排除配置,那么有人搞坏构建的平均发生时间就会很短。由于你的Maven项目变得很复杂,依赖列表也会增大,你需要在父POM中巩固版本和依赖声明。

兄弟模块版本的重复可以造成一个特殊的令人讨厌的问题,该问题不是直接由Maven造成的,只有在你多次遇到这个问题之后你才会有认识。如果你使用Maven Release插件来运行你的发布,所有这些兄弟依赖版本都会被自动更新,因此维护它们就不是什么问题。如果simple-web版本1.3-SNAPSHOT依赖于simple-persist版本1.3-SNAPSHOT,并且你正执行一次对于两个项目的版本1.3发布,Maven Release插件很聪明,能够自动更改整个多模块项目中的所有POM。使用Realse插件来运行发布能够自动将你构建的所有版本增加到1.4-SNAPSHOT,并且release插件会将代码变更提交至代码库。发布一个大型的多模块项目会变得更简单,直到……

当开发人员将更改合并到POM中并影响了一个正进行的版本发布的时候,问题就产生了。通常一个开发人员合并更改并偶然的错误处理了对于兄弟依赖的冲突,不注意的回退到了前一个发布的版本。由于同一个依赖的连续版本通常是相互兼容的,当开发人员构建的时候,问题不会出现,甚至暂时在持续构建系统也不会发现。想像一下一个十分复杂的构建,主干上都是1.4-SNAPSHOT的组件,现在有一个开发人员A,将项目层次深处的组件A更新至依赖于组件B的1.3-SNAPSHOT版本。虽然大部分开发者都使用1.4-SNAPSHOT了,如果组件B的1.3-SNAPSHOT1.4-SNAPSHOT相互兼容的话,该构建还是会成功。Maven会继续使用从开发者本地仓库获取的组件B的1.3-SNAPSHOT版本构建该项目。所有事情开起来都很流畅,项目构建,持续集成构建都没问题,有人可能有一个关于组件B的诡异的bug,我们也暂时将其认为是一个小问题记下来,然后继续下面的事情。

有个人,让我们称其为马虎先生,在组件A中有一个合并冲突,然后错误的将组件A对于组件B的依赖设为了1.3-SNAPSHOT,而项目的其它部分继续向前推进。一堆开发人员试图修复组件B的bug,诡异的是他们在产品环境中看不到bug被修复了。偶然间有人看了下组件A然后意识到这个依赖指向了一个错误的版本。幸运的是,这个bug还没有大到要消耗很多钱或时间,但是马虎先生感到自己十分愚蠢,由于这次的兄弟依赖关系混乱问题,人们也没以前那么信任他了。(还好,马虎先生意识到这是一个用户行为错误而非Maven的错,但可能它会写一个糟糕的博客去无休止的抱怨Maven来使自己好受一点。)

幸运的是,只要你做一些微小的更改,依赖重复和兄弟依赖不匹配问题就能简单的预防。我们要做的第一件事是找出所有被用于一个以上模块的依赖,然后将其向上移到父POMdependencyManagement片段。我们先不管兄弟依赖。simple-parentPOM现在包含内容如下:

<project>
  ...
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring</artifactId>
        <version>2.0.7</version>
      </dependency>
      <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity</artifactId>
        <version>1.5</version>
      </dependency>  
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-annotations</artifactId>
        <version>3.3.0.ga</version>
      </dependency>
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-commons-annotations</artifactId>
        <version>3.3.0.ga</version>
      </dependency>
      <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate</artifactId>
        <version>3.2.5.ga</version>
        <exclusions>
          <exclusion>
            <groupId>javax.transaction</groupId>
            <artifactId>jta</artifactId>
          </exclusion>
        </exclusions>
      </dependency>
    </dependencies>
  </dependencyManagement>
  ...
</project>

在这些依赖配置被上移之后,我们需要为每个POM移除这些依赖的版本,否则它们会覆盖定义在父项目中的dependencyManagement。这里我只是简单展示一下simple-model

<project>
  ...
  <dependencies>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-annotations</artifactId>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate</artifactId>
    </dependency>
  </dependencies>
  ...
</project>

下一步我们应该做的是修复hibernate-annotationshibernate-commons-annotations的版本重复问题,因为这两个版本应该是一致的,我们通过创建一个称为hibernate-annotations-version的属性。结果simple-parent的片段看起来这样:

<project>
  ...
  <properties>
    <hibernate.annotations.version>3.3.0.ga</hibernate.annotations.version>
  </properties>

  <dependencyManagement>
    ...
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-annotations</artifactId>
      <version>${hibernate.annotations.version}</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-commons-annotations</artifactId>
      <version>${hibernate.annotations.version}</version>
    </dependency>
    ...
  </dependencyManagement>
  ...
</project

我们需要处理的最后一个问题是兄弟依赖。一种方案是像其它依赖一样将它们移到父项目的dependencyManagement中,在最顶层的父项目中定义所有兄弟项目的版本。这样做当然是可以的,但我们也可以使用内建属性org.sonatype.mavenbook0.5来解决这个版本问题。由于它们是兄弟依赖,在父项目中枚举它们也不能获得更多的价值,因此我们依赖于内置的0.5属性。由于我们都共享一个共同的组,因此,通过使用内置的org.sonatype.mavenbook属性引用当前POM的组,我们能够提前保证这些声明是正确的。simple-command依赖片段现在变成了这样:

<project>
  ...
  <dependencies>
    ...
    <dependency>
      <groupId>org.sonatype.mavenbook</groupId>
      <artifactId>simple-weather</artifactId>
      <version>0.5</version>
    </dependency>
    <dependency>
      <groupId>org.sonatype.mavenbook</groupId>
      <artifactId>simple-persist</artifactId>
      <version>0.5</version>
    </dependency>
    ...
  </dependencies>
  ...
</project>

总结一下我们为了降低依赖重复而完成的两项优化:

上移共同的依赖至dependencyManagement

如果多于一个项目依赖于一个特定的依赖,你可以在dependencyManagement中列出这个依赖。父POM包含一个版本和一组排除配置,所有的子POM需要使用groupIdartifactId引用这个依赖。如果依赖已经在dependencyManagement中列出,子项目可以忽略版本和排除配置。

为兄弟项目使用内置的项目version和groupId

使用{project.version}org.sonatype.mavenbook来引用兄弟项目。兄弟项目基本上一直共享同样的groupId,也基本上一直共享同样的发布版本。使用0.5可以帮你避免前面提到的兄弟版本不一致问题。