Apache Maven 之所以強大,是因為他有一個強大的 Plugin 執行框架,你任何想讓 Maven 幫你完成的工作,無論是建置(Build)、封裝(Packaging)、產生報表(Reporting)、執行測試(Tests),全部都是透過 Plugins 完成的。它除了內建的核心 Plugins 之外,還有數以百計的第三方 Plugins 可以安裝使用。今天這篇文章我就來介紹一下他的基本架構與使用方式。
簡介 Apache Maven Plugins
基本上 Apache Maven 的 Plugins 分成兩大類,並且可以定義在專案的 pom.xml
檔案中:
-
建置外掛 (Build plugins)
用來執行在建置(build)過程中的所有大小事,他會定義在 POM 檔的 <build/>
元素底下。
-
報表外掛 (Reporting plugins)
用來執行在建立文件網站(site)過程中的所有大小事,他會定義在 POM 檔的 <reporting/>
元素底下。
由於報表外掛(Reporting plugins)的結果會被發佈在文件網站(site)上,所以必須考量多國語系(internationalized)與本地化(localized)的要求。詳見 Localization of Plugins 說明。
Apache Maven 官方支援許多 Plugins,並且詳細列在 Available Plugins 頁面中,光是這些內建的 Plugins 就可以幫我們解決 95%
以上的建置或報表工作,真的相當厲害且富有內涵,我很難在一篇文章內介紹這麼多 Plugins,只能淺淺帶過,有興趣大家可以個別研究不同的 Plugins。
突然覺得可以所有 Plugins 都介紹的話,那就可以參加 2022 iThome 鐵人賽 了吧!😅
以下我就列出這些官方支援的 Plugins 清單,大家可以看的大概,有點印象即可:
除了 Maven 官方支持的 Plugins 外,還有個 MojoHaus Maven Plugins Project 由社群維護了一系列好用的 Plugins,有空的時候去翻看看,搞不好有意外之喜。在這個 MojoHaus 的世界裡所稱呼的 Mojo
(Maven plain Old Java Object) 通常是指 Plugin 中的某個 Goal 實作,很多 Plugin 的 Goal 在實作的時候也會以 ___Mojo
結尾,來當成 Goal 的方法名稱,是一種 Maven plugins 常見的命名規則。我們其實經常會在許多文件中看到這個單字,而這個 Mojo
單字有「魔力」、「魅力」等含意!
認識 Maven 的建置生命週期(lifecycle
)、階段(phase
)與目標(goal
)
在 Maven 裡面有許多「抽象概念」經常會混淆著初學者,因為你只要沒有認真看懂這些概念,根本就無法好好的使用 Maven 這套優異的建置工具。
首先,你要先知道 Maven 定義了三種不同的生命週期,彼此相關而不重複:
-
default
處理專案的建置(build)與部署(deployment)等工作
-
clean
處理專案的清理工作,將建置過程中產生的檔案清除
-
site
處理專案文件網站的產生工作
你從官網的 Lifecycles Reference 文件可以發現,每個不同的生命週期(lifecycle
)分別定義了一系列依序執行的階段(phase
),這些階段是 Maven 依據過往數十年的最佳實務整理出來的寶貴經驗。
請注意,這些所謂的「生命週期」與「階段」都只是個「概念」而已,所代表意義是:
-
當你想要建置與部署時 (default
),你必須依據 Maven 所定義的階段來執行任務
<phases>
<phase>validate</phase>
<phase>initialize</phase>
<phase>generate-sources</phase>
<phase>process-sources</phase>
<phase>generate-resources</phase>
<phase>process-resources</phase>
<phase>compile</phase>
<phase>process-classes</phase>
<phase>generate-test-sources</phase>
<phase>process-test-sources</phase>
<phase>generate-test-resources</phase>
<phase>process-test-resources</phase>
<phase>test-compile</phase>
<phase>process-test-classes</phase>
<phase>test</phase>
<phase>prepare-package</phase>
<phase>package</phase>
<phase>pre-integration-test</phase>
<phase>integration-test</phase>
<phase>post-integration-test</phase>
<phase>verify</phase>
<phase>install</phase>
<phase>deploy</phase>
</phases>
-
當你想要清理時 (clean
),你必須依據 Maven 所定義的階段來執行任務
<phases>
<phase>pre-clean</phase>
<phase>clean</phase>
<phase>post-clean</phase>
</phases>
<default-phases>
<clean>
org.apache.maven.plugins:maven-clean-plugin:2.5:clean
</clean>
</default-phases>
-
當你想要建立文件網站時 (site
),你必須依據 Maven 所定義的階段來執行任務
<phases>
<phase>pre-site</phase>
<phase>site</phase>
<phase>post-site</phase>
<phase>site-deploy</phase>
</phases>
<default-phases>
<site>
org.apache.maven.plugins:maven-site-plugin:3.3:site
</site>
<site-deploy>
org.apache.maven.plugins:maven-site-plugin:3.3:deploy
</site-deploy>
</default-phases>
請注意: 所有的階段(phase
)其名稱都是不重複的。
瞭解了這些生命週期與階段的概念之後,你就必須知道,所有的 Java 專案都可以依據實際的需求來定義特定階段(phase
)要執行什麼樣的目標(goal
)!
所以,生命週期(lifecycle
)與階段(phase
)就只是個概念(Concepts),而目標(goal
)才是你真正要做的事情 (要達成的目標)!
我用一個非常簡單的例子來說明:
-
我們用以下命令建立一個全新的 Java 專案
mvn archetype:generate -B `
'-DarchetypeCatalog=internal' `
'-DgroupId=com.duotify' `
'-DartifactId=demo1' `
'-Dversion=1.0-SNAPSHOT'
由於 Maven 預設會採用 org.apache.maven.archetypes:maven-archetype-quickstart:1.0
這個專案範本(archetype
),所以不用寫底下這段較長的命令:
mvn archetype:generate -B `
'-DarchetypeCatalog=internal' `
'-DgroupId=com.duotify' `
'-DartifactId=demo1' `
'-Dversion=1.0-SNAPSHOT' `
'-DarchetypeGroupId=org.apache.maven.archetypes' `
'-DarchetypeArtifactId=maven-archetype-quickstart' `
'-DarchetypeVersion=1.0'
這只是一個非常簡單的 Console 專案:
其 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.duotify</groupId>
<artifactId>demo1</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>demo1</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>
</project>
-
然後我們直接執行 mvn
命令,並獲取以下錯誤訊息
[INFO] Total time: 0.133 s
[INFO] Finished at: 2022-09-11T18:04:06+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] No goals have been specified for this build. You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>. Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/NoGoalSpecifiedException
這裡的重點在這段:
No goals
have been specified for this build. You must specify a valid lifecycle phase
or a goal
in the format <plugin-prefix>:<goal>
or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>
. Available lifecycle phases are: validate
, initialize
, generate-sources
, process-sources
, generate-resources
, process-resources
, compile
, process-classes
, generate-test-sources
, process-test-sources
, generate-test-resources
, process-test-resources
, test-compile
, process-test-classes
, test
, prepare-package
, package
, pre-integration-test
, integration-test
, post-integration-test
, verify
, install
, deploy
, pre-clean
, clean
, post-clean
, pre-site
, site
, post-site
, site-deploy
.
這段英文應該不難理解,你必須在 mvn
命令後面指定一個 phase
或 goal
才能執行 Maven!
-
假設我們只要編譯目前的專案,那我們可以指定 compile
這個階段
mvn compile
上述命令執行時會掛掉,容後補充說明。
由於這個 compile
階段隸屬於 default
生命週期,因此 Maven 就會從 default
生命週期的第一個 phase
開始找尋是否有相對應的目標(goal
)需要達成。
所以 Maven 會依序去查找 validate
, initialize
, generate-sources
, process-sources
, generate-resources
, process-resources
, compile
這幾個階段是否有相對應的目標(goal
)去執行程式。
基本上,整個 Maven 就是這樣跑起來的!👍
此時重點就要登場了,所以我們所說的目標(goal
)具體來說到底是什麼呢?
理解 Plugin 與 Goal 之間的關係
事實上,我們註冊到 pom.xml
的每一個 Plugins,裡面分別都「實作」了一個到多個目標(goals
),而這些 Plugin 裡面的目標(goals
)會明確綁定到特定階段(phase
),當 Maven 執行到該階段(phase
)的時候,就會自動執行這個 Plugin 的目標(goal
)。說穿了,所謂的目標(goal
)就是一個類別中的方法而已。
一般來說,在 Plugin 裡面的目標名稱(goal name
)經常會跟階段名稱(phase name
)刻意設計的一樣,這樣也比較好辨識,但這並不是必要條件。
我以 Apache Maven Compiler Plugin 為例,這個 Plugin 實作了 3 個目標:
-
compiler:compile
用來綁定 compile
phase 並用來編譯 Java 主程式的原始碼(src/main/**
)。
-
compiler:testCompile
用來綁定 test-compile
phase 並用來編譯 Java 測試程式的原始碼(src/test/**
)。
-
compiler:help
用來顯示這個 compiler
plugin 的使用說明。
幾乎所有 Plugin 都會實作 help
這個目標(goal
),方便開發人員查詢相關用法。你可以執行 mvn compiler:help
查詢相關用法。
有趣的地方在於,在官網的 Plugin Documentation 文件中,並沒有明確跟你說這些 Goal 要怎樣使用、用在哪裡,這些細節已經被定義在 Maven 的生命週期(lifecycle
)、階段(phase
)與目標(goal
)之中了。因此你若沒有先建立好完整且清晰的概念,就會無法理解 Maven 的運作原理。
我們接續上一段的例子,當我們執行 mvn compile
命令的時候,實際上完整的流程如下:
-
先選中 compile
階段(phase
),藉此判斷出 default
這個生命週期(lifecycle
)
-
依序執行 default
生命週期(lifecycle
)的每個階段(phase
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
validate
階段(phase
)的目標(goal
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
initialize
階段(phase
)的目標(goal
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
generate-sources
階段(phase
)的目標(goal
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
process-sources
階段(phase
)的目標(goal
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
generate-resources
階段(phase
)的目標(goal
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
process-resources
階段(phase
)的目標(goal
)
- 找尋所有 Plugins 中是否有實作並綁定任何名稱為
compile
階段(phase
)的目標(goal
)
如此一來 mvn compile
命令便執行完畢!
由於 compile
plugin 是 Maven 的核心 plugins 之一,所以預設就會註冊在 pom.xml
之中,不用特別設定,而上述過程只有 compile
這個目標(goal
)有被比對中,因此到 compile
這個階段(phase
)的時候,就會跑去執行 compile:compile
目標(goal
)。
如果我直接執行 mvn compile:compile
的話,就代表我們執行的是 compile
這個 plugin 的 compile
目標而已,不會去執行其他階段(phase),因此也不會去執行其他 Plugins 中的任何目標。
所以 mvn compile
與 mvn compile:compile
所代表的意義是截然不同的!
解決無法編譯的問題
我先前有提到,這個範例專案其實無法編譯,你在執行 mvn compile
的時候會發現以下訊息:
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< com.duotify:demo1 >--------------------------
[INFO] Building demo1 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ demo1 ---
[WARNING] Using platform encoding (MS950 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory G:\Projects\demo1\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ demo1 ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding MS950, i.e. build is platform dependent!
[INFO] Compiling 1 source file to G:\Projects\demo1\target\classes
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] Source option 5 is no longer supported. Use 7 or later.
[ERROR] Target option 5 is no longer supported. Use 7 or later.
[INFO] 2 errors
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.991 s
G:\Projects\demo1>mvn compile:compile
[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-metadata.xml
Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/mojo/maven-metadata.xml
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-metadata.xml (14 kB at 14 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/mojo/maven-metadata.xml (21 kB at 21 kB/s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.733 s
[INFO] Finished at: 2022-09-11T18:40:45+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] No plugin found for prefix 'compile' in the current project and in the plugin groups [org.apache.maven.plugins, org.codehaus.mojo] available from the repositories [local (C:\Users\wakau\.m2\repository), central (https://repo.maven.apache.org/maven2)] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/NoPluginFoundForPrefixException
你可以從上述訊息得知,其實這個 mvn compile
在執行的過程中,執行了 2
個目標:
maven-resources-plugin:2.6:resources
maven-compiler-plugin:3.1:compile
而在執行 maven-compiler-plugin:3.1:compile
目標時,卻發生了以下錯誤:
[ERROR] Source option 5 is no longer supported. Use 7 or later.
[ERROR] Target option 5 is no longer supported. Use 7 or later.
首先,看起來 Maven 內建的 maven-compiler-plugin
選用了 3.1
版本有點過於老舊,他預設使用 Java 5
來編譯原始碼,但我的電腦裝的是 JDK 17
,編譯器最低支援版本從 Java 7
開始支援,所以才會編譯失敗。
要解決這個問題其實很簡單,只要在專案的 pom.xml
加入這個 maven-compiler-plugin
plugin 並提供選項設定即可。
我打算加入以下 XML 片段到 pom.xml
檔案中,藉此指定最新版的 maven-compiler-plugin
plugin:
<project>
...
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
</plugin>
</plugins>
</build>
...
</project>
再重新執行一次 mvn compile
就會發現專案已經可以成功建置!
其實為了讓建置過程可以更穩定,一般來說我們會明確指定各個 Plugins 的版本資訊(<version>3.10.1</version>
),否則當 Plugin 版本變化時,若是遇到破壞性更新(Breaking changes)那就遭了!
除了指定 maven-compiler-plugin
plugin 到最新版本外,你也可以透過 plugin 的 <configuration>
來指定參數,讓 maven-compiler-plugin
可以選用 1.7
當成我們的原始碼版本,這樣也能讓專案順利編譯成功!
<project>
...
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
...
</project>
再重新執行一次 mvn compile
或 mvn clean compile
也會發現專案依然可以成功建置!
注意: 執行 mvn clean compile
命令意味著先執行 clean
階段,再執行 compile
階段!
除了上述兩種方法外,還有一種直接透過 CLI 傳入 -D
指定 maven.compiler.source
與 maven.compiler.target
屬性的用法,也可以在執行時動態改變編譯器版本設定,詳見 compiler:compile 的 Optional Parameters 說明:
mvn clean compile '-Dmaven.compiler.source=8' '-Dmaven.compiler.target=8'
這個方法也可以通過編譯!
將應用程式封裝成 JAR 檔
最後,我想在多介紹 Apache Maven JAR Plugin,如果我們想要把應用程式打包成 JAR 檔,就需要用到這個內建的 plugin,以本文的範例來說,還需要對這個 plugin 做出一些調整才行。
你至少需要加入以下設定到 pom.xml
的 <project><build><plugins>
底下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>com.duotify.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
注意: 其實 <groupId>org.apache.maven.plugins</groupId>
是可以省略的,因為 org.apache.maven.plugins
這個 groupId
是 Maven 的預設值。
然後執行以下 mvn clean package
命令即可產生 target\demo1-1.0-SNAPSHOT.jar
檔:
mvn clean package '-Dmaven.compiler.source=8' '-Dmaven.compiler.target=8'
接著就可以用以下命令直接測試執行:
java -jar target/demo1-1.0-SNAPSHOT.jar
總結
有了上述這個完整的例子,我相信你應該已經學會基本的 Maven Plugins 的運作架構與基本設定方法!👍
另外,你可以試試執行以下命令,他可以幫你找出你目前專案最終生效的 POM 檔完整內容,包含那些 Maven 內建的 POM 設定值,都會出現在執行結果中:
mvn help:effective-pom
上述命令事實上是執行 help
plugin 的 effective-pom
目標(goal
)!
你還可以利用以下命令,找出特定一個階段(phase
)是哪些 plugins 有綁定目標(goal
)
mvn help:describe -Dcmd=compile
他會顯示相當清楚的結果:
[INFO] 'compile' is a phase corresponding to this plugin:
org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
It is a part of the lifecycle for the POM packaging 'jar'. This lifecycle includes the following phases:
* validate: Not defined
* initialize: Not defined
* generate-sources: Not defined
* process-sources: Not defined
* generate-resources: Not defined
* process-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:resources
* compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
* process-classes: Not defined
* generate-test-sources: Not defined
* process-test-sources: Not defined
* generate-test-resources: Not defined
* process-test-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
* test-compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
* process-test-classes: Not defined
* test: org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
* prepare-package: Not defined
* package: org.apache.maven.plugins:maven-jar-plugin:2.4:jar
* pre-integration-test: Not defined
* integration-test: Not defined
* post-integration-test: Not defined
* verify: Not defined
* install: org.apache.maven.plugins:maven-install-plugin:2.4:install
* deploy: org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
相關連結