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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章