Maven 核心原理


Maven 核心原理

Maven 是每一位Java工程師每天都會接觸的工具, 但據我所知其實很多人對Maven理解的並不深, 只把它當做一個依賴管理工具(下載依賴、打包), Maven很多核心的功能反而沒用上. 最近重讀 Maven實戰, 雖然這本書年歲較老(10年出版: 那還是Hudson年代), 但絕大部分還是很值得參考的. 本文講述Maven的核心原理和概念, 因此還是大綱參考了這本書, 但細節大多參考的Maven的官方文檔以及網友釋出的博客. 本文主要講解Maven的:

  • 坐標與依賴、
  • 倉庫、
  • 生命周期與插件、
  • 模塊聚合、
  • 模塊繼承

等概念, 並通過一個開發Maven插件的實例來深入了解Maven的核心機制. 而對於 如何配置MavenNexus私服Jenkins持續集成Maven測試構建Web資源過濾自定義Archetype 等相對簡單、講解繁瑣且網上有大量實踐案例的內容沒有涉及. 本文的目標是希望讀者能夠通過本文能對Maven核心原理有個相對深入的理解.


坐標與依賴

為了能夠自動化地解析任何一個Java構件, Maven必須將它們唯一標識, 這就是依賴管理的底層基礎-坐標.


坐標

在數學中, 任何一個坐標可以唯一確定一個“點”.

Maven 坐標為Java構件引入了秩序, 任何一個構件都必須明確定義自己的坐標, 坐標元素包括groupIdartifactIdversionpackagingclassfier:

<groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> <packaging>jar</packaging>
  • 1
  • 2
  • 3
  • 4
元素 描述 ext
groupId 定義當前模塊隸屬的實際Maven項目, 表示方式與Java包類似 groupId不應直接對應項目隸屬的公司/組織(一個公司/組織下可能會有很多的項目).
artifactId 定義實際項目中的一個Maven模塊 推薦使用項目名作為artifactId前綴, 如:commons-lang3以commons作為前綴(因為Maven打包默認以artifactId作為前綴)
version 定義當前項目所處版本(如1.0-SNAPSHOT4.2.7.RELEASE1.2.1514.0.1-h-3 等) Maven版本號定義約定: <主版本>.<次版本>.<增量版本>-<里程碑版本>
packaging 定義Maven項目打包方式, 通常打包方式與所生成構件擴展名對應 jar(默認)、warpommaven-plugin等.
classifier 用來幫助定義構建輸出的一些附屬構件(如javadocsources) 不能直接定義項目的classifier(因為附屬構件不是由項目默認生成, 須有附加插件的幫助)

依賴

Maven最著名的(也是幾乎每個人最先接觸到的)就是Maven的依賴管理, 它使得我們不必再到開源項目的官網一個個下載開源組件, 然后再一個個放入classpath. 一個依賴聲明可以包含如下元素:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.2.7.RELEASE</version> <type>jar</type> <scope>compile</scope> <optional>false</optional> <exclusions> <exclusion> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </exclusion> </exclusions> </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 依賴傳遞 
    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. 第一聲明者優先.) 
    更多傳遞依賴信息可參考: Dependency Mechanism-Transitive Dependencies.

  • 聲明一個或者多個項目依賴, 可以包含的元素有:

元素 描述 Ext
groupId、artifactId和version 依賴的基本坐標(最最重要)  
type 依賴的類型, 對應於項目坐標定義的packaging 默認jar
scope 依賴的范圍, 用來控制依賴與三種classpath(編譯classpath測試classpath運行classpath)的關系 包含compileprovidedruntimetestsystemimport 6種, 詳見:Dependency Scope.
optional 依賴是否可選 假如一個Jar包支持MySQL與Oracle兩種DB, 因此其構建時必須添加兩類驅動, 但用戶使用時只會選擇一種DB. 此時對A、B就可使用optional可選依賴
exclusions 排除傳遞性依賴 傳遞性依賴極大的簡化了項目依賴的管理, 但也會引入Jar包版本沖突等問題, 此時就需要對傳遞性依賴進行排除. optional與exclusions詳細可參考: Optional Dependencies and Dependency Exclusions
  • 依賴管理 
    Maven提供了dependency插件可以對Maven項目依賴查看以及優化, 如$ mvn dependency:tree可以查看當前項目的依賴樹, 詳見$ mvn dependency:help.

Maven 倉庫

Maven 中, 任何一個依賴、插件或項目構建的輸出, 都可稱為構件, 而Maven倉庫就是集中存儲這些構件的地方.


兩類倉庫

Maven倉庫可簡單分成兩類: 本地倉庫與遠程倉庫. 當Maven根據坐標尋找構件時, 它會首先檢索本地倉庫, 如果本地存在則直接使用, 否則去遠程倉庫下載.

  • 本地倉庫: 默認地址為~/.m2/, 一個構件只有在本地倉庫存在之后, 才能由Maven項目使用.
  • 遠程倉庫: 遠程倉庫又可簡單分成兩類: 中央倉庫和私服. 
    • 由於原始的本地倉庫是空的, Maven必須至少知道一個遠程倉庫才能在執行命令時下載需要的構件, 中央倉庫就是這樣一個默認的遠程倉庫.

關於倉庫的詳細配置可參考: Settings Reference 的 ServersMirrorsProfiles 等章節.


私服

私服是一種特殊的遠程倉庫, 它設在局域網內, 通過代理廣域網上的遠程倉庫, 供局域網內的Maven用戶使用. 
當需要下載構件時, Maven客戶端先向私服請求, 如果私服不存在該構件, 則從外部的遠程倉庫下載, 並緩存在私服上, 再為客戶提供下載服務. 此外, 一些無法從外部倉庫下載到的構建也能從本地上傳到私服供大家使用(如公司內部二方包、Oracle的JDBC啟動等). 為了節省帶寬和時間, 一般在公司內部都會架設一台Maven私服, 但將公司內部項目部署到私服還需要對POM做如下配置:

<project > ... <distributionManagement> <repository> <id>releases</id> <url>http://mvn.server.com/nexus/content/repositories/releases/</url> </repository> <snapshotRepository> <id>snapshots</id> <url>http://mvn.server.com/nexus/content/repositories/snapshots/</url> </snapshotRepository> </distributionManagement> </project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • repository表示發布版本構件的倉庫, snapshotRepository代表快照版本的倉庫.
  • id為該遠程倉庫唯一標識, url表示該倉庫地址.

配置正確后, 執行$ mvn clean deploy則可以將項目構建輸出的構件部署到對應配置的遠程倉庫.

注: 往遠程倉庫部署構件時, 往往需要認證: 需要在settings.xml中創建servers元素, 其id與倉庫的id匹配, 詳見: Password Encryption.


倉庫搜索


生命周期與插件

Maven 將所有項目的構建過程統一抽象成一套生命周期: 項目的清理、初始化、編譯、測試、打包、集成測試、驗證、部署和站點生成 … 幾乎所有項目的構建,都能映射到這一組生命周期上. 但生命周期是抽象的(Maven的生命周期本身是不做任何實際工作), 任務執行(如編譯源代碼)均交由插件完成. 其中每個構建步驟都可以綁定一個或多個插件的目標,而且Maven為大多數構建步驟都編寫並綁定了默認插件.當用戶有特殊需要的時候, 也可以配置插件定制構建行為, 甚至自己編寫插件. 


生命周期

Maven 擁有三套相互獨立的生命周期: cleandefault 和 site, 而每個生命周期包含一些phase階段, 階段是有順序的, 並且后面的階段依賴於前面的階段. 而三套生命周期相互之間卻並沒有前后依賴關系, 即調用site周期內的某個phase階段並不會對clean產生任何影響.

  • clean 
    clean生命周期的目的是清理項目: 

    執行如$ mvn clean;

  • default 
    default生命周期定義了真正構建時所需要執行的所有步驟: 

    執行如$ mvn clean install;

  • site 
    site生命周期的目的是建立和發布項目站點: Maven能夠基於POM所包含的信息,自動生成一個友好的站點,方便團隊交流和發布項目信息 

    執行命令如$ mvn clean deploy site-deploy;

更多 Maven 生命周期介紹可以參考: Introduction to the Build LifecycleLifecycles Reference.


插件

生命周期的階段phase與插件的目標goal相互綁定, 用以完成實際的構建任務. 而對於插件本身, 為了能夠復用代碼,它往往能夠完成多個任務, 這些功能聚集在一個插件里,每個功能就是一個目標. 
如:$ mvn compiler:compile: 冒號前是插件前綴, 后面是該插件目標(即: maven-compiler-plugin的compile目標). 
而該目標綁定了default生命周期的compile階段: 
 
因此, 他們的綁定能夠實現項目編譯的目的.


內置綁定

為了能讓用戶幾乎不用任何配置就能使用Maven構建項目, Maven 默認為一些核心的生命周期綁定了插件目標, 當用戶通過命令調用生命周期階段時, 對應的插件目標就會執行相應的邏輯.

  • clean生命周期階段綁定
生命周期階段 插件目標
pre-clean -
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 將項目輸出部署到遠程倉庫

注: 上表只列出了打包方式為jar且擁有插件綁定關系的階段, 其他打包類型生命周期的默認綁定關系可參考: Built-in Lifecycle BindingsPlugin Bindings for default Lifecycle Reference.

  • site生命周期階段綁定
生命周期階段 插件目標
pre-site -
site maven-site-plugin:site
post-site -
site-deploy maven-site-plugin:deploy

自定義綁定

除了內置綁定以外, 用戶還能夠自定義將某個插件目標綁定到生命周期的某個階段上. 如創建項目的源碼包, maven-source-plugin插件的jar-no-fork目標能夠將項目的主代碼打包成jar文件, 可以將其綁定到verify階段上:

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>attach-sources</id> <phase>verify</phase> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • executions下每個execution子元素可以用來配置執行一個任務.

關於Maven插件的自定義綁定及其他詳細配置可參考: Guide to Configuring Plug-ins.


插件查詢

mvn help:describe -Dplugin=org.apache.maven.plugins:maven-source-plugin[:3.0.0] [-Ddetail] [-Dgoal=jar-no-fork]
  • 1

聚合與繼承

Maven的聚合特性(aggregation)能夠使項目的多個模塊聚合在一起構建, 而繼承特性(inheritance)能夠幫助抽取各模塊相同的依賴、插件等配置,在簡化模塊配置的同時, 保持各模塊一致.


模塊聚合

隨着項目越來越復雜(需要解決的問題越來越多、功能越來越重), 我們更傾向於將一個項目划分幾個模塊並行開發, 如: 將feedcenter-push項目划分為clientcoreweb三個模塊, 而我們又想一次構建所有模塊, 而不是針對各模塊分別執行$ mvn命令. 於是就有了Maven的模塊聚合 -> 將feedcenter-push作為聚合模塊將其他模塊聚集到一起構建:

  • 聚合POM 
    聚合模塊POM僅僅是幫助聚合其他模塊構建的工具, 本身並無實質內容:
<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.vdian.feedcenter</groupId> <artifactId>feedcenter-push</artifactId> <packaging>pom</packaging> <version>1.0.0.SNAPSHOT</version> <modules> <module>feedcenter-push-client</module> <module>feedcenter-push-core</module> <module>feedcenter-push-web</module> </modules> </project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 通過在一個打包方式為pom的Maven項目中聲明任意數量的module以實現模塊聚合: 
    1. packagingpom, 否則無法聚合構建.
    2. modules: 實現聚合的核心,module值為被聚合模塊相對於聚合POM的相對路徑, 每個被聚合模塊下還各自包含有pom.xmlsrc/main/javasrc/test/java等內容, 離開聚合POM也能夠獨立構建(注: 模塊所處目錄最好與其artifactId一致).

Tips: 推薦將聚合POM放在項目目錄的最頂層, 其他模塊作為聚合模塊的子目錄.

其他關於聚合與反應堆介紹可參考: Guide to Working with Multiple Modules.


模塊繼承

在面向對象中, 可以通過類繼承實現復用. 在Maven中同樣也可以創建POM的父子結構, 通過在父POM中聲明一些配置供子POM繼承來實現復用與消除重復:


1. 父POM

與聚合類似, 父POM的打包方式也是pom, 因此可以繼續復用聚合模塊的POM(這也是在開發中常用的方式):

<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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.vdian.feedcenter</groupId> <artifactId>feedcenter-push</artifactId> <packaging>pom</packaging> <version>1.0.0.SNAPSHOT</version> <modules> <module>feedcenter-push-client</module> <module>feedcenter-push-core</module> <module>feedcenter-push-web</module> </modules> <properties> <finalName>feedcenter-push</finalName> <warName>${finalName}.war</warName> <spring.version>4.0.6.RELEASE</spring.version> <junit.version>4.12</junit.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <warExplodedDirectory>exploded/${warName}</warExplodedDirectory> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> </dependencyManagement> <build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <id>attach-sources</id> <phase>verify</phase> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> </plugins> </pluginManagement> </build> </project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • dependencyManagement: 能讓子POM繼承父POM的配置的同時, 又能夠保證子模塊的靈活性: 在父POMdependencyManagement元素配置的依賴聲明不會實際引入子模塊中, 但能夠約束子模塊dependencies下的依賴的使用(子模塊只需配置groupIdartifactId, 見下).
  • pluginManagement: 與dependencyManagement類似, 配置的插件不會造成實際插件的調用行為, 只有當子POM中配置了相關plugin元素, 才會影響實際的插件行為.

2. 子POM

<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <parent> <groupId>com.vdian.feedcenter</groupId> <artifactId>feedcenter-push</artifactId> <version>1.0.0.SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>feedcenter-push-client</artifactId> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> </plugin> </plugins> </build> </project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 元素繼承 
    可以看到, 子POM中並未定義模塊groupIdversion, 這是因為子POM默認會從父POM繼承了如下元素:

    • groupId、version
    • dependencies
    • developers and contributors
    • plugin lists (including reports)
    • plugin executions with matching ids
    • plugin configuration
    • resources 
      因此所有的springframework都省去了version、junit還省去了scope, 而且插件還省去了executionsconfiguration配置, 因為完整的聲明已經包含在父POM中.
  • 優勢: 當依賴、插件的版本、配置等信息在父POM中聲明之后, 子模塊在使用時就無須聲明這些信息, 也就不會出現多個子模塊使用的依賴版本不一致的情況, 也就降低了依賴沖突的幾率. 另外如果子模塊不顯式聲明依賴與插件的使用, 即使已經在父POM的dependencyManagementpluginManagement中配置了, 也不會產生實際的效果.

  • 推薦: 模塊繼承與模塊聚合同時進行,這意味着, 你可以為你的所有模塊指定一個父工程, 同時父工程中可以指定其余的Maven模塊作為它的聚合模塊. 但需要遵循以下三條規則:

    • 在所有子POM中指定它們的父POM;
    • 將父POM的packaging值設為pom;
    • 在父POM中指定子模塊/子POM的目錄.

注: parent元素內還包含一個relativePath元素, 用於指定父POM的相對路徑, 默認../pom.xml.


超級pom-約定優先於配置

任何一個Maven項目都隱式地繼承自超級POM, 因此超級POM的大量配置都會被所有的Maven項目繼承, 這些配置也成為了Maven所提倡的約定.

<!-- START SNIPPET: superpom --> <project> <modelVersion>4.0.0</modelVersion> <!-- 定義了中央倉庫以及插件倉庫, 均為:https://repo.maven.apache.org/maven2 --> <repositories> <repository> <id>central</id> <name>Central Repository</name> <url>https://repo.maven.apache.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>central</id> <name>Central Repository</name> <url>https://repo.maven.apache.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> <releases> <updatePolicy>never</updatePolicy> </releases> </pluginRepository> </pluginRepositories> <!-- 依次定義了各類代碼、資源、輸出目錄及最終構件名稱格式, 這就是Maven項目結構的約定 --> <build> <directory>${project.basedir}/target</directory> <outputDirectory>${project.build.directory}/classes</outputDirectory> <finalName>${project.artifactId}-${project.version}</finalName> <testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory> <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory> <scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory> <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> </resource> </resources> <testResources> <testResource> <directory>${project.basedir}/src/test/resources</directory> </testResource> </testResources> <!-- 為核心插件設定版本 --> <pluginManagement> <!-- NOTE: These plugins will be removed from future versions of the super POM --> <!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) --> <plugins> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.3</version> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.2-beta-5</version> </plugin> <plugin> <artifactId>maven-dependency-plugin</artifactId> <version>2.8</version> </plugin> <plugin> <artifactId>maven-release-plugin</artifactId> <version>2.3.2</version> </plugin> </plugins> </pluginManagement> </build> <!-- 定義項目報告輸出路徑 --> <reporting> <outputDirectory>${project.build.directory}/site</outputDirectory> </reporting> <!-- 定義release-profile, 為構件附上源碼與文檔 --> <profiles> <!-- NOTE: The release profile will be removed from future versions of the super POM --> <profile> <id>release-profile</id> <activation> <property> <name>performRelease</name> <value>true</value> </property> </activation> <build> <plugins> <plugin> <inherited>true</inherited> <artifactId>maven-source-plugin</artifactId> <executions> <execution> <id>attach-sources</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> <plugin> <inherited>true</inherited> <artifactId>maven-javadoc-plugin</artifactId> <executions> <execution> <id>attach-javadocs</id> <goals> <goal>jar</goal> </goals> </execution> </executions> </plugin> <plugin> <inherited>true</inherited> <artifactId>maven-deploy-plugin</artifactId> <configuration> <updateReleaseInfo>true</updateReleaseInfo> </configuration> </plugin> </plugins> </build> </profile> </profiles> </project> <!-- END SNIPPET: superpom -->
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135

附: Maven繼承與組合的其他信息還可參考: Introduction to the POM.


Maven Plugin 開發

幾乎100%的場景都不用我們自己開發Maven插件, 但理解插件開發可以使我們更加深入的理解Maven. 下面我們實際開發一個用於統計代碼行數的插件 lc-maven-plugin.


1. 創建plugin項目

mvn archetype:generate 
    -DgroupId=com.fq.plugins -DartifactId=lc-maven-plugin -Dversion=0.0.1-SNAPSHOT -DarchetypeArtifactId=maven-archetype-plugin -DinteractiveMode=false -DarchetypeCatalog=internal
  • 1
  • 2
  • 3
  • 4
  • 5

使用maven-archetype-plugin Archetype可以快速創建一個Maven插件項目(關於Maven Archetype可參考What is an Archetype 、Creating Archetypes[注: 該文檔介紹的是Archetype 1.x編寫, 2.x內附鏈接]).

  • pom.xml 
    插件本身也是Maven項目, 特殊之處在於packaging方式為maven-plugin:
<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.fq.plugins</groupId> <artifactId>lc-maven-plugins</artifactId> <packaging>maven-plugin</packaging> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>19.0</version> </dependency> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>3.3.3</version> </dependency> <dependency> <groupId>org.apache.maven.plugin-tools</groupId> <artifactId>maven-plugin-annotations</artifactId> <version>3.3</version> </dependency> </dependencies> </project>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

maven-plugin 打包方式能控制Maven為其在生命周期階段綁定插件處理的相關目標.


2. 編寫目標Mojo

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.

/** * @author jifang * @since 16/10/9 上午11:33. */ @Mojo(name = "lc", defaultPhase = LifecyclePhase.VERIFY) public class LCMavenMojo extends AbstractMojo { private static final List<String> DEFAULT_FILES = Arrays.asList("java", "xml", "properties"); @Parameter(defaultValue = "${project.basedir}", readonly = true) private File baseDir; @Parameter(defaultValue = "${project.build.sourceDirectory}", readonly = true) private File srcDir; @Parameter(defaultValue = "${project.build.testSourceDirectory}", readonly = true) private File testSrcDir; @Parameter(defaultValue = "${project.build.resources}", readonly = true) private List<Resource> resources; @Parameter(defaultValue = "${project.build.testResources}", readonly = true) private List<Resource> testResources; @Parameter(property = "lc.file.includes") private Set<String> includes = new HashSet<>(); private Log logger = getLog(); @Override public void execute() throws MojoExecutionException, MojoFailureException { if (includes.isEmpty()) { logger.debug("includes/lc.file.includes is empty!"); includes.addAll(DEFAULT_FILES); } logger.info("includes: " + includes); try { long lines = 0; lines += countDir(srcDir); lines += countDir(testSrcDir); for (Resource resource : resources) { lines += countDir(new File(resource.getDirectory())); } for (Resource resource : testResources) { lines += countDir(new File(resource.getDirectory())); } logger.info("total lines: " + lines); } catch (IOException e) { logger.error("error: ", e); throw new MojoFailureException("execute failure: ", e); } } private LineProcessor<Long> lp = new LineProcessor<Long>() { private long line = 0; @Override public boolean processLine(String fileLine) throws IOException { if (!Strings.isNullOrEmpty(fileLine)) { ++this.line; } return true; } @Override public Long getResult() { long result = line; this.line = 0; return result; } }; private long countDir(File directory) throws IOException { long lines = 0; if (directory.exists()) { Set<File> files = new HashSet<>(); collectFiles(files, directory); for (File file : files) { lines += CharStreams.readLines(new FileReader(file), lp); } String path = directory.getAbsolutePath().substring(baseDir.getAbsolutePath().length()); logger.info("path: " + path + ", file count: " + files.size() + ", total line: " + lines); logger.info("\t-> files: " + files.toString()); } return lines; } private void collectFiles(Set<File> files, File file) { if (file.isFile()) { String fileName = file.getName(); int index = fileName.lastIndexOf("."); if (index != -1 && includes.contains(fileName.substring(index + 1))) { files.add(file); } } else { File[] subFiles = file.listFiles(); for (int i = 0; subFiles != null && i < subFiles.length; ++i) { collectFiles(files, subFiles[i]); } } } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • @Parameter: 配置點, 提供Mojo的可配置參數. 大部分Maven插件及其目標都是可配置的, 通過配置點, 用戶可以自定義插件行為:
<plugin> <groupId>com.fq.plugins</groupId> <artifactId>lc-maven-plugins</artifactId> <version>0.0.1-SNAPSHOT</version> <executions> <execution> <id>lc</id> <phase>verify</phase> <goals> <goal>lc</goal> </goals> <configuration> <includes> <include>java</include> <include>lua</include> <include>json</include> <include>xml</include> <include>properties</include> </includes> </configuration> </execution> </executions> </plugin>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • execute(): 實際插件功能;
  • 異常: execute()方法可以拋出以下兩種異常: 
    • MojoExecutionException: Maven執行目標遇到該異常會顯示 BUILD FAILURE 錯誤信息, 表示在運行期間發生了預期的錯誤;
    • MojoFailureException: 表示運行期間遇到了未預期的錯誤, 顯示 BUILD ERROR 信息.

3. 測試&執行

通過mvn clean install將插件安裝到倉庫后, 就可將其配置到實際Maven項目中, 用於統計項目代碼了:

$ mvn com.fq.plugins:lc-maven-plugins:0.0.1-SNAPSHOT:lc [-Dlc.file.includes=js,lua] [-X]
  • 1

注: 其他關於Maven Plugin開發可參考:Plugin Developers CentreMaven API Reference.


參考 & 擴展
Maven實戰
持續部署, 並不簡單
maven.apache.org
Guide to creating assemblies
Maven入門指南(一)
用 Maven 做項目構建
項目管理: Maven 讓事情變得簡單
Maven開始逃離 XML 陣營
Apache Maven JDeps插件3.0.0版本發布
Maven實戰(六)——Gradle,構建工具的未來?
Maven實戰(七)——常用Maven插件介紹(上)
Maven實戰(八)——常用Maven插件介紹(下)
Maven實戰(九)——打包的技巧
Gradle 2.0發布:簡單、快速、支持Java 8


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM