Maven多模块项目构建

划分多模块项目的原因

多人协作的Maven管理的真实的项目可以选择分模块,每个模块都对应着一个pom.xml。它们之间通过继承和聚合(也称作多模块,multi-module)相互关联。那么,为什么要这么做呢?我们明明在开发一个项目,划分模块后,导入Eclipse变成了N个项目,这会带来复杂度,给开发带来不便。
为了解释原因,假设有这样一个项目,很常见的Java Web应用。在这个应用中,我们分了几层:

  • Dao层负责数据库交互,封装了Mysql交互的接口 。
  • Service层处理业务逻辑,放一些Service接口和实现相关的Bean。
  • Web层负责与客户端交互,主要有一些Control类。

对应的,在一个项目中,我们会看到一些包名:

  • hans.app.dao
  • hans.app.service
  • hans.app.web
  • hans.app.util

这样整个项目的框架就清晰了,但随着项目的进行,你可能会遇到如下问题:
这个应用可能需要有一个前台和一个后台管理端(admin),你发现大部分dao,一些service,和大部分util是在两个应用中可。这样的问题,你一周内遇到了好几次。
pom.xml中的依赖列表越来越长以重用的,但是,由于目前只有一个项目(WAR),你不得不新建一个项目依赖这个WAR,这变得非常的恶心,因为在Maven中配置对WAR的依赖远不如依赖JAR那样简单明了,而且你根本不需要hans.app.web。有人修改了dao,提交到git并且不小心导致build失败了,你在编写service的代码,发现编译不过,只能等那人把dao修复了,你才能继续进行,很多人都在修改,到后来你根本就不清楚哪个依赖是谁需要的,渐渐的,很多不必要的依赖被引入。甚至出现了一个依赖有多个版本存在。
build整个项目的时间越来越长,尽管你只是一直在web层工作,但你不得不build整个项目。
某个模块,比如util,你只想让一些经验丰富的人来维护,可是,现在这种情况,每个开发者都能修改,这导致关键模块的代码质量不能达到你的要求。

我们会发现,其实这里实际上没有遵守一个设计模式原则:“高内聚,低耦合”。虽然我们通过包名划分了层次,并且你还会说,这些包的依赖都是单向的,没有包的环依赖。这很好,但还不够,因为就构建层次来说,所有东西都被耦合在一起了。因此我们需要使用Maven划分模块。

聚合

一个简单的Maven模块结构是这样的:

---- app-parent
|-- pom.xml (pom)
|
|-- app-util
| |-- pom.xml (jar)
|
|-- app-dao
| |-- pom.xml (jar)
|
|-- app-service
| |-- pom.xml (jar)
|
|-- app-web
|-- pom.xml (war)

上述简单示意图中,有一个父项目(app-parent)聚合很多子项目(app-util, app-dao, app-service, app-web)。每个项目,不管是父子,都含有一个pom.xml文件。而且要注意的是,小括号中标出了每个项目的打包类型。父项目是pom,也只能是pom。子项目有jar,或者war。根据它包含的内容具体考虑。

这些模块的依赖关系如下:

app-dao --> app-util
app-service --> app-mapper
app-web --> app-service

注意依赖的传递性(大部分情况是传递的,除非你配置了特殊的依赖scope),app-dao依赖于app-util,app-service依赖于app-dao,于是app-service也依赖于app-util。同理,app-web依赖于app-dao,app-util。

用项目层次的划分替代包层次的划分能给我们带来如下好处:
方便重用,如果你有一个新的项目需要用到app-dao和app-service,添加对它们的依赖即可,你不再需要去依赖一个WAR。而有些模块,如app-util,完全可以渐渐进化成公司的一份基础工具类库,供所有项目使用。这是模块化最重要的一个目的。
由于你现在划分了模块,每个模块的配置都在各自的pom.xml里,不用再到一个混乱的纷繁复杂的总的POM中寻找自己的配置。
某些模块,如app-util被所有人依赖,但你不想给所有人修改,现在你完全可以从这个项目结构出来,做成另外一个项目,svn只给特定的人访问,但仍提供jar给别人使用。
多模块的Maven项目结构支持一些Maven的更多的特性(如DepencencyManagement)

接下来讨论一下POM配置细节,实际上非常简单,先看app-parent的pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.test.hans</groupId>
    <artifactId>app-parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>app-util</module>
        <module>app-dao</module>
        <module>app-service</module>
        <module>app-web</module>
    </modules>
</project>

Maven的坐标GAV(groupId, artifactId, version)在这里进行配置,这些都是必须的。特殊的地方在于,这里的packaging为pom。所有带有子模块的项目的packaging都为pom。packaging如果不进行配置,它的默认值是jar,代表Maven会将项目打成一个jar包。
该配置重要的地方在于modules,例子中包含的子模块有app-util, app-dao, app-service, app-war。在Maven build app-parent的时候,它会根据子模块的相互依赖关系整理一个build顺序,然后依次build。这里model中的名称应与子模块所在目录名保持一致。
父模块的内容仅是一个pom.xml文件,它不想其他模块那样有src/main/java等目录。这也是容易理解的,父模块仅仅是帮助聚合其他模块构建的工具,它本身并无实际的内容。
父模块与其他模块的目录结构并非一定是父子关系,还可以是平行关系。那样的话只需要更改一下modules

<modules>
    <module>../app-util</module>
    <module>../app-mapper</module>
    <module>../app-service</module>
    <module>../app-web</module>
</modules>

在父模块运行mvn clean install命令 会得到:


命令行输出

上述输出中显示的是各模块的name,而不是artifactId,这也解释了为什么要在POM中配置合理的name字段,其目的是让Maven的构建输出更清晰。

继承

多模块构建的另一个特性就继承。继承解决最大的问题就是,重复。重复往往意味着更多的劳动和更多潜在的问题。POM的继承机制能让我们抽取重复的配置。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <parent>
        <artifactId>app-parent</artifactId>
        <groupId>com.test.hans</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>app-util</artifactId>
</project>

这个POM没有为app-util声明groupId和version,不过这并不代表app-util没有groupId和version。实际上,这个子模块隐式的从父模块继承了这两个元素,这也就消除了一些不必要的配置。如果子模块需要使用和父模块不一样的groupId和version完全可以显示声明。
还有哪些POM元素可以被继承呢?以下是一个常见的列表:

  • url:项目的URL地址
  • dependencies:项目的依赖配置
  • dependencyManagement:项目的依赖管理配置
  • repositories:项目的仓库配置
  • build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
  • reporting:包括项目的报告输出目录配置、报告插件配置等

上面的列表中包含了dependencies,说明依赖是会被继承的,这时我们会想,将spring等我们依赖的jar包就都可以配置到父模块中,这样子模块就能移除这些依赖,简化配置。但这种做法是存在问题的。当其中一个子模块不需要这些依赖的时候,强制它依赖spring等显然是不合理的。Maven提供的dependencyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块的灵活性。在dependencyManagement元素下的依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用。下面是测试项目的POM文件:
父模块:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.test.hans</groupId>
            <artifactId>org-remote-service</artifactId>
            <version>1.0.26</version>
        </dependency>
    </dependencies>
</dependencyManagement>

子模块

<dependencies>
    <dependency>
        <groupId>com.test.hans</groupId>
        <artifactId>org-remote-service</artifactId>
    </dependency>
</dependencies>

而且还可以将这些依赖的版本会Maven变量的形式提取了出来,这样所有模块的依赖的版本处于更加明显的位置。这样可以很好的解决多个子模块使用的依赖版本不一致的情况。可以降低依赖冲突的几率。如果子模块没有声明,即使该依赖已经在父POM中,子模块也不会引用。
但是如果其中的一个子模块想使用和父模块一样的POM,只要像下面这么写就可以了

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.test.hans</groupId>
            <artifactId>app-parent</artifactId>
            <version>1.0-SNAPSHOT</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Maven提供了dependencyManagement元素帮助管理项目依赖,类似的,Maven也提供了pluginManagement元素帮助管理插件。在该元素中配置的依赖不会造成实际的插件调用行为,当POM中配置了真正的plugin元素,并且其groupId和artifactId与pluginManagement中配置的插件匹配时,pluginManagement的配置才会影响实际的插件行为。
父模块:

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <encoding>${encoding}</encoding>
                 </configuration>
            </plugin>         
        </plugins>
     </pluginManagement>
</build>

子模块

<build>
     <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
            </plugin>         
        </plugins>
</build>

聚合与继承的关系

如果用一句话总结两者的关系就是没有关系。聚合主要是为了方便构建项目,继承主要是为了消除重复配置。
对于聚合模块来说,它知道有哪些被聚合的模块,但那些被聚合的模块不知道这个聚合模块的存在。
对于继承关系的父pom来说,它不知道有哪些子模块继承于它,但那些子模块都必须知道自己的父POM是什么。
它们之间的共同点就是,聚合POM与继承关系中的父POM的packaging都必须是pom,同时聚合模块与继承关系中的父模块出了POM之外都没有实际内容。
在现有的实际项目中,一般是一个POM即是聚合POM,又是父POM,这么做十分方便同时也是Maven的默认约定。



文/hansjason(简书作者)
原文链接:http://www.jianshu.com/p/79cc250be1b2
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
展开阅读全文

没有更多推荐了,返回首页