The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何使用 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,例如:

<packaging>pom</packaging>

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

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

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

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

嘗試打造一個多模組專案

我原本是參考 Baeldung 的 Multi-Module Project with Maven 文章所述的步驟複刻一遍這個實作過程,不過該文章缺漏太多步驟,因此實作過程我有做了不少調整。

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

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

開始實作:

  1. 建立一個空專案

    使用 org.apache.maven.archetypes:maven-archetype-quickstart:1.0 這個 archetype 建立專案:

    mvn archetype:generate -B `
      '-DarchetypeCatalog=internal' `
      '-DgroupId=com.duotify' `
      '-DartifactId=parent-project' `
      '-Dversion=1.0-SNAPSHOT'
    
  2. 手動調整 parent-project/pom.xml 的內容

    使用 VSCode 開啟專案

    cd parent-project
    code .
    

    修改 pom.xml<packaing> 設定,從 jar 改為 pom

    <packaging>pom</packaging>
    

    只要你將 pom.xml<packaing> 設定為 pom,這個專案就會變成只有 POM 的專案,也自動成為所謂的 ParentAggregator 專案,更不會輸出任何 artifact 成品,因此該專案內的 src 資料夾也不重要了,可以砍掉。

    注意: 在 ParentAggregator 專案下的 POM 之後會自動繼承給所有的子專案子模組使用。

  3. 加入 Git 版控

    curl -sL 'https://www.gitignore.io/api/java,maven' > .gitignore
    git init
    git add .
    git commit -m 'Initial commit'
    
  4. 建立 3 個子模組

    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> 區段,內容如下:

    <modules>
      <module>core</module>
      <module>service</module>
      <module>webapp</module>
    </modules>
    

    除此之外,這三個子模組的 pom.xml 也會自動加入 <parent> 區段,內容如下:

    <parent>
      <groupId>com.duotify</groupId>
      <artifactId>parent-project</artifactId>
      <version>1.0-SNAPSHOT</version>
    </parent>
    

    注意: 每個子模組只能有一個 <parent> 宣告,但我們可以匯入多個 Maven BOM

  5. 建置 parent-project 專案

    mvn package
    

    輸出結果中,有個生字叫做 Reactor (反應器),像是 Nuclear reactor 就是「核子反應爐」的意思,代表你只要給他材料,他就會幫你反應生成一些結果。Reactor 是在 Maven 裡面的另一個專有名詞,也只有在建立 Multi-Module Project 的時候才會出現。Reactor 是一個抽象概念,他會幫你在多個模組之間進行化學反應,幫你融合多個模組,自動分析專案之間的相依性,並決定專案建置的順序,最後也能產生結果的摘要報告。

    如果我們多個子模組之間開始出現相依關係(Dependencies),這個 Reactor 就會自動分析出正確的建置順序,確保可以依序執行建置作業。

    image

    image

  6. parent-project 專案的 pom.xml 啟用相依管裡 (Dependency Management)

    這個 <dependencyManagement> 主要用在 Parent POM 裡面為主,意思是在這個 Parent POM 會幫你管裡套件相依性,所有的子模組(Child POM)就會自動繼承 Parent POM 的相依套件,所以子模組就不用特別再定義一次。當然,你還是可以在 Child POM 定義自己的 <dependencies> 相依套件,也可以指定不同的版本,這樣就可以覆蓋繼承的效果。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.3.22</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    

    目前 Parent POM 的內容如下:

    <?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 來封裝:

    <packaging>war</packaging>
    

    接著將以下 <plugins> 定義,加入到 webapp 模組的 pom.xml 檔的 <build> 設定區段內:

    <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

相關連結