用 Maven 管理多模块项目的最佳实践

引入

近期要开始写一个 Java Web 项目,需要进行团队合作,因此清晰的模块划分、良好的项目组织方式显得格外重要。

首先,介绍一下这个项目的大致功能:

网站系统负责处理用户请求,从 MySql 数据库读取数据并展现给用户,并将新数据写回 MySql 数据库。

后台进程(爬虫模块)负责定时爬取网页、过滤数据、分析变化,然后将结果存入 MySql 数据库;

该项目的功能可概括为:网站通过读写数据库将数据展现给用户,同时后台进程负责定时往数据库写入新数据。

这个项目从逻辑上可分为四个模块:

  • 网站前端
  • 业务逻辑
  • 数据库读写
  • 后台进程

模块依赖问题

如果是一般的开发,这些模块的代码实际上是放在同一个项目里的。也就是说,每次对一个模块进行单元测试,都需要先编译整个项目,然后执行整个项目的单元测试,才能得到该模块的测试结果。

我们不想出现这种不必要的情况,而是相对独立地对这些模块进行开发、编译、测试,并且最终可作为一个完整的项目发布。

这样问题就来了,这些模块之间可能互相依赖,某个模块的编译和测试依赖于另一个模块,例如以下的依赖关系:

  • 网站前端 依赖于 业务逻辑
  • 业务逻辑 依赖于 数据库读写、后台进程
  • 后台进程 依赖于 数据库读写

如何将它们分离开,像子项目一样独立进行开发,并且能满足这些依赖条件?

项目层次结构

接下来我将介绍如何使用 Maven 来模块化管理项目及其依赖。

我们采用以下的目录结构来组织项目:

project
|-- pom.xml
|-- module-dao/
|   `-- pom.xml
|-- module-service/
|   `-- pom.xml
|-- module-scraper/
|   `-- pom.xml
`-- webapp/
    `-- pom.xml
  • project 是根项目,用于聚合各个模块
  • module-dao 是数据库读写模块
  • module-service 是业务逻辑模块
  • module-scraper 是后台进程(爬虫)模块
  • webapp 则是前端模块。

上面的各个模块都可以看作是 project 的一个 Maven 子项目,有自己的 pom.xml 文件,可单独开发。

虽然我们在这里将 webapp 划分为前端模块,但它其实是整个 Java Web 项目的主要目录。最终打包时,所有的模块都会封装成 jar 的形式拷贝到 webapp/WEB-INF/lib 目录下,由 webapp 生成 WAR 包。

Maven 项目生成

按照上面给出的目录结构,我们开始生成项目的骨架。

首先,我们创建一个名为 project 的目录,作为聚合各个子项目的根项目,其 pom.xml 文件会在稍后创建。

在该目录下,我们使用命令来自动生成每个模块的项目模版以及初始的 pom.xml 文件。

以 module-dao 模块为例:

mvn archetype:generate -DgroupId=com.codebelief.app -DartifactId=module-dao -DinteractiveMode=false

module-scraper 和 module-service 模块均可按照相同方式生成。

webapp 的项目模版与上述三个模块不同,需要指明为 maven-archetype-webapp 类型:

mvn archetype:generate -DgroupId=com.codebelief.app -DartifactId=webapp -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false

pom.xml 配置

项目的层次结构已经有了,接下来要进行具体的配置,包括项目的版本、依赖的外部库以及各模块之间的依赖。

Maven 以 GAV 作为项目的唯一标志,GAV 是组织(groupId)、名称(artifactId)、版本(version)的缩写。

pom.xml 定义了项目的 GAV,此外还定义各种依赖和构建过程。

我们在这里不讨论有关 Maven 构建过程的相关内容,而是就项目的结构和各模块的依赖关系的 pom.xml 配置给出相应讲解。

首先,是对整个项目(project)进行配置:

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>
  <artifactId>webpage-update-subscribe</artifactId>
  <groupId>com.codebelief.app</groupId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <name>Webpage Update Subscribe</name>
  <url>http://app.codebelief.com/webpage-update-subscribe/</url>

  <modules>
      <module>webapp</module>
      <module>module-scraper</module>
      <module>module-service</module>
      <module>module-dao</module>
  </modules>

</project>

在这里,我们使用 <module> 标签来说明该项目由哪些模块构成,我们对项目的编译、测试、打包操作都会触发各个模块的相应操作。

需要注意的是,打包方式(packaging)必须设为 pom,因为根目录的 Maven 项目是负责将各个子模块进行打包,本身并没有实际代码。

接下来,是各个模块的配置:

module-dao/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">

  <parent>
      <artifactId>webpage-update-subscribe</artifactId>
      <groupId>com.codebelief.app</groupId>
      <version>1.0-SNAPSHOT</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>
  <artifactId>module-dao</artifactId>
  <name>DAO Module</name>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.44</version>
    </dependency>
  </dependencies>

</project>

这是 DAO 模块的配置,通过 <parent> 标签注明其 parent,这样该模块便会自动继承 parent 的 groupIdversion 等等。

其中, <dependency> 标签注明了依赖项,即外部的 junit 和 mysql-connector-java 库。

这里的 <packaging> 写的是 jar,因为我们要将该模块打包成 jar 后提供给其它模块使用。

module-scraper/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">

  <parent>
      <artifactId>webpage-update-subscribe</artifactId>
      <groupId>com.codebelief.app</groupId>
      <version>1.0-SNAPSHOT</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>
  <artifactId>module-scraper</artifactId>
  <name>Scraper Module</name>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.codebelief.app</groupId>
      <artifactId>module-dao</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>

</project>

scraper 模块需要数据库的读写,依赖于 DAO 模块,因此在 <dependency> 标签中注明了 DAO 模块的 GAV。

这里,我们使用变量 ${project.version} 让模块的版本号与项目(parent)保持一致。

module-service/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">

  <parent>
      <artifactId>webpage-update-subscribe</artifactId>
      <groupId>com.codebelief.app</groupId>
      <version>1.0-SNAPSHOT</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>
  <artifactId>module-service</artifactId>
  <name>Service Module</name>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.codebelief.app</groupId>
      <artifactId>module-dao</artifactId>
      <version>${project.version}</version>
    </dependency>
    <dependency>
      <groupId>com.codebelief.app</groupId>
      <artifactId>module-scraper</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>

</project>

service 模块同时依赖于 DAO 和 scraper 模块,因此两个模块都需要添加到依赖中。

webapp/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">

  <parent>
      <artifactId>webpage-update-subscribe</artifactId>
      <groupId>com.codebelief.app</groupId>
      <version>1.0-SNAPSHOT</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>
  <artifactId>webapp</artifactId>
  <name>Web Application</name>
  <packaging>war</packaging>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.struts</groupId>
      <artifactId>struts2-core</artifactId>
      <version>2.5.13</version>
    </dependency>
    <dependency>
      <groupId>com.codebelief.app</groupId>
      <artifactId>module-service</artifactId>
      <version>${project.version}</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>webpage-update-subscribe</finalName>
  </build>

</project>

该模块依赖于其它几个模块,由于这是个 Java Web 项目,因此打包方式设置为 war。

安装模块到本机

此时,虽然各模块的依赖关系已经通过 pom.xml 注明了,但如果我们直接针对某个模块进行编译测试,还是会发现缺少依赖的其它模块,因为现在各模块都尚未编译安装,Maven 无法在中央仓库、本地仓库中找到我们依赖的这些模块。

我们可以将这些模块像第三方库一样安装到本机上。

我们在 project 目录下执行 mvn install,该命令用于将项目安装到本机上,对于本项目而言,就是将各个模块都安装到本地仓库中。

该命令也可以在某个模块有了改动之后,单独在该模块的目录下执行,以便将该模块的最新版本安装到本地仓库中,供其它模块使用。

安装各模块到本机之后,当我们单独测试某一模块时,可以直接使用本地仓库中编译好的依赖模块,而不需要实时编译这些模块,这就解决了前面提到的不同模块之间编译时期源代码相互依赖,单元测试无法单独进行的问题。

如果想要移除本地仓库中安装了的模块,可以执行以下命令:

mvn build-helper:remove-project-artifact

同理,在 project 下执行就是移除所有模块,在某个模块目录下执行,就是移除该模块。

部署模块到团队服务器(*)

对于一个团队而言,有些模块可能会在不同项目中反复使用,这种情况就可以将该模块安装到团队的远程服务器上,无论是哪个成员需要用到该模块,都可以在自己的机器上配置远程依赖项,编译项目时会自动将该依赖项下载到本地。

这里以 scp 命令以及公钥认证为例,首先编辑 用户/.m2 目录下的 settings.xml 文件,添加 ssh 认证信息:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
                      http://maven.apache.org/xsd/settings-1.0.0.xsd">
  ...
  <servers>
    <server>
      <id>my-repository</id>
      <username>wray</username>
      <!-- Default value is ~/.ssh/id_dsa -->
      <privateKey>/path/to/identity</privateKey> (default is ~/.ssh/id_dsa)
      <passphrase>my_key_passphrase</passphrase>
    </server>
  </servers>
  ...
</settings>

对于需要部署到远程服务器仓库的模块,编辑其 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/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

  <groupId>com.codebelief.app</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <distributionManagement>
    <repository>
      <id>my-repository</id>
      <name>My Repository</name>
      <url>scp://repository.codebelief.com/repository/maven</url>
    </repository>
  </distributionManagement>
</project>

单独测试某一模块

有了上面的准备工作之后,现在就可以单独测试某个模块了。

进入 module-service 目录,执行 mvn test 以对 service 模块进行单独的单元测试。

还可以执行 mvn compile 进行编译操作,执行 mvn package 打包成 jar 文件。

打包整个项目

现在,一切都变得非常简单,一条命令就能完成项目的编译、测试和打包。

要将整个项目打包成 WAR 包,只需要在 project 目录下或 webapp 目录下执行 mvn clean package 即可自动完成各个模块的编译、测试,最后打包成 war 文件。

通常,我们在执行 package 操作之前,会先执行 clean 操作,用于清除之前的编译文件,以确保将打包的是干净的编译文件。

执行完打包操作之后,我们可以在 webapp/target 下看到后缀为 war 的文件,这就是项目的最终形式,可以直接部署到 Web 容器中。

参考

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注