﻿# 如何使用 Apache Maven 管裡多個模組的專案 (Multi-Module Project)

我在寫 .NET 的時候，經常會透過 Visual Studio 2022 的**方案**(Solution)來管裡多個**專案**(Project)，透過適當的切割可以讓每個專案的職責更加明確，提升可維護性。若是寫 Java 的話，一個**專案**比較常被稱呼為一個**模組**(Module)，所以經常可以看到 **Multi-Module Project** 這樣的說法。今天這篇文章我打算分享如何利用 Apache Maven 來建立一個**多模組專案**，並示範如何互相引用彼此的類別。

### 簡介多模組專案 (Multi-Module Project)

由於 Java 本身比較沒有多專案之類的概念，大多都需要透過第三方的建置工具輔助之下完成。若透過 Apache Maven 管理 Multi-Module Project 的話，則會透過一個 `aggregator POM` (聚合器 POM 定義檔) 來整合多個透過 Maven 管理的**子專案**或**子模組**(`submodules`)。

通常這個 `aggregator POM` 會置於專案根目錄，並且不會產生自己的 JAR 檔，因此在 `pom.xml` 裡面定義的通常會把 `<packaging>` 定義為 `pom`，例如：

```xml
<packaging>pom</packaging>
```

每一個**子專案**或**子模組**(`submodules`)，其實就是一個「子資料夾」而已，且內容就跟普通的 Maven 專案沒有什麼兩樣，而且可以分別建置，也可以透過 `aggregator POM` 一起建置，整合的彈性還蠻大的。

將一個大專案拆解成多個小專案，優點還蠻多的，其中最明顯的效益就是：

1.  專案變小，而且可以獨立開發、測試、部署，複雜度可控
2.  增加程式碼的複用性(Reuseability)，比較不會出現重複的程式碼
3.  若專案採行 Monorepo 架構，比較容易實現跨組織的專案配置 (可搭配 [Git Submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) 配置)
4.  複雜的軟體架構下，也可以透過簡單的、抽象的 `mvn` 命令，輕鬆的建置專案
5.  應用程式組態的部分，也可以透過 `aggregator POM` 一起管裡，管裡彈性大增

Maven 的 POM 支援繼承的特性，他可以透過 `<parent>` 元素定義 Parent POM 的 `artifact` 套件，因此你可以透過 Parent POM 來定義許多「共用」的 `Properties`、`Plugins` 與 `Dependencies`，這樣便可大幅簡化專案的 POM 定義檔內容。其實最經典的案例就是 Spring 專案，他就是透過 Parent POM 的定義，大幅降低專案 POM 的複雜度。

### 嘗試打造一個多模組專案

我原本是參考 Baeldung 的 [Multi-Module Project with Maven](https://www.baeldung.com/maven-multi-module) 文章所述的步驟複刻一遍這個實作過程，不過該文章缺漏太多步驟，因此實作過程我有做了不少調整。

以下的例子將會建立 3 個子模組：

1.  一個 `core` 模組，包含我們的領域類別
2.  一個 `service` 模組，提供 REST APIs 服務
3.  一個 `webapp` 模組，包含所有 Web 所需的檔案

開始實作：

1.  建立一個空專案
    
    使用 `org.apache.maven.archetypes:maven-archetype-quickstart:1.0` 這個 `archetype` 建立專案：
    
    ```powershell
    mvn archetype:generate -B `
      '-DarchetypeCatalog=internal' `
      '-DgroupId=com.duotify' `
      '-DartifactId=parent-project' `
      '-Dversion=1.0-SNAPSHOT'
    ```
    
2.  手動調整 `parent-project/pom.xml` 的內容
    
    使用 VSCode 開啟專案
    
    ```powershell
    cd parent-project
    code .
    ```
    
    修改 `pom.xml` 的 `<packaing>` 設定，從 `jar` 改為 `pom`
    
    ```xml
    <packaging>pom</packaging>
    ```
    
    只要你將 `pom.xml` 的 `<packaing>` 設定為 `pom`，這個專案就會變成只有 POM 的專案，也自動成為所謂的 `Parent` 或 `Aggregator` 專案，更不會輸出任何 `artifact` 成品，因此該專案內的 `src` 資料夾也不重要了，可以砍掉。
    
    > 注意: 在 `Parent` 或 `Aggregator` 專案下的 POM 之後會自動繼承給所有的**子專案**或**子模組**使用。
    
3.  加入 Git 版控
    
    ```bash
    curl -sL 'https://www.gitignore.io/api/java,maven' > .gitignore
    git init
    git add .
    git commit -m 'Initial commit'
    ```
    
4.  建立 `3` 個子模組
    
    ```powershell
    mvn archetype:generate -B `
      '-DarchetypeCatalog=internal' `
      '-DgroupId=com.duotify' `
      '-DartifactId=core' `
      '-Dversion=1.0-SNAPSHOT' `
      '-DarchetypeGroupId=org.apache.maven.archetypes' `
      '-DarchetypeArtifactId=maven-archetype-quickstart' `
      '-DarchetypeVersion=1.4'
    
    mvn archetype:generate -B `
      '-DarchetypeCatalog=internal' `
      '-DgroupId=com.duotify' `
      '-DartifactId=service' `
      '-Dversion=1.0-SNAPSHOT' `
      '-DarchetypeGroupId=org.apache.maven.archetypes' `
      '-DarchetypeArtifactId=maven-archetype-quickstart' `
      '-DarchetypeVersion=1.4'
    
    mvn archetype:generate -B `
      '-DarchetypeCatalog=internal' `
      '-DgroupId=com.duotify' `
      '-DartifactId=webapp' `
      '-Dversion=1.0-SNAPSHOT' `
      '-DarchetypeGroupId=org.apache.maven.archetypes' `
      '-DarchetypeArtifactId=maven-archetype-quickstart' `
      '-DarchetypeVersion=1.4'
    ```
    
    這三個命令執行的過程 Maven 會自動幫你調整 Parent POM 的內容，加入一個 `<modules>` 區段，內容如下：
    
    ```xml
    <modules>
      <module>core</module>
      <module>service</module>
      <module>webapp</module>
    </modules>
    ```
    
    除此之外，這三個子模組的 `pom.xml` 也會自動加入 `<parent>` 區段，內容如下：
    
    ```xml
    <parent>
      <groupId>com.duotify</groupId>
      <artifactId>parent-project</artifactId>
      <version>1.0-SNAPSHOT</version>
    </parent>
    ```
    
    > 注意: 每個子模組只能有一個 `<parent>` 宣告，但我們可以匯入多個 [Maven BOM](https://www.baeldung.com/spring-maven-bom)。
    
5.  建置 `parent-project` 專案
    
    ```bash
    mvn package
    ```
    
    > 輸出結果中，有個生字叫做 [Reactor](https://stackoverflow.com/a/48332592/910074) (反應器)，像是 [Nuclear reactor](https://en.wikipedia.org/wiki/Nuclear_reactor) 就是「核子反應爐」的意思，代表你只要給他材料，他就會幫你反應生成一些結果。`Reactor` 是在 Maven 裡面的另一個專有名詞，也只有在建立 Multi-Module Project 的時候才會出現。`Reactor` 是一個抽象概念，他會幫你在**多個模組之間**進行**化學反應**，幫你融合多個模組，自動分析專案之間的相依性，並決定專案建置的順序，最後也能產生結果的摘要報告。
    
    如果我們**多個子模組之間**開始出現**相依關係**(Dependencies)，這個 `Reactor` 就會自動分析出正確的建置順序，確保可以依序執行建置作業。
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/a6db35571dc673c76775-189543099-3d523ee3-1b21-4a69-8fde-b793128a5245.png)
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/414a323669ed2e0072c0-189543114-b324e8f5-6c30-47d2-afe5-b3bbaf875b88.png)
    
6.  在 `parent-project` 專案的 `pom.xml` 啟用**相依管裡** (Dependency Management)
    
    這個 `<dependencyManagement>` 主要用在 Parent POM 裡面為主，意思是在這個 Parent POM 會幫你管裡套件相依性，所有的**子模組**(Child POM)就會自動繼承 Parent POM 的相依套件，所以子模組就不用特別再定義一次。當然，你還是可以在 Child POM 定義自己的 `<dependencies>` 相依套件，也可以指定不同的版本，這樣就可以覆蓋繼承的效果。
    
    ```xml
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.3.22</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    ```
    
    目前 Parent POM 的內容如下：
    
    ```xml
    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <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.duotify</groupId>
      <artifactId>parent-project</artifactId>
      <packaging>pom</packaging>
      <version>1.0-SNAPSHOT</version>
      <name>parent-project</name>
      <url>http://maven.apache.org</url>
    
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>3.8.1</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    
      <dependencyManagement>
        <dependencies>
          <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.3.22</version>
          </dependency>
        </dependencies>
      </dependencyManagement>
    
      <modules>
        <module>core</module>
        <module>service</module>
        <module>webapp</module>
      </modules>
    </project>
    ```
    
    如此一來，這個 `spring-core` 就都會預設繼承到所有子模組，你用 `mvn help:effective-pom` 就可以查出最終套用生效的 POM 內容。
    
7.  調整 `webapp` 模組，改用 `war` 格式來封裝應用程式
    
    先加入 `<packaging>` 設定，調整為 `war` 來封裝：
    
    ```xml
    <packaging>war</packaging>
    ```
    
    接著將以下 `<plugins>` 定義，加入到 `webapp` 模組的 `pom.xml` 檔的 `<build>` 設定區段內：
    
    ```xml
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>3.3.2</version>
        <configuration>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin>
    </plugins>
    ```
    
    > 由於 `war` 封裝有一些基本要求，例如 `*.war` 封裝檔中必須有個 `web.xml` 定義檔，如果沒有就會封裝失敗。我們可以透過 `<failOnMissingWebXml>false</failOnMissingWebXml>` 跳過這個檢查。
    
    然後再執行一次 `mvn package` 命令，你會發現 `webapp` 的輸出格式變成 `war` 了：
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f64d8cd3d82b073605c3-189544379-f2c4df69-9028-4276-a4ba-9e836c5f4cd4.png)
    

### 相關連結

-   [Multi-Module Project with Maven | Baeldung](https://www.baeldung.com/maven-multi-module)
-   [Maven - Guide to Working with Multiple Modules](https://maven.apache.org/guides/mini/guide-multiple-modules.html)
-   [Maven - Parent and Child POM Example - HowToDoInJava](https://howtodoinjava.com/maven/maven-parent-child-pom-example/)
