我們最近有個新的 Java 專案,客戶提供原始碼之後,卻發現他們自己架設的 Maven Repository 並沒有開啟防火牆讓我們連線,以致於專案無法下載相依套件而無法建置。但除了開防火牆連線外,其實還有很多方法可以讓你獲取 Maven 所需的相依套件。今天這篇文章,我就來分享關於 Maven 如何管裡相依套件,以及如何正確的將私有的第三方 JAR 檔加到專案內,降低團隊取得這些 JAR 檔的門檻。

其實我們已經不是只有一次需要將第三方的 JAR 檔(3rd-party JARs)加入到 Maven 的相依套件中,但是這種想法其實跟 Apache Maven 希望達到的目的有一點點衝突。
如果直接使用 IntelliJ IDEA、Eclipse 或 VSCode 內建的 Build system 來建置專案,要加入一個 JAR 檔到專案中其實非常容易。然而就算你不用任何 Build tools 也可以在透過 javac 編譯 Java 程式時加入 -classpath 參數,直接參考特定的 JAR 檔也很基本。但是當你用了 Apache Maven 來幫你管裡相依套件後,它把 Project 與 JAR file 之間做了一層抽象,透過一種所謂 Dependency (相依套件) 的概念來串聯兩者之間,你必須理解這個概念,才能很清楚的知道如何更輕易的管裡相依關係。
當你在使用 Apache Maven 的時候,它有一套相當完整的 Dependency Mechanism (相依管裡機制),今天這篇文章不打算深入相依管理的細節,有興趣請自行閱讀文件。
這篇文章的主軸,是從一個很簡單的需求出發:我有一個私有的 JAR 檔,希望加入一個由 Apache Maven 管理的 Java 專案中!
因此,你必須先瞭解何謂 Repositories 或 Artifact Repositories!
理解 Artifact Repositories 的概念
在瞭解 Artifact Repositories 之前,我們先來認識幾個抽象的概念與專有名詞。
我在第一次接觸 Apache Maven 的時候,就是學寫 Spring Boot 的時候,透過 Spring Initializr 建立新專案的時候一定要輸入 groupId、artifactId 這兩個參數,我問了好幾個人,沒有人可以把這兩個概念解釋的很清楚,就只知道要設定這兩個「參數」而已。這種知其然而不知所以然的狀態,會讓我十分困擾。因為我學習任何新事物都一定要「連結」某個既有概念,即便當下對觀念的理解不夠全面,還是要先連結起來輔助我思考,待日後有更深刻的理解時,還可以逐步導正觀念。
以下我就來說說這幾個重要的參數所代表的意義:
-
groupId
這裡的 group 有群組的意思,也可以理解為一個團體,更可以理解為一個組織。而每個專案都會隸屬於一個組織,或是一個組織可以包含多個專案,這是個「一對多」關係,我想這部分非常容易理解。
一般來說這個 groupId 會取名成一個組織的 FQDN 域名,例如我們公司的網址是 duotify.com,所以公司層級的專案,就會取名為 com.duotify 當成專案的 groupId。如果是公司內部 isms 團隊的專案,我們的 groupId 就會取名為 com.duotify.isms,以此類推。
我們再以 org.apache.maven.plugins 這個 groupId 為例,這個名字所代表的含意是:所有使用這個 groupId 的專案,全部都歸類在 org.apache 組織下的 maven 計畫中的所有 plugins 專案」
-
artifactId
這裡的 artifact 有產出物的意思,也有人工製品的意思,其代表的真實含意是手工打造的物品。這個抽象的概念,其實就是我在文章中經常提到的專案(Project)。你所親手打造的專案,最終的產出物就是 Artifact。
每個專案都會有個名稱,而這個名稱就叫做 artifactId!
以 Java 專案而言,無論你是一個 Class Library 或是 Spring Boot 專案,通常專案的最終產出物通常是一個 JAR 檔 (*.jar) 或 WAR 檔 (*.war)。然而,一個專案可能會產生不只一個產出物,因此你還有可能產出含有原始碼的 JAR 檔,其檔名通常也會包含 artifactId 所標示的名稱。
-
version
我們在一個組織(groupId)下,可能會發展多個專案(artifactId),而每個專案可能會產出多個版本(version),因此 groupId + artifactId + version 就會自然組成一個全世界唯一的套件名稱。
通常一個由 Maven 產出的 Artifact 的檔名通常是 <artifactId>-<version>.<extension> 這種格式,例如:myapp-1.0.jar
如果我們把焦點拉回這個段落的主軸,什麼是 Artifact Repositories 呢?就是一個可以用來儲存 Artifact (專案產出物) 的儲存庫。
理解 Artifact 與 Project 之間的關係
我們現在知道 Artifact 就是 Project 的產出物,因此我們開發 Spring Boot 的時候,最終產生的 JAR 檔,就是該 Project 的 Artifact 而已。
我們在一個 Spring Boot 專案中所使用的 Dependency (相依套件),說穿了,也就是你在使用其他 Project 所產生的 Artifact (產出物) 而已。
理解了這個概念之後,你就要重新思考一遍,什麼叫做 Dependency (相依套件):
所謂 Dependency 就是你需要使用到另一個專案的產出物來幫助你在這個專案完成任務,但你不用管那個專案的實作細節,也不用管那個專案如何封裝產出物,你只需要很簡單的知道對方的 groupId + artifactId + version 就可以取用其結果,而這就是「抽象化」的過程,你必須理解這個過程,這整件事才不會是「抽象」的,也才能舉一反三、靈活運用。
重新理解 Artifact Repositories 的細節
我們現在知道 Artifact Repositories 就是一個可以用來儲存 Artifact (專案產出物) 的儲存庫,跟 Git Repository 一樣,他也是以檔案的形式來儲存這些內容,他其實就是一個資料庫,除了用來儲存產出物之外,還會保存一些重要的 Metadata,幫助 Maven 取得關鍵資訊。
從 Introduction to Repositories 文件中有提到一段話:
A repository in Maven holds build artifacts and dependencies of varying types.
他說在 Maven 之中,一個「儲存庫」會用來保存不同類型的 build artifacts (建置成果) 與 dependencies (相依套件),而且主要有兩種類型:
-
local (本地儲存庫)
儲存在本機的 Maven Repository,通常位於 ~/.m2/repository/ 目錄下,你可以把他視為是一種遠端儲存庫的快取版本,因為所有從遠端儲存庫下載的 artifacts 最終都會儲存在本地儲存庫之中,也就是存在 ~/.m2/repository/ 目錄下。
-
remote (遠端儲存庫)
有別於本地儲存庫,所謂的遠端儲存庫其實是一個「相對」的概念,他就是一個儲存在「遠端」的儲存庫而已,任何使用 file:// 或 https:// 開頭的協定,都可以被歸類在「遠端儲存庫」這個類別。所以你可以把所謂的遠端儲存庫部署在任何以 HTTP / HTTPS 為主的任意網站上,可以在網際網路上,也可以部署在公司內網。你甚至可以不需要架設 Web 伺服器,直接用 NAS 就可以架設一套公司內部用的遠端儲存庫來用,只要使用 file:// 為網址就行。
基本上 local (本地儲存庫) 與 remote (遠端儲存庫) 保存 Artifacts 的目錄結構與格式是完全相同的,大家也可以自由進入 ~/.m2/repository/ 資料夾查看有哪些檔案,其實都是非常公開透明的。
將私有的 JAR 檔加入到 Maven 本地儲存庫
我們假設一下這個情境,我們有個 C:\Program Files\Java\jdk1.6.0_43\jre\lib\plugin.jar 第三方 JAR 檔,由於你的 Java 專案需要在編譯的時候用到該 JAR 檔中的某些類別,你並沒有該檔案的原始碼,但可以理解這個 plugin.jar 其實就是一個 artifact (產出物),而且編譯的時候只要將該檔案加入 -classpath 就可以成功編譯。
此時你想把該 JAR 檔加入到專案中,但是該專案採用 Apache Maven 來管裡所有的相依套件(Dependencies),因此我們必須對該 JAR 檔進行一層抽象化的封裝,以便使用 Maven 的相依管裡機制。
以往,你的專案跟 JAR 檔的相依關係會是緊密耦合的,像這樣:
Your Project <---> Artifact (JAR)
但是透過 Maven 來管裡相依套件的話,你的專案與 JAR 檔的相依關係會變成以下這樣:
Your Project <---> Maven (pom.xml) <---> Dependencies <---> Artifact (JAR)
請記得幾個重點:
- Maven 的資訊中心就是
pom.xml 檔
- Maven 的相依資訊被紀錄在
pom.xml 檔的 <dependencies> 區段內
- Maven 的
pom.xml 看不到任何 *.jar 的資訊,因為被 Dependencies 隔開了 (抽象化),因此打斷了 Project 與 JAR 的直接相依關係。
還好 Maven 要把一個既有的 JAR 檔封裝成一個 Maven Repository 的 Artifact 非常容易,只要一個命令就可以完成:
mvn install:install-file -Dfile="C:\Program Files\Java\jdk1.6.0_43\jre\lib\plugin.jar" "-DgroupId=com.duotify" "-DartifactId=jre6-plugin" "-Dversion=1.0.0" "-Dpackaging=jar" "-DcreateChecksum=true"
這個命令是直接將 C:\Program Files\Java\jdk1.6.0_43\jre\lib\plugin.jar 封裝成一個 groupId 為 com.duotify、artifactId 為 jre6-plugin 與 version 為 1.0.0 且封裝類型為 jar 的 artifact,並且在不指定 Maven Repository 的情況下,預設就會安裝到 Maven 的本地儲存庫(local repository),也就是 ~/.m2/repository/ 這個目錄!
當你的 Maven 本地儲存庫已經擁有 jre6-plugin-1.0.0 (com.duotify) 這個相依套件,你就可以在 pom.xml 檔案中加入以下相依設定,就可以自動參考進來,建置專案的時候 Maven 也會自動將該套件的 jre6-plugin-1.0.0.jar 檔加入到 javac 編譯時的 -classpath 之中:
<dependencies>
<dependency>
<groupId>com.duotify</groupId>
<artifactId>jre6-plugin</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
而這,就是 Maven 相依管裡的魅力! 👍
如我們文章一開始說的,如果客戶目前的開發環境會從他們公司自架的 Maven Repository 下載套件,其實也會安裝到本地的 Maven Repository 才對 (~/.m2/repository/),所以客戶大可將他自己的 ~/.m2/repository/ 資料夾整個壓縮起來傳給我們,我們只要解壓縮到開發人員本機的 ~/.m2/repository/ 目錄,就可以取得完整的相依套件,就可以在本地開發了。但是你必須用「離線」的方式執行 mvn 命令,加上一個 -o 參數即可。例如:
mvn -o package
將私有的 JAR 檔加入到 Maven 專案儲存庫
上述作法只能把 JAR 檔安裝到「本地儲存庫」之中,然而如果你有一整個開發團隊,每個人都需要用到這個 plugin.jar 的話,那麼每個人都需要手動安裝一遍,相當不方便。不過再怎麼不方便,其實一台電腦也只需要跑一次而已,影響不會太大,文件寫清楚也可以。
另一種解決方法,就是直接在「專案」內直接建立好一個資料夾 (假設為 libs 資料夾),並把這個資料夾視為一個「遠端儲存庫」來用,然後你可以將 plugin.jar 這個透過 mvn install:install-file 命令,自動將其加入到我稱做「專案儲存庫」的地方,你只要記得將該目錄加入版控即可。以下是執行命令範例:
mvn install:install-file -Dfile="C:\Program Files\Java\jdk1.6.0_43\jre\lib\plugin.jar" "-DgroupId=com.duotify" "-DartifactId=jre6-plugin" "-Dversion=1.0.0" "-Dpackaging=jar" "-DcreateChecksum=true" "-DlocalRepositoryPath=libs"
這裡的 -DlocalRepositoryPath=libs 就是用來將 artifact 安裝到 libs 專案儲存庫之中!
當你在專案的 libs 建立一個「遠端儲存庫」且已擁有 jre6-plugin-1.0.0 (com.duotify) 這個相依套件,你就可以在 pom.xml 檔案中加入 <repositories> 設定,明確指定遠端 Maven Repository 的位址:
<repositories>
<repository>
<id>ProjectRepo</id>
<name>ProjectRepo</name>
<url>file:///${project.basedir}/libs</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.duotify</groupId>
<artifactId>jre6-plugin</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
如此一來,就不會有人遇到抓不到 JAR 檔的問題了! 👍
相關連結