當前位置: 妍妍網 > 碼農

四十五圖,一萬五千字!一文讓你走出迷霧玩轉Maven!

2024-02-23碼農

引言

Maven 應該是大家的老熟客了,身為 Java 程式設計師,幾乎每天都會跟他打交道。

不過有趣的是: 很多夥伴對 Maven ,似乎很熟,但又好像不熟;在理解上,處於似懂非懂的「量子纏結態」 ,為什麽這麽說呢?原因很簡單,要說不熟吧,偏偏每天都有所接觸;要說熟吧,可是對許多高級功能又僅是一知半解。

正因如此,為了輔助大家從「量子纏結態」中走出來,本文會從零開始,帶著大家玩轉 Maven 技術。當然,其實寫這篇文章更大的目的,是為後續寫 【漫談分布式】 專欄做準備,畢竟後續會頻繁用到 Maven 構建多工程計畫。

一、Maven快速上手/回顧

1.1、Maven工作原理剖析

Maven 中,節點會分為工程、倉庫兩大類,工程是「依賴使用者」,倉庫是「依賴提供者」,關系如下:

倉庫/工程關系

看著或許有點頭大,要講明白得先弄清裏面三種倉庫:

  • • 中央倉庫:就是前面配置的映像源,裏面擁有海量的公共 jar 包資源;

  • • 遠端倉庫:也叫私服倉庫,主要儲存公司內部的 jar 包資源,這個後續會細說;

  • • 本地倉庫:自己電腦原生的倉庫,會在磁盤上儲存 jar 包資源。

  • 大致了解三種倉庫的含義後,接著來梳理 Maven 的工作流程:

  • • ①計畫透過 GAV 座標引入依賴,首先會去本地倉庫尋找 jar 包;

  • • ②如果在本地倉庫中找到了,直接把依賴載入到當前工程的 External Libraries 中;

  • • ③如果沒找到,則去讀取 settings.xml 檔,判斷是否存在私服配置;

  • • ④如果有私服配置,根據配置的地址找到遠端倉庫,接著拉取依賴到本地倉庫;

  • • ⑤如果遠端倉庫中沒有依賴,根據私服配置去中央倉庫拉取,然後放到私服、本地倉庫;

  • • ⑥從遠端或中央倉庫中,把依賴下載到本地後,再重復第二步,把依賴載入到計畫中。

  • 上述六步便是 Maven 的完整工作流程,可能許多人沒接觸過私服,這個會放到後面聊。如果你的計畫沒配置 Maven 私服,那麽第三步時,會直接從 settings.xml 讀取映像源配置,直接去到中央倉庫拉取依賴。

    不過這裏有個問題,拉取/引入依賴時, Maven 是怎麽知道要找誰呢?答案是依靠 GAV 座標,大家可以去觀察一下本地倉庫,當你引入一個依賴後,本地倉庫中的目錄,會跟你的 GAV 座標一一對應,如:

    倉庫結構

    無論是什麽型別的倉庫,都會遵循這個原則進行構建,所以,只要你書寫了正確的 GAV 座標,就一定能夠找到所需的依賴,並將其載入到計畫中。

    1.2、Maven生命周期

    透過 IDEA 工具的輔助,能很輕易看見 Maven 的九種 Lifecycle 命令,如下:

    構建流程

    雙擊其中任何一個,都會執行相應的 Maven 構建動作,為啥 IDEA 能實作這個功能呢?道理很簡單,因為 IDEA 封裝了 Maven 提供的命令,如:點選圖中的 clean ,本質是在當前目錄中,執行了 mvn clean 命令,下面解釋一下每個命令的作用:

  • clean :清除當前工程編譯後生成的檔(即刪除 target 整個目錄);

  • validate :對工程進行基礎驗證,如工程結構、 pom 、資原始檔等是否正確;

  • compile :對 src/main/java 目錄下的源碼進行編譯(會生成 target 目錄);

  • test :編譯並執行 src/test/java/ 目錄下的所有測試用例;

  • package :將當前計畫打包,普通計畫打 jar 包, webapp 計畫打 war 包;

  • verify :驗證工程所有程式碼、配置進行是否正確,如類中程式碼的語法檢測等;

  • install :將當前工程打包,然後安裝到本地倉庫,別人可透過 GAV 匯入;

  • site :生成計畫的概述、源碼測試覆蓋率、開發者列表等站點文件(需要額外配置);

  • deploy :將當前工程對應的包,上傳到遠端倉庫,提供給他人使用(私服會用)。

  • 上述便是九個周期階段命令的釋義,而 Maven 總共劃分了三套生命周期:

    生命周期

    主要看 default 這套,該生命周期涵蓋了構建過程中的檢測、編譯、測試、打包、驗證、安裝、部署每個階段。註意一點: 同一生命周期內,執行後面的命令,前面的所有命令會自動執行 !比如現在執行一條命令:

    mvn test

    test 命令位於 default 這個生命周期內,所以它會先執行 validate、compile 這兩個階段,然後才會真正執行 test 階段。同時,還可以一起執行多個命令,如:

    mvn clean install

    這兩個命令隸屬於不同的周期,所以會這樣執行:先執行 clean 周期裏的 pre-clean、clean ,再執行 default 周期中, validate~install 這個閉區間內的所有階段。

    從上面不難發現, default Maven 的核心周期,但其實上面並沒有給完整,因為官方定義的 default 一共包含 23 個小階段,上面的圖只列出了七個核心周期,對詳細階段感興趣的可以自行了解。

    Maven 中只定義了三套生命周期,以及每套周期會包含哪些階段,而每個階段具體執行的操作,這會交給外掛程式去幹,也就是說:** Maven 外掛程式會實作生命周期中的每個階段**,這也是大家為什麽看到 IDEA Lifecycle 下面,還會有個 Plugins 的原因:

    Maven外掛程式

    當你雙擊 Lifecycle 中的某個生命周期階段,實際會呼叫 Plugins 中對應的外掛程式。在 Shell 視窗執行 mvn 命令時,亦是如此,因為外掛程式對應的實作包,都會以 jar 包形式儲存在本地倉柯瑞。

    你有特殊的需求,也可以在 pom.xml <build> 標簽中,依靠 <plugins> 外掛程式來匯入。

    二、Maven進階操作

    上面所說到的一些知識,僅僅只是 Maven 的基本操作,而它作為 Java 計畫管理占有率最高的工具,還提供了一系列高階功能,例如內容管理、多模組開發、聚合工程等,不過這裏先來說說依賴沖突。

    2.1、依賴沖突

    依賴沖突是指: Maven 計畫中,當多個依賴包,引入了同一份類別庫的不同版本時,可能會導致編譯錯誤或執行時異常 。這種情況下,想要解決依賴沖突,可以靠升級/降級某些依賴項的版本,從而讓不同依賴引入的同一類別庫,保持一致的版本號。

    另外,還可以透過隱藏依賴、或者排除特定的依賴項來解決問題。但是想搞明白這些,首先得理解 Maven 中的依賴傳遞性,一起來看看。

    2.1.1、依賴的傳遞性

    先來看個例子:

    依賴層級

    目前的工程中,僅匯入了一個 spring-web 依賴,可是從下面的依賴樹來看, web 還間接依賴於 beans、core 包,而 core 包又依賴於 jcl 包,此時就出現了依賴傳遞,所謂的依賴傳遞是指: 當引入的一個包,如果依賴於其他包(類別庫),當前的工程就必須再把其他包引入進來

    這相當於無限套娃,而這類「套娃」引入的包,被稱為間接性依賴。與之對應的是直接性依賴,即: 當前工程的 pom.xml 中,直接透過 GAV 座標引入的包 。既然如此,那麽一個工程內的依賴包,就必然會出現層級,如:

    boot-test依賴

    在這裏我們僅引入了一個 boot-test 座標,但當開啟依賴樹時,會發現這一個包,依賴於其他許多包,而它所依賴的包又依賴於其他包……,如此不斷套娃,最深套到了五層。而不同的包,根據自己所處的層級不同,會被劃分為 1、2、3、4…… 級。

    2.1.2、自動解決沖突問題

    Maven 作為 Apache 旗下的產品,而且還經過這麽多個版本叠代,對於依賴沖突問題,難道官方想不到嗎?必然想到了,所以在絕對大多數情況下,依賴沖突問題並不需要我們考慮, Maven 工具會自動解決,怎麽解決的呢?就是基於前面所說的依賴層級,下面來詳細說說。

    ①層級優先原則 Maven 會根據依賴樹的層級,來自動剔除相同的包,層級越淺,優先級越高。這是啥意思呢?同樣來看個例子:

    層級優先

    我們又透過 GAV 座標匯入了 spring-web 包,根據前面所說, web 依賴於 beans、core 包,而 beans 包又依賴於 core 包,此時註意,這裏出現了兩個 core 包,前者的層級為 2 ,後者的層級為 3 ,所以 Maven 會自動將後者剔除,這點從圖中也可明顯看出,層級為 3 core 直接變灰了。

    ②聲明優先原則 ,上條原則是基於層級深度,來自動剔除沖突的依賴,那假設同級出現兩個相同的依賴怎麽辦?來看例子:

    聲明優先

    此時用 GAV 引入了 web、jdbc 兩個包,來看右邊的依賴樹, web 依賴於 beans、core 包, jdbc 也依賴於這兩個包,此時相同層級出現了依賴沖突,可從結果上來看,後面 jdbc 所依賴的兩個包被剔除了,能明顯看到一句: omitted for duplicate ,這又是為啥呢?因為根據聲明優先原則, 同層級出現包沖突時,先聲明的會覆蓋後聲明的,為此後者會被剔除

    ③配置優先原則 ,此時問題又又來了,既然相同層級出現同版本的類別庫,前面的會覆蓋後面的,可是當相同層級,出現不同版本的包呢?依舊來看例子:

    配置優先

    此時 pom 引入了兩個 web 包,前者版本為 5.1.8 ,後者為 5.1.2 ,這兩個包的層級都是 1 ,可是看右邊的依賴樹,此時會發現, 5.1.8 壓根沒引進來啊!為啥?這就是配置優先原則, 同級出現不同版本的相同類別庫時,後配置的會覆蓋先配置的

    所以大家發現了嘛?在很多時候,並不需要我們考慮依賴沖突問題, Maven 會依據上述三條原則,幫我們智慧化自動剔除沖突的依賴,其他包都會共享留下來的類別庫,只有當出現無法解決的沖突時,這才需要咱們手動介入。

    通常來說, Maven 如果無法自動解決沖突問題,會在構建過程中丟擲異常並提供相關資訊,這時大家可以根據給出的資訊,手動排除指定依賴。

    2.1.3、主動排除依賴

    所謂的排除依賴,即是指從一個依賴包中,排除掉它依賴的其他包,如果出現了 Maven 無法自動解決的沖突,就可以基於這種手段進行處理,例如:

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.1.8.RELEASE</version>
    <exclusions>
    <!-- 排除web包依賴的beans包 -->
    <exclusion>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    </exclusion>
    </exclusions>
    </dependency>

    依賴排除

    從圖中結果可以明顯看出,透過這種方式,可以手動移除包所依賴的其他包。當出現沖突時,透過這種方式將沖突的兩個包,移除掉其中一個即可。

    其實還有種叫做「隱藏依賴」的手段,不過這種手段是用於多工程聚合計畫,所以先講清楚「多模組/工程」計畫,接著再講「隱藏依賴」。

    2.2、Maven分模組開發

    現如今,一個稍具規模的完整計畫,通常都要考慮接入多端,如 PC、WEB、APP 端等,那此時問題來了,每個端之間的邏輯,多少會存在細微差異,如果將所有程式碼融入在一個 Maven 工程裏,這無疑會顯得十分臃腫!為了解決這個問題, Maven 推出了分模組開發技術。

    所謂的分模組開發, 即是指建立多個 Maven 工程,組成一個完整計畫 。通常會先按某個維度劃分出多個模組,接著為每個模組建立一個 Maven 工程,典型的拆分維度有三個:

  • • ①接入維度:按不同的接入端,將計畫劃分為多個模組,如 APP、WEB 、小程式等;

  • • ②業務維度:根據業務性質,將計畫劃分為一個個業務模組,如前台、後台、使用者等;

  • • ③功能維度:共用程式碼做成基礎模組,業務做成一個模組、 API 做成一個模組……。

  • 當然,通常①、②會和③混合起來用,比如典型的「先根據程式碼功能拆分,再根據業務維度拆分」。

    相較於把所有程式碼揉在一起的「大鍋飯」,多模組開發的好處特別明顯:

  • • ①簡化計畫管理,拆成多個模組/子系統後,每個模組可以獨立編譯、打包、釋出等;

  • • ②提高程式碼復用性,不同模組間可以相互參照,可以建立公共模組,減少程式碼冗余度;

  • • ③方便團隊協作,多人各司其職,負責不同的模組, Git 管理時也能減少交叉沖突;

  • • ④構建管理度更高,更方便做持續整合,可以根據需要靈活配置整個計畫的構建流程;

  • • ……

  • 不過 Maven2.0.9 才開始支持聚合工程,在最初的時期裏,想要實作分模組開發,需要手動先建立一個空的 Java 計畫( Empty Project ):

    空計畫

    接著再在其中建立多個 Maven Project

    建立子計畫

    然後再透過 mvn install 命令,將不同的 Maven 計畫安裝到本地倉庫,其他工程才能透過 GAV 座標引入。

    這種傳統方式特別吃力,尤其是多人開發時,另一個模組的程式碼更新了,必須手動去更新本地倉庫的 jar 包;而且多個模組之間相互依賴時,構建起來額外的麻煩!正因如此,官方在後面推出了「聚合工程」,下面聊聊這個。

    2.3、Maven聚合工程

    所謂的聚合工程,即是指: 一個計畫允許建立多個子模組,多個子模組組成一個整體,可以統一進行計畫的構建 。不過想要弄明白聚合工程,得先清楚「父子工程」的概念:

  • • 父工程:不具備任何程式碼、僅有 pom.xml 的空計畫,用來定義公共依賴、外掛程式和配置;

  • • 子工程:編寫具體程式碼的子計畫,可以繼承父工程的配置、依賴項,還可以獨立拓展。

  • Maven 聚合工程,就是基於父子工程結構,來將一個完整計畫,劃分出不同的層次,這種方式可以很好的管理多模組之間的依賴關系,以及構建順序,大大提高了開發效率、維護性。並且當一個子工程更新時,聚合工程可以保障同步更新其他存在關聯的子工程!

    2.3.1、聚合工程入門指南

    理解聚合工程是個什麽東東之後,接著來聊聊如何建立聚合工程,首先要建立一個空的 Maven 計畫,作為父工程,這時可以在 IDEA 建立 Maven 計畫時,把打包方式選成 POM ,也可以建立一個普通的 Maven 計畫,然後把 src 目錄刪掉,再修改一下 pom.xml

    <!-- 寫在當前計畫GAV座標下面 -->
    <packaging>pom</packaging>

    這樣就得到了一個父工程,接著可以在此基礎上,繼續建立子工程:

    建立子工程-1

    當點選 Next 後,大家會發現:

    建立子工程-2

    這時無法手動指定 G、V 了,而是會從父工程中繼承,最終效果如下:

    聚合工程

    這裏我建立了兩個子工程,所以父工程的 pom.xml 中,會用一個 <modules> 標簽,來記錄自己名下的子工程列表,而子工程的 pom 頭,也多了一個 <parent> 標簽包裹!大家看這個標簽有沒有眼熟感?大家可以去看一下 SpringBoot 計畫,每個 pom.xml 檔的頭,都是這樣的。

    這裏提個問題: 子工程下面能不能繼續建立子工程 ?答案 Yes ,你可以無限套娃下去,不過我的建議是:一個聚合計畫,最多只能有三層,路徑太深反而會出現稀奇古怪的問題。

    2.3.2、聚合工程的依賴管理

    前面搭建好了聚合工程,接著來看個問題:

    依賴冗余

    zhuzi_001、002 兩個子工程中,各自引入了三個依賴,可觀察上圖會發現,兩者引入的依賴僅有一個不同,其余全部一模一樣!所以這時,就出現了「依賴冗余」問題,那有沒有好的方式解決呢?答案是有的,前面說過: 公共的依賴、配置、外掛程式等,都可以配置在父工程裏 ,如下:

    繼承依賴

    當把公共的依賴定義在父工程中,此時觀察圖中右側的依賴樹,會發現兩個子工程都繼承了父依賴。

    不過此時問題又來了!為了防止不同子工程引入不同版本的依賴,最好的做法是在父工程中,統一對依賴的版本進行控制,規定所有子工程都使用同一版本的依賴,怎麽做到這點呢?可以使用 <dependencyManagement> 標簽來管理,例如:

    可選依賴

    在父工程中, <dependencies> 裏只定義了一個 webmvc 依賴,而 <dependencyManagement> 中定義了 druid、test、jdbc 三個依賴,這兩個標簽有何區別呢?

  • <dependencies> :定義強制性依賴,寫在該標簽裏的依賴項,子工程必須強制繼承;

  • <dependencyManagement> :定義可選性依賴,該標簽裏的依賴項,子工程可選擇使用。

  • 相信這樣解釋後,大家對於兩個標簽的區別,就能一清二楚了!同時註意,子工程在使用 <dependencyManagement> 中已有的依賴項時,不需要寫 <version> 版本號,版本號在父工程中統一管理,這就滿足了前面的需求。這樣做的好處在於: 以後為計畫的技術棧升級版本時,不需要單獨修改每個子工程的 POM ,只需要修改父 POM 檔即可,大大提高了維護性

    2.3.3、聚合工程解決依賴沖突

    之前傳統的 Maven 計畫會存在依賴沖突問題,那聚合工程中存不存在呢?當然存在,比如 001 中引入了 jdbc、test 這兩個包,而 002 中也引入了,這時假設把 001 工程打包到本地倉庫,在 002 工程中引入時,此時依賴是不是又沖突了? Yes ,怎麽處理呢?先看例子:

    聚合依賴傳遞

    在上圖中, 001 引入了 aop 包,接著透過 install 操作,把 001 工程打到了本地倉庫。於是,在 002 工程中,引入了 web、zhuzi_001 這兩個包。根據前面所說的依賴傳遞原則, 002 在引入 001 時,由於 001 參照了別的包,所以 002 被迫也引入了其他包。

    還是那句話,大多數情況下, Maven 會基於那三條原則,自動幫你剔除重復的依賴,如上圖右邊的依賴樹所示, Maven 自動剔除了重復依賴。這種結果顯然是好現象,可是萬一 Maven 不能自動剔除怎麽辦?這時就需要用到最開始所說的「隱藏依賴」技術了!

    修改 001 pom.xml ,如下:

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.8.RELEASE</version>
    <optional>true</optional>
    </dependency>

    眼尖的小夥應該能發現,此時多了一個 <optional> 標簽,該標簽即是「隱藏依賴」的開關:

  • true :開啟隱藏,當前依賴不會向其他工程傳遞,只保留給自己用;

  • false :預設值,表示當前依賴會保持傳遞性,其他引入當前工程的計畫會間接依賴。

  • 此時重新把 001 打到本地倉庫,再來看看依賴樹關系:

    隱藏依賴

    當開啟隱藏後,其他工程引入當前工程時,就不會再間接引入當前工程的隱藏依賴,因此來手動排除聚合工程中的依賴沖突問題。其他許多資料裏,講這塊時,多少講的有點令人迷糊,而相信看到這裏,大家就一定理解了 Maven 依賴管理。

    2.3.4、聚合工程的構建

    前面說到過, Maven 聚合工程可以對所有子工程進行統一構建,這是啥意思呢?如果是傳統的分模組計畫,需要挨個進行打包、測試、安裝……等工作,而聚合工程則不同,來看 IDEA 提供的 Maven 輔助工具:

    計畫構建

    尾巴上帶有 root 標識的工程,意味著這是一個父工程,在我們的案例中,有一個父、兩個子,來看 IDEA 的工具,除開給兩個子工程提供了 Lifecycle 命令外,還給父工程提供了一套 Lifecycle 命令,這兩者的區別在哪兒呢?當你雙擊父工程的某個 Lifecycle 命令,它找到父 POM <modules> 標簽,再根據其中的子工程列表,完成對整個聚合工程的構建工作。

    大家可以去試一下,當你雙擊父工程 Lifecycle 下的 clean ,它會把你所有子工程的 target 目錄刪除。同理,執行其他命令時也一樣,比如 install 命令,雙擊後它會把你所有的子工程,打包並安裝到本地倉庫,不過問題又又又來了!

    假設這裏 001 參照了 002 002 又參照了 001 ,兩者相互參照, Maven 會如何構建啊?到底該先打包 001 ,還是該先打包 002 ?我沒去看過 Lifecycle 外掛程式的源碼,不過相信背後的邏輯,應該跟 Spring 解決依賴迴圈類似,感興趣的小夥伴可以自行去研究。不過我這裏聲明一點:** Maven 聚合工程的構建流程,跟 <modules> 標簽裏的書寫順序無關,它會自行去推斷依賴關系,從而完成整個計畫的構建**。

    2.3.5、聚合打包跳過測試

    當大家要做計畫發版時,就需要對整個聚合工程的每個工程打包( jar war 包),此時可以直接雙擊父工程裏的 package 命令,但 test 命令在 package 之前,按照之前聊的生命周期原則,就會先執行 test ,再進行打包。

    test 階段,會去找到所有子工程的 src/test/java 目錄,並執行裏面的測試用例,如果其中任何一個報錯,就無法完成打包工作。而且就算不報錯,執行所有測試用例也會特別耗時,這時該怎麽辦呢?可以選擇跳過 test 階段,在 IDEA 工具裏的操作如下:

    跳過測試

    先選中 test 命令,接著點選上面的閃電圖示,這時 test 就會畫上橫線,表示該階段會跳過。如果你是在用 mvn 命令,那麽打包跳過測試的命令如下:

    mvn package –D skipTests

    同時大家還可以在 pom.xml 裏,配置外掛程式來精準控制,比如跳過某個測試類不執行,配置規則如下:

    <build>
    <plugins>
    <plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.1</version>
    <configuration>
    <skipTests>true</skipTests>
    <includes>
    <!-- 指定要執行的測試用例 -->
    <include>**/XXX*Test.java</include>
    </includes>
    <excludes>
    <!-- 執行要跳過的測試用例 -->
    <exclude>**/XXX*Test.java</exclude>
    </excludes>
    </configuration>
    </plugin>
    </plugins>
    </build>

    不過這個功能有點雞肋,了解即可,通常不需要用到。

    2.4、Maven內容

    回到之前案例的父工程 POM 中,此時來思考一個問題:

    版本冗余

    雖然我們透過 <dependencyManagement> 標簽,來控制了子工程中的依賴版本,可目前還有一個小問題: 版本冗余 !比如現在我想把 Spring 版本從 5.1.8 升級到 5.2.0 ,雖然不需要去修改子工程的 POM 檔,可從上圖中大家會發現,想升級 Spring 的版本,還需要修改多處地方!

    咋辦?總不能只升級其中一個依賴的版本吧?可如果全部都改一遍,無疑就太累了……,所以,這裏我們可以透過 Maven 內容來做管理,我們可以在 POM <properties> 標簽中,自訂內容,如:

    <properties>
    <spring.version>5.2.0.RELEASE</spring.version>
    </properties>

    而在 POM 的其他位置中,可以透過 ${} 來參照該內容,例如:

    內容管理

    這樣做的好處特別明顯,現在我想升級 Spring 版本,只需要修改一處地方即可!

    除開可以自訂內容外, Maven 也會有很多內建內容,大體可分為四類:

    型別 使用方式
    Maven 內建內容 ${內容名},如 ${version}\
    計畫環境內容 ${ setting. 內容名},如 ${settings.localRepository}\
    Java 環境變量 ${ . 內容名},如 ${java. class.path}\
    系統環境變量 ${ env. 內容名},如 ${env.USERNAME}\

    不過這些用的也不多,同時不需要記,要用的時候, IDEA 工具會有提示:

    提示

    2.5、Maven多環境配置

    實際工作會分為開發、測試、生產等環境,不同環境的配置資訊也略有不同,而大家都知道,我們可以透過 spring.profiles.active 內容,來動態使用不同環境的配置,而 Maven 為何又整出一個多環境配置出來呢?想要搞清楚,得先搭建一個 SpringBoot 版的 Maven 聚合工程。

    首先建立一個只有 POM 的父工程,但要註意,這裏是 SpringBoot 版聚合計畫,需稍微改造:

    <!-- 先把Spring Boot Starter聲明為父工程 -->
    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/>
    </parent>
    <!-- 當前父工程的GAV座標 -->
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zhuzi</groupId>
    <artifactId>maven_zhuzi</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <!-- 配置JDK版本 -->
    <properties>
    <java.version>8</java.version>
    </properties>
    <dependencies>
    <!-- 引入SpringBootWeb的Starter依賴 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    </dependencies>
    <build>
    <plugins>
    <!-- 引入SpringBoot整合Maven的外掛程式 -->
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>



    對比普通聚合工程的父 POM 來說, SpringBoot 版的聚合工程,需要先把 spring-boot-starter 聲明成自己的「爹」,同時需要引入 SpringBoot 相關的外掛程式,並且我在這裏還引入了一個 boot-web 依賴。

    接著來建立子工程,在建立時記得選 SpringBoot 樣版來建立,不過建立後記得改造 POM

    <!-- 聲明父工程 -->
    <parent>
    <artifactId>maven_zhuzi</artifactId>
    <groupId>com.zhuzi</groupId>
    <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <!-- 子工程的描述資訊 -->
    <artifactId>boot_zhuzi_001</artifactId>
    <name>boot_zhuzi_001</name>
    <description>Demo project for Spring Boot</description>

    就只需要這麽多,因為 SpringBoot 的外掛程式、依賴包,在父工程中已經聲明了,這裏會繼承過來。

    接著來做 Maven 多環境配置,找到父工程的 POM 進行修改,如下:

    <profiles>
    <!-- 開發環境 -->
    <profile>
    <id>dev</id>
    <properties>
    <profile.active>dev</profile.active>
    </properties>
    </profile>
    <!-- 生產環境 -->
    <profile>
    <id>prod</id>
    <properties>
    <profile.active>prod</profile.active>
    </properties>
    <!-- activeByDefault=true,表示打包時,預設使用這個環境 -->
    <activation>
    <activeByDefault>true</activeByDefault>
    </activation>
    </profile>
    <!-- 測試環境 -->
    <profile>
    <id>test</id>
    <properties>
    <profile.active>test</profile.active>
    </properties>
    </profile>
    </profiles>

    配置完這個後,重新整理當前 Maven 工程, IDEA 中就會出現這個:

    多環境

    預設停留在 prod 上,這是因為 POM 中用 <activeByDefault> 標簽指定了,接著去到子工程的 application.yml 中,完成 Spring 的多環境配置,如下:

    # 設定啟用的環境
    spring:
    profiles:
    active:${profile.active}
    ---
    # 開發環境
    spring:
    profiles:dev
    server:
    port:80
    ---
    # 生產環境
    spring:
    profiles:prod
    server:
    port:81
    ---
    # 測試環境
    spring:
    profiles:test
    server:
    port:82
    ---

    這裏可以透過檔來區分不同環境的配置資訊,但我這裏為了簡單,就直接用 --- 進行區分,這組配置大家應該很熟悉,也就是不同的環境中,使用不同的埠號,但唯一不同的是:**以前 spring.profiles.active 內容會寫上固定的值,而現在寫的是 ${profile.active} **,這是為什麽呢?

    這代表從 pom.xml 中,讀取 profile.active 內容值的意思,而父 POM 中配了三組值: dev、prod、test ,所以當前子工程的 POM ,也會繼承這組配置,而目前預設勾選在 prod 上,所以最終 spring.profiles.active=prod ,不過想要在 application.yml 讀到 pom.xml 的值,還需在父 POM 中,加一個依賴和外掛程式:

    <!-- 開啟 yml 檔的 ${} 取值支持 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>2.1.5.RELEASE</version>
    <optional>true</optional>
    </dependency>
    <!-- 添加外掛程式,將計畫的資原始檔復制到輸出目錄中 -->
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
    <encoding>UTF-8</encoding>
    <useDefaultDelimiters>true</useDefaultDelimiters>
    </configuration>
    </plugin>

    最後來嘗試啟動子工程,操作流程如下:

  • • ①在 Maven 工具的 Profiles 中勾選 dev ,並重新整理當前計畫;

  • • ②接著找到子工程的啟動類,並右鍵選擇 Run …… 啟動子計畫。

  • 多環境整合

    先仔細看執行的結果,我來解釋一下執行流程:

  • • ①啟動時, pom.xml 根據勾選的 Profiles ,使用相應的 dev 環境配置;

  • • ② yml ${profile.active} 會讀到 profile.active=dev ,使用 dev 配置組;

  • • ③ application.yml 中的 dev 配置組, server.port=80 ,所以最終透過 80 埠啟動。

  • 看完這個流程,大家明白最開始那個問題了嗎? Maven 為何還整了一個多環境配置?

    大家可能有種似懂非懂的感覺,這裏來說明一下,先把環境換到微服務計畫中,假設有 20 個微服務,此時計畫要上線或測試,所以需要更改配置資訊,比如把資料庫地址換成測試、線上地址等,而不同環境的配置,相信大家一定用 application-dev.yml、application-prod.yml…… 做好了區分。

    但就算提前準備了不同環境的配置,可到了切換環境時,還需要挨個服務修改 spring.profiles.active 這個值,從 dev 改成 prod、test ,然後才能使用對應的配置進行打包,可這裏有 20 個微服務啊,難道要手動改 20 次嗎?

    而在父 POM 中配置了 Maven 多環境後,這時 yml 會讀取 pom.xml 中的值,來使用不同的配置檔,此時大家就只需要在 IDEA 工具的 Profiles 中,把勾點從 dev 換到 test、prod ,然後重新整理一下 Maven SpringBoot 就能動態的切換配置檔,這是不是妙極了?因此,這才是 Maven 多環境的正確使用姿勢!

    三、Maven私服搭建

    前面叨叨絮絮說了一大堆,最後就來聊聊 Maven 私服配置,為啥需要私服呢?

    大家來設想這麽個場景,假設你身在基建團隊,主要負責研發各個業務開發組的公用元件,那麽當你寫完一個元件後,為了能讓別的業務開發組用上,難道是先把程式碼打包,接著用 U 盤拷出來,給別人送過去嘛?有人說不至於,難道我不會直接發過去啊……

    的確,用通訊軟體發過去也行,但問題依舊在,假設你的元件升級了,又發一遍嗎?所以,為了便於團隊協作,搭建一個遠端倉庫很有必要,寫完公用程式碼後,直接釋出到遠端倉庫,別人需要用到時,直接從遠端倉庫拉取即可,而你升級元件後,只需要再釋出一個新版本即可!

    那遠端倉庫該怎麽搭建呀?這就得用到 Maven 私服技術,最常用的就是基於 Nexus 來搭建。

    3.1、Nexus私服搭建指南

    Nexus Sonatype 公司開源的一款私服產品,大家可以先去到 Nexus官網 下載一下安裝包, Nexus 同樣是一款解壓即用的工具,不過也要註意: 解壓的目錄中不能存在中文,否則後面啟動不起來!

    解壓完成後,會看到兩個目錄:

  • nexus-x.x.x-xx :裏面會放 Nexus 啟動時所需要的依賴、環境配置;

  • sonatype-work :存放 Nexus 執行時的工作數據,如儲存上傳的 jar 包等。

  • 接著可以去到:

    解壓目錄/etc/nexus-default.properties

    這個檔修改預設配置,預設埠號是 8081 ,如果你這個埠已被使用,就可以修改一下,否則通常不需要更改。接著可以去到解壓目錄的 bin 資料夾中,開啟 cmd 終端,執行啟動命令:

    nexus.exe /run nexus

    初次啟動的過程會額外的慢,因為它需要初始化環境,建立工作空間、內嵌資料庫等,直到看見這句提示:

    啟動成功

    此時才算啟動成功, Nexus 初次啟動後,會在 sonatype-work 目錄中生成一個 /nexus3/admin.password 檔,這裏面存放著你的初始密碼,預設帳號就是 admin ,在瀏覽器輸入:

    http://localhost:8081

    存取 Nexus 界面,接著可以在網頁上透過初始密碼登入,登入後就會讓你修改密碼,改完後就代表 Nexus 搭建成功(不過要記住,改完密碼記得重新登入一次,否則後面的操作會沒有許可權)。

    3.2、Nexus私服倉庫

    預設倉庫

    登入成功後,點選 Browse 會看到一些預設倉庫,這裏稍微解釋一下每個欄位的含義。

  • Name :倉庫的名字;

  • Type :倉庫的型別;

  • Format :倉庫的格式;

  • Status :倉庫的狀態;

  • URL :倉庫的網路地址。

  • 重點來說說倉庫的分類,總共有四種型別:

    型別 釋義 作用
    hosted 宿主倉庫 保存中央倉庫中沒有的資源,如自研元件
    proxy 代理倉庫 配置中央倉庫,即映像源,私服中沒有時會去這個地址拉取
    group 倉庫組 用來對宿主、代理倉庫分組,將多個倉庫組合成一個對外服務
    virtual 虛擬倉庫 並非真實存在的倉庫,類似於 MySQL 中的檢視

    倉庫的關系如下:

    倉庫關系

    原生的 Maven 需要配置私服地址,當計畫需要的依賴,在本地倉庫沒有時,就會去到相應的宿主/遠端倉庫拉取;如果宿主倉庫也沒有,就會根據配置的代理倉庫地址,去到中央倉庫拉取,最後依次返回……。

    3.3、Maven配置私服

    Maven 想要使用私服,需要先修改 settings.xml 檔,我的建議是別直接改,先拷貝一份出來,接著來講講配置步驟。

    ①修改 settings.xml 裏的映像源配置,之前配的阿裏雲映像不能用了,改成:

    <mirror>
    <id>nexus-zhuzi</id>
    <mirrorOf>*</mirrorOf>
    <url>http://localhost:8081/repository/maven-public/</url>
    </mirror>

    ②在私服中修改存取許可權,允許匿名使用者存取,如下:

    開啟許可權

    ③在 Nexus 私服中配置一下代理倉庫地址,即配置映像源:

    配置映像

    將這個預設的中央倉庫地址,改為國內的阿裏雲映像:

    http://maven.aliyun.com/nexus/content/groups/public/

    改完後記得拖動到最下方,點選 Save 保存一下即可。

    ④在 Maven settings.xml 中,配置私服的帳號密碼:

    <server>
    <id>zhuzi-release</id>
    <username>admin</username>
    <password>你的私服帳號密碼</password>
    </server>
    <server>
    <id>zhuzi-snapshot</id>
    <username>admin</username>
    <password>你的私服帳號密碼</password>
    </server>

    這兩組配置,放到 <servers> 標簽中的任何一處即可,這裏可以先這樣配置,看不懂沒關系。

    3.4、計畫配置私服

    前面配置好了本地 Maven 與私服的對映關系,接著要配置計畫和私服的連線,說下流程。

    ①為計畫建立對應的私服倉庫,如果已有倉庫,可以直接復用,建立步驟如下:

    建立倉庫

    其中唯一值得一提的就是倉庫格式,這裏有三個可選項:

  • Release :穩定版,表示存放可以穩定使用的版本倉庫;

  • Snapshot :快照版,代表儲存開發階段的版本倉庫;

  • Mixed :混合版,不區分格式,表示混合儲存程式碼的倉庫。

  • 為了規範性,我的建議是 Release、Snapshot 格式的倉庫,各自都建立一個。

    ②在 Maven 工程的 pom.xml 檔中,配置對應的私服倉庫地址,如下:

    <!-- 配置當前工程,在私服中保存的具體位置 -->
    <distributionManagement>
    <repository>
    <!-- 這裏對應之前 settings.xml 裏配置的server-id -->
    <id>zhuzi-release</id>
    <!-- 這裏代表私服倉庫的地址,大家只需要把後面的名字換掉即可 -->
    <url>http://localhost:8081/repository/zhuzi-release/</url>
    </repository>
    <snapshotRepository>
    <id>zhuzi-snapshot</id>
    <url>http://localhost:8081/repository/zhuzi-snapshot/</url>
    </snapshotRepository>
    </distributionManagement>

    ③將當前計畫釋出到私服倉庫,這裏可以執行 mvn clean deploy 命令,也可以透過 IDEA 工具完成:

    釋出

    不過這裏有一個細節要註意,由於配置了私服上的兩個宿主倉庫,一個為穩定倉庫,另一個為快照倉庫,所以釋出時,預設會根據當前計畫的 <version> 版本結尾,來選擇上傳到相應的倉庫,例如上圖中的結尾是 SNAPSHOT ,所以會被釋出到快照倉庫,如果結尾不是這個字尾時,就會被釋出到 Release 倉庫。

    當釋出完成後,大家就可以登入 Nexus 界面,找到對應的宿主倉庫,檢視相應的 jar 包資訊啦!不過還有一點要註意:你要釋出的包不能帶有上級,即不能有 parent 依賴,否則在其他人在拉取該計畫時,會找不到其父計畫而構建失敗。要解決這個問題,可以先將 parent 計畫打包並上傳至遠端倉庫,然後再釋出依賴於該 parent 計畫的子模組。

    3.5、Nexus配置倉庫組

    前面在說倉庫型別時,還提到過一個「倉庫組」的概念,如果你目前所處的公司,是一個大型企業,不同團隊都有著各自的宿主倉庫,而你恰恰又需要用到其他團隊的元件,這時難道需要在 pom.xml 中,將遠端倉庫地址先改為其他團隊的地址嗎?答案是不需要的,這時可以建立一個倉庫組。

    建立倉庫組

    大家可以看到,圖中的 Members 區域代表當前倉庫組的成員,而這些成員會按照你排列的順序,具備不同的優先級,越靠前的優先級越高。建立好倉庫組後,接著可以去配置一下倉庫組,這裏有兩種方式。

    3.5.1、配置單個工程與倉庫組的對映

    這種方式只需修改 pom.xml 即可:

    <repositories>
    <repository>
    <id>zhuzi-group</id>
    <!-- 配置倉庫組的地址 -->
    <url>http://localhost:8081/repository/zhuzi-group/</url>
    <!-- 允許從中拉取穩定版的依賴 -->
    <releases>
    <enabled>true</enabled>
    </releases>
    <!-- 也允許從中拉取快照版的依賴 -->
    <snapshots>
    <enabled>true</enabled>
    </snapshots>
    </repository>
    </repositories>
    <pluginRepositories>
    <pluginRepository>
    <id>plugin-group</id>
    <url>http://localhost:8081/repository/zhuzi-group/</url>
    <releases>
    <enabled>true</enabled>
    </releases>
    <snapshots>
    <enabled>true</enabled>
    </snapshots>
    </pluginRepository>
    </pluginRepositories>

    在上述這組配置中,配置了 <repositories>、<pluginRepositories> 兩個標簽,分別是啥意思呢?很簡單,第一個是普通依賴的倉庫組地址,第二個是外掛程式依賴的倉庫組地址,前者針對於 pom.xml 中的 <dependency> 標簽生效,後者針對 <plugin> 標簽生效。

    當你透過 GAV 座標,引入一個依賴時,如果本地倉庫中沒找到,則會根據配置的倉庫組地址,去到 Nexus 私服上拉取依賴。不過因為倉庫組是由多個倉庫組成的,所以拉取時,會根據倉庫的優先級,依次搜尋相應的依賴,第一個倉庫將是最優先搜尋的倉庫。

    3.5.2、配置本地Maven與倉庫組的對映

    上一種配置方式,只針對於單個 Maven 工程生效,如果你所有的 Maven 工程,都需要與 Nexus 私服上的倉庫組繫結,這時就可以直接修改 settings.xml 檔,如下:

    <profile>
    <id>zhuzi-group</id>
    <repositories>
    <repository>
    <id>nexus-maven</id>
    <url>http://localhost:8081/repository/zhuzi-group/</url>
    <releases>
    <enabled>true</enabled>
    <updatePolicy>always</updatePolicy>
    </releases>
    <snapshots>
    <enabled>true</enabled>
    <updatePolicy>always</updatePolicy>
    </snapshots>
    </repository>
    </repositories>
    <pluginRepositories>
    <pluginRepository>
    <id>nexus-maven</id>
    <url>http://localhost:8081/repository/zhuzi-group/</url>
    <releases>
    <enabled>true</enabled>
    <updatePolicy>always</updatePolicy>
    </releases>
    <snapshots>
    <enabled>true</enabled>
    <updatePolicy>always</updatePolicy>
    </snapshots>
    </pluginRepository>
    </pluginRepositories>
    </profile>

    這組配置要寫在 <profiles> 標簽裏面,其他的與前一種方式沒太大區別,唯一不同的是多了一個 <updatePolicy> 標簽,該標簽的作用是指定倉庫映像的更新策略,可選項如下:

  • always :每次需要 Maven 依賴時,都先嘗試從遠端倉庫下載最新的依賴項;

  • daily :每天首次使用某個依賴時,從遠端倉庫中下載一次依賴項;

  • interval:X :每隔 X 個小時,下載一次遠端倉庫的依賴, X 只能是整數;

  • never :僅使用本地倉庫中已經存在的依賴項,不嘗試從遠端倉庫中拉取。

  • Maven 工程使用依賴時,首先會從本地倉庫中尋找所需的依賴項,如果本地倉庫沒有,則從配置的遠端倉庫下載這時會根據 <updatePolicy> 策略來決定是否需要從遠端倉庫下載依賴。

    不過上述這樣配置後,還無法讓配置生效,如果想要生效,還得啟用一下上述配置:

    <activeProfiles>
    <!-- 這裏寫前面配置的ID -->
    <activeProfile>zhuzi-group</activeProfile>
    </activeProfiles>

    不過要記住,無論兩種方式內的哪一種,都只允許從私服上拉取依賴,如果你的某個工程,想要打包釋出到私服上,還是需要配置 3.4 階段的 <distributionManagement> 標簽。

    四、Maven總結

    最後,對於 Maven 計畫的命名,不同單詞最好用 - 減號分割,而不是 _ 底線,畢竟 Spring、Apache…… 的開源計畫,都采用這種命名方式。不過,如果你要問我:「你為啥用 _ 不用 - 啊」?別問,問就是我控幾不住我寄幾啊……,更何況有句話說的好:知錯不改,善莫大焉!

    到這裏,對於 Maven 常用的功能已經講完了,掌握這些知識後,玩轉 Maven 的難度應該不大,不過 Maven 的功能遠不僅如此,就光說 pom.xml 這個檔,可以配置的標簽有幾百個,本文僅講到了幾十個罷了。

    個人簡介

    哪咤,群友愛稱:咤哥,一個靠著熱情攀登至C站巔峰的中年男子,CSDN粉絲50萬+,2022CSDN部落格之星Top1,10年開發管理經驗,目前就職於某一線大廠,專註Java硬核幹貨分享,立誌做到Java賽道全網Top N。

    喜歡的可以給個關註,關註「哪咤編程」,提高Java技能







    最後歡迎大家加入哪咤的知識星球【Java學習星球】,星球中有很多獨家的幹貨內容。比如:Java後端學習路線,10萬字208道Java經典面試題,10大學習專欄,包含Java基礎、資料庫、SSM、SpringBoot、微服務、華為OD機試、演算法、最佳化、中介軟體、設計模式等系列文章。


    堅持每日學習打卡,養成持續學習、持續成長的好習慣。


    成功秘訣只有一個,那就是,督促和鞭策自己,永不放棄。


    和哪咤一起學Java,陪伴學習,共同優秀。