我在寫 .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
一起建置,整合的彈性還蠻大的。
將一個大專案拆解成多個小專案,優點還蠻多的,其中最明顯的效益就是:
- 專案變小,而且可以獨立開發、測試、部署,複雜度可控
- 增加程式碼的複用性(Reuseability),比較不會出現重複的程式碼
- 若專案採行 Monorepo 架構,比較容易實現跨組織的專案配置 (可搭配 Git Submodules 配置)
- 複雜的軟體架構下,也可以透過簡單的、抽象的
mvn
命令,輕鬆的建置專案
- 應用程式組態的部分,也可以透過
aggregator POM
一起管裡,管裡彈性大增
Maven 的 POM 支援繼承的特性,他可以透過 <parent>
元素定義 Parent POM 的 artifact
套件,因此你可以透過 Parent POM 來定義許多「共用」的 Properties
、Plugins
與 Dependencies
,這樣便可大幅簡化專案的 POM 定義檔內容。其實最經典的案例就是 Spring 專案,他就是透過 Parent POM 的定義,大幅降低專案 POM 的複雜度。
嘗試打造一個多模組專案
我原本是參考 Baeldung 的 Multi-Module Project with Maven 文章所述的步驟複刻一遍這個實作過程,不過該文章缺漏太多步驟,因此實作過程我有做了不少調整。
以下的例子將會建立 3 個子模組:
- 一個
core
模組,包含我們的領域類別
- 一個
service
模組,提供 REST APIs 服務
- 一個
webapp
模組,包含所有 Web 所需的檔案
開始實作:
-
建立一個空專案
使用 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'
-
手動調整 parent-project/pom.xml
的內容
使用 VSCode 開啟專案
cd parent-project
code .
修改 pom.xml
的 <packaing>
設定,從 jar
改為 pom
<packaging>pom</packaging>
只要你將 pom.xml
的 <packaing>
設定為 pom
,這個專案就會變成只有 POM 的專案,也自動成為所謂的 Parent
或 Aggregator
專案,更不會輸出任何 artifact
成品,因此該專案內的 src
資料夾也不重要了,可以砍掉。
注意: 在 Parent
或 Aggregator
專案下的 POM 之後會自動繼承給所有的子專案或子模組使用。
-
加入 Git 版控
curl -sL 'https://www.gitignore.io/api/java,maven' > .gitignore
git init
git add .
git commit -m 'Initial commit'
-
建立 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。
-
建置 parent-project
專案
mvn package
輸出結果中,有個生字叫做 Reactor (反應器),像是 Nuclear reactor 就是「核子反應爐」的意思,代表你只要給他材料,他就會幫你反應生成一些結果。Reactor
是在 Maven 裡面的另一個專有名詞,也只有在建立 Multi-Module Project 的時候才會出現。Reactor
是一個抽象概念,他會幫你在多個模組之間進行化學反應,幫你融合多個模組,自動分析專案之間的相依性,並決定專案建置的順序,最後也能產生結果的摘要報告。
如果我們多個子模組之間開始出現相依關係(Dependencies),這個 Reactor
就會自動分析出正確的建置順序,確保可以依序執行建置作業。
-
在 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 內容。
-
調整 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
了:
相關連結