标簽 : Java基礎
Maven 是每一位Java工程師每天都會接觸的工具, 但據我所知其實很多人對Maven了解的并不深, 隻把它當做一個依賴管理工具(下載下傳依賴、打包), Maven很多核心的功能反而沒用上. 最近重讀 Maven實戰, 雖然這本書年歲較老(10年出版: 那還是Hudson年代), 但絕大部分還是很值得參考的. 本文講述Maven的核心原理和概念, 是以還是大綱參考了這本書, 但細節大多參考的Maven的官方文檔以及網友釋出的部落格. 本文主要講解Maven的:
坐标與依賴、
倉庫、
生命周期與插件、
子產品聚合、
子產品繼承
等概念, 并通過一個開發Maven插件的執行個體來深入了解Maven的核心機制. 而對于 如何配置Maven、Nexus私服、Jenkins持續內建、Maven測試、建構Web、資源過濾、自定義Archetype 等相對簡單、講解繁瑣且網上有大量實踐案例的内容沒有涉及. 本文的目标是希望讀者能夠通過本文能對Maven核心原理有個相對深入的了解.
為了能夠自動化地解析任何一個Java構件, Maven必須将它們唯一辨別, 這就是依賴管理的底層基礎-坐标.
在數學中, 任何一個坐标可以唯一确定一個“點”.
Maven 坐标為Java構件引入了秩序, 任何一個構件都必須明确定義自己的坐标, 坐标元素包括<code>groupId</code>、<code>artifactId</code>、<code>version</code>、<code>packaging</code>、<code>classfier</code>:
元素
描述
ext
groupId
定義目前子產品隸屬的實際Maven項目, 表示方式與Java包類似
groupId不應直接對應項目隸屬的公司/組織(一個公司/組織下可能會有很多的項目).
artifactId
定義實際項目中的一個Maven子產品
推薦使用項目名作為artifactId字首, 如:commons-lang3以commons作為字首(因為Maven打包預設以<code>artifactId</code>作為字首)
version
定義目前項目所處版本(如1.0-SNAPSHOT、4.2.7.RELEASE、1.2.15、14.0.1-h-3 等)
Maven版本号定義約定: <主版本>.<次版本>.<增量版本>-<裡程碑版本>
packaging
定義Maven項目打包方式, 通常打包方式與所生成構件擴充名對應
有jar(預設)、war、pom、maven-plugin等.
classifier
用來幫助定義建構輸出的一些附屬構件(如javadoc、sources)
不能直接定義項目的classifier(因為附屬構件不是由項目預設生成, 須有附加插件的幫助)
Maven最著名的(也是幾乎每個人最先接觸到的)就是Maven的依賴管理, 它使得我們不必再到開源項目的官網一個個下載下傳開源元件, 然後再一個個放入classpath. 一個依賴聲明可以包含如下元素:
依賴傳遞
Maven依賴傳遞機制會自動加載我們引入的依賴包的依賴, 而不必去手動指定(好拗口(⊙﹏⊙)b : This allows you to avoid needing to discover and specify the libraries that your own dependencies require, and including them automatically).
如: 我們的項目依賴了spring-core, 而spring-core又依賴了commons-logging:
有了依賴傳遞機制, 在項目中添加了spring-core依賴時就不用再去考慮它依賴了什麼, 也不用擔心引入多餘的依賴. Maven會解析各個直接依賴的POM, 将必要的間接依賴以傳遞性依賴的形式引入到目前目錄中(inherits from its parents, or from its dependencies, and so on).
(依賴調節原則: 1. 路徑最近者優先; 2. 第一聲明者優先.)
聲明一個或者多個項目依賴, 可以包含的元素有:
Ext
groupId、artifactId和version
依賴的基本坐标(最最重要)
type
依賴的類型, 對應于項目坐标定義的packaging
預設jar
scope
依賴的範圍, 用來控制依賴與三種classpath(編譯classpath、測試classpath、運作classpath)的關系
optional
依賴是否可選
假如一個Jar包支援MySQL與Oracle兩種DB, 是以其建構時必須添加兩類驅動, 但使用者使用時隻會選擇一種DB. 此時對A、B就可使用optional可選依賴
exclusions
排除傳遞性依賴
依賴管理
Maven提供了dependency插件可以對Maven項目依賴檢視以及優化, 如<code>$ mvn dependency:tree</code>可以檢視目前項目的依賴樹, 詳見<code>$ mvn dependency:help</code>.
Maven 中, 任何一個依賴、插件或項目建構的輸出, 都可稱為構件, 而Maven倉庫就是集中存儲這些構件的地方.
Maven倉庫可簡單分成兩類: 本地倉庫與遠端倉庫. 當Maven根據坐标尋找構件時, 它會首先檢索本地倉庫, 如果本地存在則直接使用, 否則去遠端倉庫下載下傳.
本地倉庫: 預設位址為~/.m2/, 一個構件隻有在本地倉庫存在之後, 才能由Maven項目使用.
遠端倉庫: 遠端倉庫又可簡單分成兩類: 中央倉庫和私服.
由于原始的本地倉庫是空的, Maven必須至少知道一個遠端倉庫才能在執行指令時下載下傳需要的構件, 中央倉庫就是這樣一個預設的遠端倉庫.
私服是一種特殊的遠端倉庫, 它設在區域網路内, 通過代理廣域網上的遠端倉庫, 供區域網路内的Maven使用者使用.
當需要下載下傳構件時, Maven用戶端先向私服請求, 如果私服不存在該構件, 則從外部的遠端倉庫下載下傳, 并緩存在私服上, 再為客戶提供下載下傳服務. 此外, 一些無法從外部倉庫下載下傳到的建構也能從本地上傳到私服供大家使用(如公司内部二方包、Oracle的JDBC啟動等). 為了節省帶寬和時間, 一般在公司内部都會架設一台Maven私服, 但将公司内部項目部署到私服還需要對POM做如下配置:
<code>repository</code>表示釋出版本構件的倉庫, <code>snapshotRepository</code>代表快照版本的倉庫.
<code>id</code>為該遠端倉庫唯一辨別, <code>url</code>表示該倉庫位址.
配置正确後, 執行<code>$ mvn clean deploy</code>則可以将項目建構輸出的構件部署到對應配置的遠端倉庫.
推薦幾個可用的Maven倉庫搜尋服務:
Maven 将所有項目的建構過程統一抽象成一套生命周期: 項目的清理、初始化、編譯、測試、打包、內建測試、驗證、部署和站點生成 … 幾乎所有項目的建構,都能映射到這一組生命周期上. 但生命周期是抽象的(Maven的生命周期本身是不做任何實際工作), 任務執行(如編譯源代碼)均交由插件完成. 其中每個建構步驟都可以綁定一個或多個插件的目标,而且Maven為大多數建構步驟都編寫并綁定了預設插件.當使用者有特殊需要的時候, 也可以配置插件定制建構行為, 甚至自己編寫插件.
Maven 擁有三套互相獨立的生命周期: clean、default 和 site, 而每個生命周期包含一些phase階段, 階段是有順序的, 并且後面的階段依賴于前面的階段. 而三套生命周期互相之間卻并沒有前後依賴關系, 即調用site周期内的某個phase階段并不會對clean産生任何影響.
clean
clean生命周期的目的是清理項目:
執行如<code>$ mvn clean</code>;
default
default生命周期定義了真正建構時所需要執行的所有步驟:
執行如<code>$ mvn clean install</code>;
site
site生命周期的目的是建立和釋出項目站點: Maven能夠基于POM所包含的資訊,自動生成一個友好的站點,友善團隊交流和釋出項目資訊
執行指令如<code>$ mvn clean deploy site-deploy</code>;
生命周期的階段phase與插件的目标goal互相綁定, 用以完成實際的建構任務. 而對于插件本身, 為了能夠複用代碼,它往往能夠完成多個任務, 這些功能聚集在一個插件裡,每個功能就是一個目标.
如:<code>$ mvn compiler:compile</code>: 冒号前是插件字首, 後面是該插件目标(即: maven-compiler-plugin的compile目标).
而該目标綁定了default生命周期的compile階段:
是以, 他們的綁定能夠實作項目編譯的目的.
為了能讓使用者幾乎不用任何配置就能使用Maven建構項目, Maven 預設為一些核心的生命周期綁定了插件目标, 當使用者通過指令調用生命周期階段時, 對應的插件目标就會執行相應的邏輯.
clean生命周期階段綁定
生命周期階段
插件目标
pre-clean
-
maven-clean-plugin:clean
post-clean
default聲明周期階段綁定
執行任務
process-resources
maven-resources-plugin:resources
複制主資源檔案到主輸出目錄
compile
maven-compiler-plugin:compile
編譯主代碼到主輸出目錄
process-test-resources
maven-resources-plugin:testResources
複制測試資源檔案到測試輸出目錄
test-compile
maven-compiler-plugin:testCompile
編譯測試代碼到測試輸出目錄
test
maven-surefire-plugin:test
執行測試用例
package
maven-jar-plugin:jar
打jar包
install
maven-install-plugin:install
将項目輸出安裝到本地倉庫
deploy
maven-deploy-plugin:deploy
将項目輸出部署到遠端倉庫
site生命周期階段綁定
pre-site
maven-site-plugin:site
post-site
site-deploy
maven-site-plugin:deploy
除了内置綁定以外, 使用者還能夠自定義将某個插件目标綁定到生命周期的某個階段上. 如建立項目的源碼包, maven-source-plugin插件的jar-no-fork目标能夠将項目的主代碼打包成jar檔案, 可以将其綁定到verify階段上:
<code>executions</code>下每個<code>execution</code>子元素可以用來配置執行一個任務.
線上
Maven 官方插件
<a href="https://maven.apache.org/plugins/index.html">https://maven.apache.org/plugins/index.html</a>
CodeHaus 插件
<a href="http://www.mojohaus.org/plugins.html">http://www.mojohaus.org/plugins.html</a>
maven-help-plugin
Maven的聚合特性(aggregation)能夠使項目的多個子產品聚合在一起建構, 而繼承特性(inheritance)能夠幫助抽取各子產品相同的依賴、插件等配置,在簡化子產品配置的同時, 保持各子產品一緻.
随着項目越來越複雜(需要解決的問題越來越多、功能越來越重), 我們更傾向于将一個項目劃分幾個子產品并行開發, 如: 将<code>feedcenter-push</code>項目劃分為<code>client</code>、<code>core</code>和<code>web</code>三個子產品, 而我們又想一次建構所有子產品, 而不是針對各子產品分别執行<code>$ mvn</code>指令. 于是就有了Maven的子產品聚合 -> 将<code>feedcenter-push</code>作為聚合子產品将其他子產品聚集到一起建構:
聚合POM
聚合子產品POM僅僅是幫助聚合其他子產品建構的工具, 本身并無實質内容:
通過在一個打包方式為<code>pom</code>的Maven項目中聲明任意數量的<code>module</code>以實作子產品聚合:
<code>packaging</code>: <code>pom</code>, 否則無法聚合建構.
<code>modules</code>: 實作聚合的核心,<code>module</code>值為被聚合子產品相對于聚合POM的相對路徑, 每個被聚合子產品下還各自包含有pom.xml、src/main/java、src/test/java等内容, 離開聚合POM也能夠獨立建構(注: 子產品所處目錄最好與其artifactId一緻).
Tips: 推薦将聚合POM放在項目目錄的最頂層, 其他子產品作為聚合子產品的子目錄.
在面向對象中, 可以通過類繼承實作複用. 在Maven中同樣也可以建立POM的父子結構, 通過在父POM中聲明一些配置供子POM繼承來實作複用與消除重複:
與聚合類似, 父POM的打包方式也是<code>pom</code>, 是以可以繼續複用聚合子產品的POM(這也是在開發中常用的方式):
<code>dependencyManagement</code>: 能讓子POM繼承父POM的配置的同時, 又能夠保證子子產品的靈活性: 在父POM<code>dependencyManagement</code>元素配置的依賴聲明不會實際引入子子產品中, 但能夠限制子子產品<code>dependencies</code>下的依賴的使用(子子產品隻需配置<code>groupId</code>與<code>artifactId</code>, 見下).
<code>pluginManagement</code>: 與<code>dependencyManagement</code>類似, 配置的插件不會造成實際插件的調用行為, 隻有當子POM中配置了相關<code>plugin</code>元素, 才會影響實際的插件行為.
元素繼承
可以看到, 子POM中并未定義子產品<code>groupId</code>與<code>version</code>, 這是因為子POM預設會從父POM繼承了如下元素:
groupId、version
dependencies
developers and contributors
plugin lists (including reports)
plugin executions with matching ids
plugin configuration
resources
是以所有的springframework都省去了<code>version</code>、junit還省去了<code>scope</code>, 而且插件還省去了<code>executions</code>與<code>configuration</code>配置, 因為完整的聲明已經包含在父POM中.
優勢: 當依賴、插件的版本、配置等資訊在父POM中聲明之後, 子子產品在使用時就無須聲明這些資訊, 也就不會出現多個子子產品使用的依賴版本不一緻的情況, 也就降低了依賴沖突的幾率. 另外如果子子產品不顯式聲明依賴與插件的使用, 即使已經在父POM的<code>dependencyManagement</code>、<code>pluginManagement</code>中配置了, 也不會産生實際的效果.
推薦: 子產品繼承與子產品聚合同時進行,這意味着, 你可以為你的所有子產品指定一個父工程, 同時父工程中可以指定其餘的Maven子產品作為它的聚合子產品. 但需要遵循以下三條規則:
在所有子POM中指定它們的父POM;
将父POM的<code>packaging</code>值設為<code>pom</code>;
在父POM中指定子子產品/子POM的目錄.
注: <code>parent</code>元素内還包含一個<code>relativePath</code>元素, 用于指定父POM的相對路徑, 預設<code>../pom.xml</code>.
任何一個Maven項目都隐式地繼承自超級POM, 是以超級POM的大量配置都會被所有的Maven項目繼承, 這些配置也成為了Maven所提倡的約定.
幾乎100%的場景都不用我們自己開發Maven插件, 但了解插件開發可以使我們更加深入的了解Maven. 下面我們實際開發一個用于統計代碼行數的插件 lc-maven-plugin.
pom.xml
插件本身也是Maven項目, 特殊之處在于<code>packaging</code>方式為<code>maven-plugin</code>:
<code>maven-plugin</code> 打包方式能控制Maven為其在生命周期階段綁定插件處理的相關目标.
What is a Mojo? A mojo is a M aven plain O ld J ava O bject. Each mojo is an executable goal in Maven, and a plugin is a distribution of one or more related mojos.
<code>@Parameter</code>: 配置點, 提供Mojo的可配置參數. 大部分Maven插件及其目标都是可配置的, 通過配置點, 使用者可以自定義插件行為:
<code>execute()</code>: 實際插件功能;
異常: <code>execute()</code>方法可以抛出以下兩種異常:
<code>MojoExecutionException</code>: Maven執行目标遇到該異常會顯示 BUILD FAILURE 錯誤資訊, 表示在運作期間發生了預期的錯誤;
<code>MojoFailureException</code>: 表示運作期間遇到了未預期的錯誤, 顯示 BUILD ERROR 資訊.
通過<code>mvn clean install</code>将插件安裝到倉庫後, 就可将其配置到實際Maven項目中, 用于統計項目代碼了:
<dl></dl>
<dt>參考 & 擴充</dt>
by 翡青