重复,还是重复

程序员应该有狗一般的嗅觉,要能嗅到重复这一最常见的坏味道,不管重复披着怎样的外衣,一旦发现,都应该毫不留情地彻底地将其干掉。不要因为POM不是产品代码而纵容重复在这里发酵,例如这样一段代码就有重复:

  1. <dependency>
  2. <groupId>org.springframework</groupId>
  3. <artifactid>spring-beans</artifactId>
  4. <version>2.5</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework</groupId>
  8. <artifactid>spring-context</artifactId>
  9. <version>2.5</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.springframework</groupId>
  13. <artifactid>spring-core</artifactId>
  14. <version>2.5</version>
  15. </dependency>

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

  1. <properties>
  2. <spring.version>2.5</spring.version>
  3. </properties>
  4. <depencencies>
  5. <dependency>
  6. <groupId>org.springframework</groupId>
  7. <artifactid>spring-beans</artifactId>
  8. <version>${spring.version}</version>
  9. </dependency>
  10. <dependency>
  11. <groupId>org.springframework</groupId>
  12. <artifactid>spring-context</artifactId>
  13. <version>${spring.version}</version>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.springframework</groupId>
  17. <artifactid>spring-core</artifactId>
  18. <version>${spring.version}</version>
  19. </dependency>
  20. </depencencies>

现在2.5只出现在一个地方,虽然代码稍微长了点,但重复消失了,日后升级依赖版本的时候,只需要修改一处,而且也能避免漏掉升级某个依赖。

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

消除多模块依赖配置重复

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

  1. <dependency>
  2. <groupId>junit</groupId>
  3. <artifactid>junit</artifactId>
  4. <version>4.8.</version>
  5. <scope>test</scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>log4j</groupId>
  9. <artifactid>log4j</artifactId>
  10. <version>1.2.</version>
  11. </dependency>

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

  1. <dependency>
  2. <groupId>junit</groupId>
  3. <artifactid>junit</artifactId>
  4. <version>3.8.</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>log4j</groupId>
  8. <artifactid>log4j</artifactId>
  9. <version>1.2.</version>
  10. </dependency>

看出什么问题来没有?对的,他漏了JUnit依赖的scope,那是因为他不熟悉Maven。还有什么问题?对,版本!虽然他和我一样都依赖了JUnit及Log4j,但版本不一致啊。我们开会讨论没有细化到具体用什么版本,但如果一个项目同时依赖某个类库的多个版本,那是十分危险的!OK,现在只是两个模块的两个依赖,手动修复一下没什么问题,但如果是10个模块,每个模块10个依赖或者更多呢?看来这真是一个泥潭,一旦陷进去就难以收拾了。

好在Maven提供了优雅的解决办法,使用继承机制以及dependencyManagement元素就能解决这个问题。注意,是dependencyMananget而非dependencies。也许你已经想到在父模块中配置dependencies,那样所有子模块都自动继承,不仅达到了依赖一致的目的,还省掉了大段代码,但这么做是有问题的,例如你将模块C的依赖spring-aop提取到了父模块中,但模块A和B虽然不需要spring-aop,但也直接继承了。dependencyManagement就没有这样的问题,dependencyManagement只会影响现有依赖的配置,但不会引入依赖。例如我们可以在父模块中配置如下:

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>junit</groupId>
  5. <artifactid>junit</artifactId>
  6. <version>4.8.</version>
  7. <scope>test</scope>
  8. </dependency>
  9. <dependency>
  10. <groupId>log4j</groupId>
  11. <artifactid>log4j</artifactId>
  12. <version>1.2.</version>
  13. </dependency>
  14. </dependencies>
  15. </dependencyManagement>

这段配置不会给任何子模块引入依赖,但如果某个子模块需要使用JUnit和Log4j的时候,我们就可以简化依赖配置成这样:

  1. <dependency>
  2. <groupId>junit</groupId>
  3. <artifactid>junit</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>log4j</groupId>
  7. <artifactid>log4j</artifactId>
  8. </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:

  1. <project>
  2. <modelVersion>4.0.</modelVersion>
  3. <groupId>com.juvenxu.sample</groupId>
  4. <artifactId>sample-dependency-infrastructure</artifactId>
  5. <packaging>pom</packaging>
  6. <version>1.0-SNAPSHOT</version>
  7. <dependencyManagement>
  8. <dependencies>
  9. <dependency>
  10. <groupId>junit</groupId>
  11. <artifactid>junit</artifactId>
  12. <version>4.8.</version>
  13. <scope>test</scope>
  14. </dependency>
  15. <dependency>
  16. <groupId>log4j</groupId>
  17. <artifactid>log4j</artifactId>
  18. <version>1.2.</version>
  19. </dependency>
  20. </dependencies>
  21. </dependencyManagement>
  22. </project>

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

  1. <dependencyManagement>
  2. <dependencies>
  3. <dependency>
  4. <groupId>com.juvenxu.sample</groupId>
  5. <artifactid>sample-dependency-infrastructure</artifactId>
  6. <version>1.0-SNAPSHOT</version>
  7. <type>pom</type>
  8. <scope>import</scope>
  9. </dependency>
  10. </dependencies>
  11. </dependencyManagement>
  12.  
  13. <dependency>
  14. <groupId>junit</groupId>
  15. <artifactid>junit</artifactId>
  16. </dependency>
  17. <dependency>
  18. <groupId>log4j</groupId>
  19. <artifactid>log4j</artifactId>
  20. </dependency>

这样,父模块的POM就会非常干净,由专门的packaging为pom的POM来管理依赖,也契合的面向对象设计中的单一职责原则。此外,我们还能够创建多个这样的依赖管理POM,以更细化的方式管理依赖。这种做法与面向对象设计中使用组合而非继承也有点相似的味道。

消除多模块插件配置重复

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

  1. <build>
  2. <pluginManagement>
  3. <plugins>
  4. <plugin>
  5. <groupId>org.apache.maven.plugins</groupId>
  6. <artifactId>maven-compiler-plugin</artifactId>
  7. <version>2.3.</version>
  8. <configuration>
  9. <source>1.5</source>
  10. <target>1.5</target>
  11. <encoding>UTF-</encoding>
  12. </configuration>
  13. </plugin>
  14. </plugins>
  15. </pluginManagement>
  16. </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中的坏味道。

关于作者

许晓斌(Juven Xu),国内社区公认的Maven技术专家、Maven中文用户组创始人、Maven技术的先驱和积极推动者。对Maven有深刻的认识,实战经验丰富,不仅撰写了大量关于Maven的技术文章,而且还翻译了开源书籍《Maven权威指南》,对Maven技术在国内的普及和发展做出了很大的贡献。就职于Maven之父的公司,负责维护Maven中央仓库,是Maven仓库管理器Nexus(著名开源软件)的核心开发者之一,曾多次受邀到淘宝等大型企业开展Maven方面的培训。此外,他还是开源技术的积极倡导者和推动者,擅长Java开发和敏捷开发实践。他的个人网站是:http://www.juvenxu.com

来源:http://www.infoq.com/cn/news/2011/01/xxb-maven-3-pom-refactoring

学习笔记——Maven实战(三)多模块项目的POM重构的更多相关文章

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

    在本专栏的上一篇文章POM重构之增还是删中.我们讨论了一些简单有用的POM重构技巧,包含重构的前提--持续集成,以及怎样通过加入或者删除内容来提高POM的可读性和构建的稳定性.但在实际的项目中,这些技 ...

  2. 学习笔记——Maven实战(二)POM重构之增还是删

    重构是广大开发者再熟悉不过的技术,在Martin Fowler的<重构——改善既有代码的设计>一书中,其定义为“重构(名词):对软件内部结构的一种调整,目的是在不改变软件之可察行为前提下, ...

  3. 学习笔记-[Maven实战]-第三章:Maven使用入门(1)

    说明:[Maven实战]一书还介绍了怎么样手工创建Maven工程,学习这本书是为了能尽快在工作中使用,就忽略了手工建工程的部分 如果想了解这部分的内容,可以自己看看书 开始: 1.新建一个maven工 ...

  4. 学习笔记-[Maven实战]-第三章:Maven使用入门(2)

    使用maven执行编译和测试 1.maven执行编译 (1).在pom.xml上点右键,选择Maven build... (2).在Goals里输入clean complie,执行编译 执行结果: [ ...

  5. 学习笔记——Maven实战(一)坐标规划

    坐标是什么?为什么要规划? 坐标是Maven最基本的概念,它就像每个构件的身份证号码,有了它我们就可以在数以千万计的构件中定位任何一个我们感兴趣的构件.举个最简单的例子,如果没有坐标,使用JUnit的 ...

  6. 学习笔记——Maven实战(六)Gradle,构建工具的未来?

    Maven面临的挑战 软件行业新旧交替的速度之快往往令人咂舌,不用多少时间,你就会发现曾经大红大紫的技术已经成为了昨日黄花,当然,Maven也不会例外.虽然目前它基本上是Java构建的事实标准,但我们 ...

  7. 学习笔记——Maven实战(七)常用Maven插件介绍(上)

    我们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven-compiler-plugin完成的.进一步说,每个任务对应了 ...

  8. 学习笔记——Maven实战(四)基于Maven的持续集成实践

    Martin的<持续集成> 相信很多读者和我一样,最早接触到持续集成的概念是来自Martin的著名文章<持续集成>,该文最早发布于2000年9月,之后在2006年进行了一次修订 ...

  9. 学习笔记——Maven实战(八)常用Maven插件介绍(下)

    我们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的.进一步说,每个任务对应 ...

随机推荐

  1. linux内核调优参考

    对于新部署的机器,需要做一些基本的调优操作,以更改一些默认配置带来的性能问题 1 修改打开文件数 root@mysql:/data/tools/db# vim /etc/security/limits ...

  2. linux常用查看日志命令

    转自:http://yinfeifei.iteye.com/blog/779217 1.cat命令: 功能:1)显示整个文件. 示例: $ cat fileName 2)把文件串连接后传到基本输出,如 ...

  3. Java基础の第二弹 基础语法

    Java关键字 •  abstract:表明类或类中的方法是抽象的:•  boolean:基本数据类型之一,布尔类型:•  break:提前跳出一个块:•  byte:基本数据类型之一,字节类型:•  ...

  4. Icacls 在windows目录文件授权中的应用

    前言 最近因工作需要,需要对批量服务器某一目录下的文件进行统一授权,对于linux来说,授权很方便,对于window来说,要对目录下的文件进行批量授权还是很不方便的,windows平台授权自然想到用i ...

  5. Team Foundation Server 2015(Update 1)集成 SharePoint Server 2013

    自TFS 2008开始,TFS就提供了与SharePoint Server的集成功能,通过多年的研发和提升,目前微软已经实现了两个服务器产品之间的完美集成.通过二者之间的集成,TFS服务器大幅提高和扩 ...

  6. JVM 运行时数据区域

    Java虚拟机管理的内存包括以下几个运行时数据区域: 1.程序计数器: 程序计数器是一块比较小的内存空间,是当前线程执行的字节码行号指示器.Java多线程是通过线程轮流切换来实现的,所以每个线程都有一 ...

  7. 自定义input[type="radio"]的样式

    对于表单,input[type="radio"] 的样式总是不那么友好,在不同的浏览器中表现不一. 为了最大程度的显示出它们的差别,并且为了好看,首先定义了一些样式: <fo ...

  8. HFSS 边界条件

    Ansoft HFSS求解就是对微分形式的麦克斯韦方程采取有限元方法进行数值求解,在场矢量和导数是都单值.有界而且沿空间连续分布的假设下,这些方程才可以使用.在边界和场 源处,场是不连续的,场的导数变 ...

  9. Golang tips ----- 函数

    1.在函数调用时,Golang没有默认参数值 2.一个函数声明如果没有函数体,表面该函数不是由Golang实现的,这样的声明定义了函数标识符 3.拥有函数名的函数只能在包级语法块中被声明 4.函数值( ...

  10. UVA 11573 Ocean Currents --BFS+优先队列

    采用优先队列做BFS搜索,d[][]数组记录当前点到源点的距离,每次出队时选此时eng最小的出队,能保证最先到达的是eng最小的.而且后来用普通队列试了一下,超时..所以,能用优先队列的,就要用优先队 ...