Maven实战(三)——多模块项目的POM重构

在本专栏的上一篇文章POM重构之增还是删中。我们讨论了一些简单有用的POM重构技巧,包含重构的前提——持续集成,以及怎样通过加入或者删除内容来提高POM的可读性和构建的稳定性。但在实际的项目中,这些技巧还是不够的。特别值得一提的是,实际的Maven项目基本都是多模块的。假设只重构单个POM而不考虑模块之间的关系。那就会造成无谓的反复。本文就讨论一些基于多模块的POM重构技巧。

反复,还是反复

程序猿应该有狗一般的嗅觉,要能嗅到反复这一最常见的坏味道,无论反复披着如何的外衣,一旦发现,都应该毫不留情地彻底地将其干掉。

不要由于POM不是产品代码而纵容反复在这里发酵,比如这样一段代码就有反复:

<dependency>
  <groupId>org.springframework</groupId>
  <artifactid>spring-beans</artifactId>
  <version>2.5</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactid>spring-context</artifactId>
  <version>2.5</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactid>spring-core</artifactId>
  <version>2.5</version>
</dependency>

你会在一个项目中使用不同版本号的SpringFramework组件么?答案显然是不会。因此这里就不是必需反复写三次<version>2.5</version>,使用Maven属性将2.5提取出来例如以下:

<properties>
  <spring.version>2.5</spring.version>
</properties>
<depencencies>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-beans</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-context</artifactId>
    <version>${spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactid>spring-core</artifactId>
    <version>${spring.version}</version>
  </dependency>
</depencencies>

如今2.5仅仅出如今一个地方,尽管代码略微长了点,但反复消失了,日后升级依赖版本号的时候,仅仅须要改动一处,并且也能避免漏掉升级某个依赖。

读者可能已经很熟悉这个样例了,我这里再啰嗦一遍是为了给后面做铺垫。多模块POM重构的目的和该例一样,也是为了消除反复,模块越多。潜在的反复就越多。重构就越有必要。

消除多模块依赖配置反复

考虑这样一个不大不小的项目,它有10多个Maven模块。这些模块分工明白,各司其职。相互之间耦合度比較小,这样大家就行专注在自己的模块中进行开发而不用过多考虑他人对自己的影响。(好吧。我承认这是比較理想的情况)那我開始对模块A进行编码了,首先就须要引入一些常见的依赖如JUnit、Log4j等等:

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
    <version>4.8.2</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
    <version>1.2.16</version>
  </dependency>

我的同事在开发模块B。他也要用JUnit和Log4j(我们开会讨论过了。统一单元測试框架为JUnit而不是TestNG,统一日志实现为Log4j而不是JUL。为什么做这个决定就不解释了。总之就这么定了)。同事就写了例如以下依赖配置:

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
    <version>3.8.2</version>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
    <version>1.2.9</version>
  </dependency>

看出什么问题来没有?对的。他漏了JUnit依赖的scope。那是由于他不熟悉Maven。还有什么问题?对。版本号。尽管他和我一样都依赖了JUnit及Log4j,但版本号不一致啊。

我们开会讨论没有细化到详细用什么版本号,但假设一个项目同一时候依赖某个类库的多个版本号,那是十分危急的。OK。如今仅仅是两个模块的两个依赖,手动修复一下没什么问题,但假设是10个模块,每一个模块10个依赖或者很多其它呢?看来这真是一个泥潭,一旦陷进去就难以收拾了。

好在Maven提供了优雅的解决的方法,使用继承机制以及dependencyManagement元素就能解决问题。

注意,是dependencyMananget而非dependencies。或许你已经想到在父模块中配置dependencies,那样全部子模块都自己主动继承,不仅达到了依赖一致的目的。还省掉了大段代码,但这么做是有问题的,比如你将模块C的依赖spring-aop提取到了父模块中,但模块A和B尽管不须要spring-aop,但也直接继承了。dependencyManagement就没有这种问题。dependencyManagement仅仅会影响现有依赖的配置。但不会引入依赖。比如我们能够在父模块中配置例如以下:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactid>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>log4j</groupId>
      <artifactid>log4j</artifactId>
      <version>1.2.16</version>
    </dependency>
  </dependencies>
</dependencyManagement>

这段配置不会给不论什么子模块引入依赖,但假设某个子模块须要使用JUnit和Log4j的时候,我们就能够简化依赖配置成这样:

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
  </dependency>

如今仅仅须要groupId和artifactId。其他元素如version和scope都能通过继承父POM的dependencyManagement得到。假设有依赖配置了exclusions,那节省的代码就更加可观。但重点不在这。重点在于如今可以保证全部模块使用的JUnit和Log4j依赖配置是一致的。并且子模块仍然可以按需引入依赖,假设我不配置dependency,父模块中dependencyManagement下的spring-aop依赖不会对我产生不论什么影响。

或许你已经意识到了。在多模块Maven项目中。dependencyManagement差点儿是不可缺少的,由于仅仅有它是才可以有效地帮我们维护依赖一致性

本来关于dependencyManagement我想介绍的也几乎相同了,但几天前和Sunng的一次讨论让我有了很多其它的内容分享。

那就是在使用dependencyManagement的时候,我们能够不从父模块继承,而是使用特殊的import scope依赖。Sunng将其列为自己的Maven Recipe #0。我再简介下。

我们知道Maven的继承和Java的继承一样。是无法实现多重继承的,假设10个、20个甚至很多其它模块继承自同一个模块。那么依照我们之前的做法,这个父模块的dependencyManagement会包括大量的依赖。

假设你想把这些依赖分类以更清晰的管理,那就不可能了,import scope依赖能解决问题。

你能够把dependencyManagement放到单独的专门用来管理依赖的POM中,然后在须要使用依赖的模块中通过import scope依赖,就能够引入dependencyManagement。比如能够写这样一个用于依赖管理的POM:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.juvenxu.sample</groupId>
  <artifactId>sample-dependency-infrastructure</artifactId>
  <packaging>pom</packaging>
  <version>1.0-SNAPSHOT</version>
  <dependencyManagement>
    <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactid>junit</artifactId>
          <version>4.8.2</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>log4j</groupId>
          <artifactid>log4j</artifactId>
          <version>1.2.16</version>
        </dependency>
    </dependencies>
  </dependencyManagement>
</project>

然后我就能够通过非继承的方式来引入这段依赖管理配置:

  <dependencyManagement>
    <dependencies>
        <dependency>
          <groupId>com.juvenxu.sample</groupId>
          <artifactid>sample-dependency-infrastructure</artifactId>
          <version>1.0-SNAPSHOT</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
    </dependencies>
  </dependencyManagement>

  <dependency>
    <groupId>junit</groupId>
    <artifactid>junit</artifactId>
  </dependency>
  <dependency>
    <groupId>log4j</groupId>
    <artifactid>log4j</artifactId>
  </dependency>

这样,父模块的POM就会很干净,由专门的packaging为pom的POM来管理依赖,也契合的面向对象设计中的单一职责原则。此外,我们还可以创建多个这种依赖管理POM,以更细化的方式管理依赖。

这种做法与面向对象设计中使用组合而非继承也有点相似的味道。

消除多模块插件配置反复

与dependencyManagement类似的,我们也能够使用pluginManagement元素管理插件。一个常见的使用方法就是我们希望项目全部模块的使用Maven Compiler Plugin的时候,都使用Java 1.5。以及指定Java源文件编码为UTF-8。这时能够在父模块的POM中例如以下配置pluginManagement:

<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
          <encoding>UTF-8</encoding>
        </configuration>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

这段配置会被应用到全部子模块的maven-compiler-plugin中,因为Maven内置了maven-compiler-plugin与生命周期的绑定。因此子模块就不再须要不论什么maven-compiler-plugin的配置了。

与依赖配置不同的是,通常全部项目对于随意一个依赖的配置都应该是统一的,但插件却不是这样。比如你能够希望模块A执行全部单元測试,模块B要跳过一些測试,这时就须要配置maven-surefire-plugin来实现。那样两个模块的插件配置就不一致了。

这也就是说。简单的把插件配置提取到父POM的pluginManagement中往往不适合全部情况,那我们在使用的时候就须要注意了,仅仅有那些普适的插件配置才应该使用pluginManagement提取到父POM中。

关于插件pluginManagement。Maven并没有提供与import scope依赖类似的方式管理,那我们仅仅能借助继承关系。只是好在一般来说插件配置的数量远没有依赖配置那么多。因此这也不是一个问题。

小结

关于Maven POM重构的介绍。在此就告一段落了。基本上假设你掌握了本篇和上一篇Maven专栏讲述的重构技巧,并理解了其背后的目的原则,那么你肯定能让项目的POM变得更清晰易懂,也能尽早避免一些潜在的风险。尽管Maven不过用来帮助你构建项目和管理依赖的工具,POM也并非你正式产品代码的一部分。但我们也应该认真对待POM,这有点像測试代码,曾经可能大家都认为測试代码可有可无。更不会去用心重构优化測试代码,但随着敏捷开发和TDD等方式越来越被人接受,測试代码得到了开发者越来越多的关注。

因此这里我希望大家不只满足于一个“能用”的POM,而是可以积极地去修复POM中的坏味道。

文章出处:http://www.infoq.com/cn/news/2011/01/xxb-maven-3-pom-refactoring



原文地址:https://www.cnblogs.com/tlnshuju/p/6794590.html