9.6.2. 多模块 vs. 继承

继承于一个父项目和被一个多模块项目管理是有区别的。一个父项目是指它把所有的值传给它的子项目。一个多模块项目只是说它管理一组子模块,或者说一组子项目。多模块关系从上层往下定义。当建立一个多模块项目的时候,你告诉一个项目它的构建需要包含指定的模块。多模块构建用来将模块聚集到一个单独的构建中。父子关系是从叶节点往上定义的。父子关系更多的是处理一个特定项目的定义。当你给一个子项目关联一个父项目的时候,你告诉Maven该项目的POM起源于另一个项目。

为了展示选择继承还是多模块的决策过程,考虑如下的两个例子:用来生成本书的Maven项目,以及一个包含很多逻辑上同组模块的假想项目。

9.6.2.1. 简单项目

首先,我们看一下maven-book项目。它的继承和多模块关系如Figure 9.5, “maven-book 多模块 vs. 继承”所示。

maven-book 多模块 vs. 继承

Figure 9.5. maven-book 多模块 vs. 继承


当我们构建你正阅读的Maven书的时候,我们在名为maven-book的项目下运行mvn package。该多模块项目包含两个子模块:book-examplesbook-chapters。两者都共享同样的父项目,它们只通过同为maven-book子项目的方式关联。book-examples构建了可下载的本书样例的ZIPTGZ存档文件。当我们在book-examples/目录使用mvn package运行book-examples的构建的时候,它完全不知道它是maven-book项目的一部分。book-examples完全不关心maven-book,它知道的是它的父项目是最顶层的sonatype POM,它自身创建样例的归档文件。该例中,maven-book的存在只是为了方便聚集模块构建。

该些书的项目没有定义一个父项目。所有这个三个项目:maven-bookbook-examplesbook-chapters和都继承同一个共享的“团体”父项目——sonatype。这是一种采用Maven的组织中常见的实践,一些组织定义一个顶层的团体POM,作为一个默认的父项目为其它项目服务,而不是让每个项目默认去扩展超级POM。在这个书本样例中,并不是一定要让book-examplesbook-chapters共享同样的父POM,它们是完全不同的两个项目,拥有完全不同的而依赖,不同的构建配置,使用极为不同的插件创建你正阅读的内容。“团体”POM能让组织有机会自定义一些Maven的默认行为,提供一些组织特定的信息,如配置部署设置和构建profile。

9.6.2.2. 多模块企业级项目

让我们看一下另一个例子,它提供了现实项目中继承和多模块关系存在的更准确的画面。Figure 9.6, “企业级多模块 vs. 继承”展示了类似于典型企业应用中的一组项目。有一个的公司顶层POM,其artifactId值为sonatype。有一个名为big-system的多模块项目,引用了子模块server-sideclient-side

企业级多模块 vs. 继承

Figure 9.6. 企业级多模块 vs. 继承


这里到底是怎么回事呢?让我们尝试着给这一组混乱的箭头解构。首先,看一下big-system。这个big-system可能就是你将要运行mvn package以构建并测试整个系统的地方。big-system引用了子模块client-sideserver-side。这两个项目都管理了大量运行在服务端或者客户端的代码。让我们看一下server-side项目。在server-side下面有一个名为server-lib的项目和一个名为web-apps的多模块项目。在web-apps下面有两个Java web应用:client-webadmin-web

让我们从client-webadmin-webweb-apps开始讨论父子关系。由于这两个web应用都用同样的web应用框架实现(假设是Wicket),两个项目都共享同样的一组核心依赖。对Servlet APIJSP API,和Wicket的依赖可以放到web-apps项目中。client-webadmin-web都需要依赖server-lib,它就可以定义为一个web-appsserver-lib之间的依赖。因为client-webadmin-web通过继承web-apps共享了如此多的配置,它们的POM很小,只包含定义符,父项目声明和最终构建名称。

本例中,使用父子关系的最主要原因是为了给一组逻辑关联的项目共享依赖和通用配置。所有big-system下的项目都通过子模块与其它项目关联,但是并不是所有子模块都被配置成指回该父项目。所有模块都是子模块是为了方便,要构建整个系统,只要到big-system项目目录下运行mvn package。再仔细看下上图,你会发现在server-sidebig-system之间没有父子关联。这是为什么?POM继承十分有用,但它可能被滥用。当然在需要共享依赖和配置的时候,父子关联需要被使用。但当两个项目截然不同的时候使用父子关联是不明智的。举个例子,server-sideclient-side项目。在系统中,让server-sideclient-side都从big-system继承通用的POM是可能的,但一旦这两个子项目的重大分歧出现,你就需要费脑子一方面将构建配置抽离到big-system中,另一方面又需要不影响其它的子项目。即使server-sideclient-side同时依赖于Log4J,它们也可能拥有截然不同的插件配置。

有时候基于风格和经验,为了允许项目如server-sideclient-side保持完全独立,少量的重复配置是最小的代价。设计一组继承了五六层POM的第三方项目永远都不是一个好主意。这样的配置下,你可能不再需要在多个地方重复Log4J依赖,但你会需要查看五六个POM来弄清Maven如何计算出你的有效POM。所有的这些的新的复杂度只是为了避免五行依赖声明。在Maven中,有一种“Maven方式”,但是也有很多其它方式完成同样的事情。这都可归结为一种偏好和风格。大部分情况下,如果你的子模块定义了往回的父项目引用,不会出什么问题,但是你的Maven使用情况一直在变。

9.6.2.3. 原型父项目

Figure 9.7, “为特定的项目使用父项目作为“原型””的例子所示,这是又一种假想的创造性的方式,使用继承和多模块构建达到重用依赖的目的。

为特定的项目使用父项目作为“原型”

Figure 9.7. 为特定的项目使用父项目作为“原型”


在该例中,你有两个截然不同的系统:system-asystem-b,各自定义独立的应用。system-a定义了两个模块a-liba-swingsystem-aa-lib两者都定义了顶层的sonatype POM作为父项目,但a-swing项目定义了swing-proto作为它的父项目。在该系统中,swing-proto为所有Swing应用程序提供了一个基础POM,而struts-proto为所有Struts 2 web应用程序提供了一个基础POM。sonatype POM提供了高层的信息如groupId,组织信息和构建profile,struts-proto定义了所有创建struts应用需要的依赖。如果你的开发根据不同的应用有不同的特征,每类应用又需要遵循同样一组规则,那么这里介绍的方法很有用。如果你正创建很多struts应用,但是它们很少相互关联,你可能只需要在struts-proto中定义所有通用的东西。这种方式的缺点是有不能在system-asystem-b项目层次中使用父子关系来共享如开发人员和其它构建配置信息。一个项目只能有一个父项目。

这种方法的另外一个缺点是,一旦你有一个项目需要“破坏该模型”,你需要重写父POM,或者想办法将自定义信息提取到一个共享的父项目中,而不让这些自定义信息影响所有子项目。总得来说,为特定的项目“类型”使用POM作为原型不是一种推荐的做法。