10 月,开发者不可错过的开源大数据大会-2021 WeDataSphere 社区大会深圳站 了解详情
写点什么

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

2011 年 1 月 09 日

在本专栏的上一篇文章 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 组件么?答案显然是不会。因此这里就没必要重复写三次2.5,使用 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 中的坏味道。

关于作者

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

2011 年 1 月 09 日 18:1644778

评论

发布
暂无评论
发现更多内容

一个草根的日常杂碎(10月18日)

刘新吾

随笔杂谈 生活记录 社会百态

深入java week1-01 字节码、内存、GC、调试工具

闷骚程序员

Flink窗口算子-6-8

小知识点

scala 大数据 flink

Go语言内存管理三部曲(三)图解GC算法和垃圾回收原理

网管

Go 内存管理 垃圾回收 GC GC算法

万物互联的IoT时代,柔性电子会大行其道吗?

脑极体

一个草根的日常杂碎(10月19日)

刘新吾

随笔杂谈 生活记录 社会百态

一个草根的日常杂碎(10月20日)

刘新吾

随笔杂谈 生活记录 社会百态

java week1练习

闷骚程序员

聊聊技术人员如何学习成长

架构精进之路

职业成长

游戏数值策划之常用excel函数

吴优秀同学

Excel 游戏

架构师必备的那些分布式事务解决方案!!

架构师修行之路

分布式 微服务 架构设计

Linux的上手命令

林昱榕

Linux 常用命令

解锁华为云AI如何助力无人车飞驰“新姿势”,大赛冠军有话说

华为云开发者社区

AI 无人驾驶

【线上排查实战】AOP切面执行顺序你真的了解吗

Zhendong

spring aop

甲方日常 35

句子

工作 随笔杂谈 日常

Java程序员还在为没有项目经验感到苦恼?快来看看GitHub上最火的SpringCloud微服务商城系统开源项目,附全套教程!

Java架构之路

Java 程序员 架构 面试 编程语言

架构训练营学习笔记之五技术选型(一)

于成龙

架构训练营

Nginx 在运维领域中的应用,看这一篇就够了

华章IT

nginx Linux 运维工程师

分布式下,我想要一致性

架构师修行之路

分布式 微服务

央行数字货币离我们还有多远?

CECBC区块链专委会

数字货币

华为云瑶光:打通云边端界限,为企业云上业务带来最优解

华为云开发者社区

华为 云服务

利用区块链等技术,加强对交通运输信用信息的归集共享和分析应用

CECBC区块链专委会

区块链 交通运输

1分钟带你入门 React 公共逻辑抽离HOC...

Leo

React Hooks 前端进阶训练营 HOC Render Props

第四周作业

dll

年纪轻轻怎么就卵巢早衰了?试管可帮忙!

Geek_65d32f

试管 三代试管

云原生在京东丨云原生时代下的监控:如何基于云原生进行指标采集?

京东科技开发者

云原生

透视HTTPS建造固若金汤的堡垒

码哥字节

https 加密解密 HTTP

算法分析关键

Geek_0b8195

算法和数据结构

mongodb 源码实现、调优、最佳实践系列-百万级代码量mongodb内核源码阅读经验分享

杨亚洲(专注mongodb及高性能中间件)

MySQL mongodb 源码 中间件 分布式数据库mongodb

二十、深入Python迭代器和生成器

刘润森

Python

膜拜!阿里技术总监纯手打的《MySQL笔记》内部资料限时分享

Java架构师迁哥

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