1. 程式人生 > >使用maven-pom進行依賴管理與自動構建

使用maven-pom進行依賴管理與自動構建

使用maven-pom進行依賴管理與自動構建

  • 使用maven-pom進行依賴管理與自動構建
    • 一、讓我們先了解一下maven
      • 1.maven是優秀的依賴管理工具
      • 2.maven是優秀的構建工具
      • 3.maven是約定大於配置、簡單易學的工具
    • 二、pom基本結構
      • 1.概覽
      • 2.專案座標
      • 3.package標籤
      • 4.專案間關係
      • 5.dependencies標籤
      • 6.dependencyManagement標籤
      • 7.Properties標籤
    • 三、配置構建任務
      • 1.maven構建的生命週期
      • 2.外掛的“構建階段”
      • 3.build標籤
      • 4.profiles標籤
      • 5.project標籤下的build標籤
      • 5.Reporting標籤

使用maven-pom進行依賴管理與自動構建

最後編輯於 :pencil::{docsify-updated}

一、讓我們先了解一下maven

1.maven是優秀的依賴管理工具

隨著開發生態環境的不斷髮展,幾乎所有的java應用都會使用第三方的類庫,尤其是在這個開源的世界裡, java應用依賴管理已經很難再由人工完成——依賴管理包括解決依賴傳遞、版本衝突、依賴臃腫等問題。

maven通過groupIdartifactIdversion形成的座標定位系統能準確的定位每一個構件(artifact), 開發者可以通過在pom檔案中列出所依賴的構件的座標,讓maven工具從maven倉庫中自動下載所有需要的構件; 另一方面,通過pom檔案間的依賴傳遞、繼承等方式降低依賴管理的難度。

2.maven是優秀的構建工具

在我們的開發過程中,除了編寫程式碼以外,有很大一部分時間是花在編譯、執行單元測試、生成文件、打包和部署這些工具上, 為也提高工作效率,使開發人員能更多的將精力用於開發工作,我們需要像maven這樣的工具, 如流水線般的將所有部署以自動化的方式完成。

3.maven是約定大於配置、簡單易學的工具

在java開發中,常用的構建工具有Ant、maven、gradle三種。

3.1 Ant

Ant是過程式的,開發者需要顯示的指定每個目標,以及完成該目標鎖需要執行的任務。

Ant不僅限於對java專案進行構建,也可以對其他語言(如C語言)的專案進行構建。

<project name="MyProject" default="dist" basedir="../../../../../../../ProgramFiles/JetBrains/IntelliJ IDEA 2019.3.2/jbr/bin">
  <description>
    simple example build file
  </description>
  <!-- set global properties for this build -->
  <property name="src" location="src"/>
  <property name="build" location="build"/>
  <property name="dist" location="dist"/>

  <target name="init">
    <!-- Create the time stamp -->
    <tstamp/>
    <!-- Create the build directory structure used by compile -->
    <mkdir dir="${build}"/>
  </target>

  <target name="compile" depends="init"
        description="compile the source">
    <!-- Compile the Java code from ${src} into ${build} -->
    <javac srcdir="${src}" destdir="${build}"/>
  </target>

  <target name="dist" depends="compile"
        description="generate the distribution">
    <!-- Create the distribution directory -->
    <mkdir dir="${dist}/lib"/>

    <!-- Put everything in ${build} into the MyProject-${DSTAMP}.jar file -->
    <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
  </target>

  <target name="clean"
        description="clean up">
    <!-- Delete the ${build} and ${dist} directory trees -->
    <delete dir="${build}"/>
    <delete dir="${dist}"/>
  </target>
</project>

與maven、gradle不同,ant本身沒有對應的中央倉庫,如果想使用maven一樣, 通過宣告的方式管理依賴,並自動處理依賴管理等問題,則使用整合ivy外掛管理依賴。 但ant不支援多模組的管理方式,而maven和gradle支援。

ant的這種宣告方式,使開發者可以根據自己的需要靈活配置,定製自己的專案構建流程; 另一方面,這種靈活的配置方式,也使得ant的配置相對繁瑣,可讀性較差,學習成本也更高 ——事實上,ant的官方網站上列出的外掛就有139種,而且部分工具的官網已經404了。

3.2 maven

maven與gradle一樣,都是宣告式的配置方式,相對於Ant來說,配置更加方便。 同時maven有內建生命週期,約定了專案的程式碼結構,只須簡單配置,就可以完成構建任務。

頂級pom中定義的專案結構(即預設專案結構)

專案根目錄
  |-- src
  |    |-- main
  |    |    |-- java        -> 主程式碼檔案
  |    |    |-- resources   -> 主資原始檔
  |    |     -- scripts     -> 雖然在maven的預設結構裡有這個目錄,但官網文件中已刪除了這一級目錄,不建議使用
  |     -- test
  |         |-- java        -> 測試程式碼檔案
  |          -- resources   -> 測試資原始檔
   -- pom.xml

小提示:可通過mvn -DarchetypeCatalog=internal archetype:generate初始化標準結構的maven專案

另一方面,maven內建生命週期,使用無法更加專案的構建順序,如果想在構建中加上一些其他處理邏輯, 則需要用編寫maven外掛的方式來完成,成本相對高昂。

3.3 Gradle

gradle是相對新穎的構建工具,它使用一種基於Groovy的特定領域語言(DSL)來宣告專案設定, 目前也增加了基於Kotlin語言的kotlin-based DSL,拋棄了基於XML的各種繁瑣配置。

gradle沒有maven般的生命週期,真正起作用的是所引入的Plugin,因此相對更為靈活。

另一方面,gradle作為新生工具,無論是程式還是文件都在不斷的完善中,使用和學習相對困難。 同時,由於gradle的配置是基於groovy/kotlin語法的,因此使用gradle需要掌握相應的語法知識。

gradle是對ant和maven特點折衷的結果,相對於ant其可讀性更好,相對於maven其更為靈活。

二、pom基本結構

POM是project object mode的簡寫,maven通過pom檔案對專案進行描述, 在開發者在pom檔案中對專案屬性和構建過程進行定義後,maven即可自動構建專案並生成站點。

1.概覽

<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>

    <!-- The Basics -->
    <groupId>...</groupId>
    <artifactId>...</artifactId>
    <version>...</version>
    <packaging>...</packaging>
    <dependencies>...</dependencies>
    <parent>...</parent>
    <dependencyManagement>...</dependencyManagement>
    <modules>...</modules>
    <properties>...</properties>

    <!-- Build Settings -->
    <build>...</build>
    <reporting>...</reporting>

    <!-- More Project Information -->
    <name>...</name>
    <description>...</description>
    <url>...</url>
    <inceptionYear>...</inceptionYear>
    <licenses>...</licenses>
    <organization>...</organization>
    <developers>...</developers>
    <contributors>...</contributors>

    <!-- Environment Settings -->
    <issueManagement>...</issueManagement>
    <ciManagement>...</ciManagement>
    <mailingLists>...</mailingLists>
    <scm>...</scm>
    <prerequisites>...</prerequisites>
    <repositories>...</repositories>
    <pluginRepositories>...</pluginRepositories>
    <distributionManagement>...</distributionManagement>
    <profiles>...</profiles>
</project>

2.專案座標

宣告式的專案依賴管理,除了需要一個儲存有所有jar包的中央倉庫外,還需要一套合適的命名系統, 能夠保證每個jar包都有一個唯一的名字,包括不同版本。
在maven中,jar包的座標通過groupIdartifactIdversion三個欄位決定, 類似於xyz座標系的三個軸。

<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>org.codehaus.mojo</groupId>
  <artifactId>my-project</artifactId>
  <version>1.0</version>
</project>
2.1 groupId

通常而言,一個組織(公司、基金會等)的專案通常使用同一個groupId, 因此一般而言groupId是該組織所擁有的域名,如jacoco對應的groupId為org.jacoco, 而對應的網站為http://jacoco.org(域名的優先順序順序是反過來的),

事實上不一定嚴格遵循該規則,如maven的groupId為org.apache.maven, 而maven官方外掛的groupId為org.apache.maven.plugins, 儘管同並不存在plugins.maven.apache.org這個四級域名, 而且他們同屬於apache基金會(apache.org)下的專案。

另外也有完全和域名不相關的,如junit包的groupId為junt

另外需要注意的一點是,雖然groupId不必與專案的包結構相對應,但是遵循這種做法是一種最佳實踐, 因為這樣可以有效避免包之間的類衝突。

2.2 artifactId

artifactId通常是已知專案(或專案模組)的名稱,它與groupId一起構成了一個專案(或專案模組)的定位, 用於將該專案(或專案模組)與世界上其他所有專案(或專案模組)分開。

實踐

雖然在groupId中就可以看到專案名稱,但在artifactId中一般仍會將專案名稱作為開頭, 這是因為我們在溝通時通常會用 artifactId 進行交流,而不會帶上 groupId , 如張三告訴李四需要在專案中引入maven-enforcer-plugin包做專案自動檢查。

2.3 version

如同字面的含義,version代表的是專案(或專案模組)的版本。

groupIdartifactIdversion一同定位了一個釋出包的唯一位置。 如maven-enforcer-plugin-3.0.0-M3.jar在倉庫中的位置為 $M2_REPO/org/apache/maven/plugins/maven-enforcer-plugin/3.0.0-M3, 而其對應的pom中的內容如下:

<parent>
  <groupId>org.apache.maven.enforcer</groupId>
  <artifactId>enforcer</artifactId>
  <version>3.0.0-M3</version>
</parent>

<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>

parent標籤我們後續會提到,parent標籤下是父pom的座標,子pom繼承父pom的所有定義,包括version

3.package標籤

package標籤定義了構件的打包方式和使用方式,如果不指定值,預設使用jar作為打包方式。

這裡只說明兩個我們使用較多的打包方式:pom、jar。

3.1 打包為pom

定義打包方式為pom是有一些特殊的專案,其本身除了pom檔案外,不包含其他檔案, 這些專案中的pom主要是用來被其他專案引用、繼承,或者用來管理專案擁有的模組。

這裡的繼承概念與java中類的繼承概念類似,子pom擁有父pom的所有配置, 可以通過自定義的方式覆蓋/複寫父pom中已經存在的配置。

我們拿springboot整合中的兩個打包方式為pom的專案舉例說明。 在springboot的官方整合教程中,會告訴你可以使用兩種方式整合,一種通過父pom繼承:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.1.RELEASE</version>
</parent>

另一種是通過引入spring-boot的依賴管理:

<dependencyManagement>
    <dependencies>
        <dependency>
            <!-- Import dependency management from Spring Boot -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.1.1.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

如果我們找到spring-boot-starter-parent專案,就會發現這個專案裡僅僅只有pom檔案 springboot-starter

這是因為在專案中,我們通常會存在多個子模組,而這些子模組會需要一些公用的配置, 比如統一的構建流程、統一的依賴版本管理,而這些內容就以pom的形式抽取出來, 形成了這種打包方式為pom的專案。

需要注意的是,打包方式為pom的專案裡,除了pom檔案,其他檔案不會參與構建。 事實上如果我們去看pom打包對應的預設外掛配置就會發現,只有install和deploy兩個生命週期有外掛繫結:

<phases>
  <install>
    org.apache.maven.plugins:maven-install-plugin:2.4:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
  </deploy>
</phases>
3.2 打包為jar

jar包是java程式包常用的釋出形式,其可以作為庫檔案被其他專案使用, 也可以通過java -jar命令直接執行。

jar包中即有靜態資源,也有編譯後生成的class檔案,這意味著其打包流程也更為複雜:

<phases>
  <process-resources>
    org.apache.maven.plugins:maven-resources-plugin:2.6:resources
  </process-resources>
  <compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
  </compile>
  <process-test-resources>
    org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
  </process-test-resources>
  <test-compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
  </test-compile>
  <test>
    org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
  </test>
  <package>
    org.apache.maven.plugins:maven-jar-plugin:2.4:jar
  </package>
  <install>
    org.apache.maven.plugins:maven-install-plugin:2.4:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
  </deploy>
</phases>
3.2 其他

目前maven支援這些打包方式: pom、jar、maven-plugin、ejb、war、ear、rar。 其中每種打包方式都有相應的打包外掛對其進行打包,這些外掛都已經被maven預設進行了繫結, 具體打包方式對應的外掛配置有興趣的可以去 官網進行檢視。

4.專案間關係

在上文中我們提到了多種專案間關係,包含依賴關係、繼承關係、聚合關係(上文提到的模組管理)。

4.1依賴關係

專案間依賴關係是最常用的關係,當我們專案A使用一個專案B打成的jar時,A就對B形成了依賴。

需要注意的是,如果的java專案開發已經無法離開依賴了,A所依賴的專案B,實際上也開發中也依賴了專案C。 此時就生成的依賴的傳遞,即如果A依賴了B,B依賴了C,那麼A依賴C。

依賴傳遞的存在,會使得單點的依賴關係也變得複雜起來。實際在專案中,我們會依賴大量的jar包, 此時就可能存A依賴的B、C同時使用兩個不同版本的D:

A
 |-B
    |-D:1.1
 |-C
    |-D:1.2

我們知道,一個專案的不同版本通常會有大量的類是重複了,如果jvm在載入同名類時,會忽略重複的類, 這樣可能導致出現其中一個包只有一半程式碼的情況,這明顯是不合理的。這種依賴的出現就叫到依賴衝突, 當出現衝突時,maven會選取其中的一個版本放入最終的打包後文件中(如jar),而忽略其他版本。

具體的選取策略可以參考這篇短文

4.2繼承關係

繼承關係我們在上文中已經提到,我們可以通過parent標籤引入父級pom, 此時子pom擁有了父級pom中的所有定義,並可以在子pom中複寫相應的定義。

由於pom的繼承關係可能有多級,因此如果你想確認當前專案的最終pom結構時, 就必須要將所有的pom整合到一起,此時我們可以通過mvn help:effective-pom命令生成整合後的pom (如果使用idea,右鍵maven選單下也有相應的生成操作)。

如果在一個沒有父pom的專案中執行mvn help:effective-pom,你會發現最終生成的pom中, 突然出現了大量你沒有定義的內容,這是因此maven自身定義了一個super pom, 所有的pom都預設繼承該pom(可類比java中的Object類), 在上文中我們提到的maven專案的預設結構,就是在這裡定義的。

super pom的具體內容,可以去官網檢視

子pom並不會繼承父pom的所有內容,僅以下內容將會被繼承: - groupId - version - scm - issueManagement - ciManagement - mailingLists - properties - dependencyManagement - dependencies - repositories - pluginRepositories - build - reporting - profiles - description - url - inceptionYear - organization - licenses - developers - contributors

需要注意的是以下幾個標籤不會被繼承: - artifactId - package - name - prerequisites

4.3聚合關係

聚合關係常常出現在有多個模組的專案中,比如專案A有三個子專案aa、ab、ac, 這時我們可以在目錄A下也新增一個pom,並定義aa、ab、ac是專案A的三個子專案, 這樣我們對專案A做構建(如mvn clean package)時,maven就會分別對aa、ab、ac做相應的構建操作。

A
 |-- pom.xml
 |-- aa
       |-- pom.xml
 |-- ab
       |-- pom.xml
 |-- ac
       |-- pom.xml

子專案的宣告方式以通過modules標籤進行,當我們在專案A的pom中新增如下配置, aa、ab、ac就會成為A的子專案:

<modules>
    <module>aa</module>
    <module>ab</module>
    <module>ac</module>
</modules>

aa、ab、ac並不是這三個專案的相對路徑(相對於A)

在這裡,可能有的人已經注意到了,這種專案層級結構,非常適合將A專案的pom作為父pom, 將aa、ab、ac的pom中相同的內容抽取至父pom中進行管理。
實際上在專案的實踐中,通常也是這麼處理的,但也有一些例外。比如aa、ab是springboot專案, 而ac卻是老舊的spring專案,此時可能出現:A作為aa和ab的父級pom統一管理相關配置, 同時aa和ab作為子專案存在,而ac不繼承A,只作為子專案存在,這樣我們即能夠統一管理aa和ab的配置, 也能夠通過一條命令,對aa、ab、ac一起完成構建操作。 (實際實踐過程中,不推薦這種模式,我們可以把ac提到與A平級的位置,這樣的結構更清晰)

值得一提的是,如果aa與ab間存在依賴關係,比如aa依賴於ab,那麼即使aa比ab先宣告,maven在構建時, 也會很聰明的先對ab進行構建,再構建aa。

5.dependencies標籤

<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <type>jar</type>
      <scope>test</scope>
      <optional>true</optional>
    </dependency>
</dependencies>

dependencies標籤下聲明瞭當前專案所使用的依賴構件。

需要注意的是,這裡宣告的檔案必須能夠在maven倉庫中找到, maven預設使用中央倉庫,中央倉庫中包括了所有常用的開源jar包, 但對於一些閉源的三方jar包,就無法在中央倉庫中找到。
因此,公司一般都會有自己的私有倉庫,我們需要修改maven的settings.xml檔案, 使maven使用私服處理依賴和釋出。
對於私有倉庫上沒有的專案,我們可能通過以下命令,將其上傳至私有倉庫:

shell script mvn deploy:deploy-file -Dfile=non-maven-proj.jar -DgroupId=some.group \ -DartifactId=non-maven-proj -Dversion=1.0 -Dpackaging=jar

5.1 classifier標籤

在引用依賴時,僅僅使用groupIdartifactIdversion作為座標可能還並不太足夠。 考慮一下這種場景:有一個專案,該專案需要提供一個基於jre1.7的jar包, 同時還提供了一個基於jre1.8的jar包,即一套原始碼需要使用兩種jdk打成不同的jar包。

這種情況下我們會配置兩套打包方式,預設使用1.8打包,並可選擇1.7打包,配置如下:

<profile>
    <id>jdk17</id>
    <properties>
        <!--使用jdk1.7打包-->
        <java.version>1.7</java.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-jar-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                        <configuration>
                            <!--配置classifier,與1.8版本的包區分-->
                            <classifier>jdk17</classifier>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

如果用1.8打包釋出的jar名稱為demo-1.0.jar,那麼用1.7打包時, 釋出的jar的名稱為demo-1.0-jdk17.jar——實際場景下,classifier標籤可以配置成任意字串, 並不一定是jdk17。

而使用者在引入1.7版本的依賴時,就需要額外新增classifier標籤

<dependency>
  <groupId>some.example</groupId>
  <artifactId>demo</artifactId>
  <version>1.0</version>
  <classifier>jdk17</classifier>
</dependency>

classifier標籤有一處更常見的地方,是sources.jar:
classifier_sources

這就是我們上文中說到的一個pom,打出了兩個包,原始碼包對應的classifier標籤就是sources, 而測試原始碼包對應的值為test-sources

5.2 type標籤

type標籤描述了依賴型別,如果不定義該值,使用預設值jar。
對於不同型別的依賴,maven的處理策略也不相同:

typeclassifierextensionpackaginglanguageadded to classpathincludesDependencies
pom   = type = type none    
jar   = type = type java true  
test-jar tests jar jar java true  
maven-plugin   jar = type java true  
ejb   jar = type java true  
ejb-client client jar ejb java true  
war   = type = type java   true
ear   = type = type java   true
rar   = type = type java   true
java-source sources jar = type java    
javadoc javadoc jar = type java true  

實際開發中,我們只會用到jarpom兩種型別,大家可以注意到pom型別的依賴是不會產生依賴傳遞, 也不會被加入classpath中。

5.3 scope標籤

該標籤定義了該包在哪些階段會被放到classpath中,另外還定義了依賴的可傳遞性。其聚會範圍如下:

classpath即類、類庫載入路徑,在我們的依賴中,大多數依賴會在編譯、測試、執行都用到, 但也有一些依賴可能只有在編譯、測試階段運到(比如測試相關的包),還有部分包可能在編譯、測試時需要, 但實際釋出時不需要(比如jdk相關的包)。

含義是否會將該依賴向下傳遞
compile 該值是預設值。該依賴在所有階段的都會放入classpath中,是最常用的值。
provided 只有在編譯和測試時放入classpath中,實際打包時忽略該包
runtime 編譯時不需要,只在測試和打包時需要
test 只在對測試程式碼做編譯和執行時需要
system 手動指定所依賴包的本地存在路徑 只傳遞本身
  • compile是最常用的,一般專案引入的包在編譯、測試、釋出階段都會用到。
  • provided一般用於基礎包,在基礎包的開發過程中會依賴一些三方包, 但可能不想為使用者帶來依賴衝突的負擔,此時可以使用該範圍,由使用者自行決定該三方包的版本 (當然需要是相容的版本);另一種不推薦的使用方式是將該依賴作為可選依賴向下傳遞, 舉一個例子:我們開發了一個基礎工具類包,裡面含有許多工具,像poi工具、時間轉換工具、中文數字工具, 另外裡面還包含了TapConfigUtil用來獲取配置, 此時我們注意到配置中心的整合是以springboot為基礎的, 而我們想讓該工具包也可以被其他型別的專案使用, 這種情況下我們可以給commons-aap-appconfig包加上provided範圍。 如果某個使用者只不需要使用TapConfigUtil,那麼他就不需要引入commons-aap-appconfig包。 (事實上commons-aap-appconfig包裡引入spring相關的依賴裡使用了provided範圍)
  • runtime很少用到,一般情況下使用compile就可以了, 使用runtime甚至可能給IDE帶來一些問題
  • test用在測試相關的包上,測試程式碼的安全性會更低(畢竟沒人會寫測試程式碼來測試測試程式碼 ——對外發布的測試工具除外),使用test標記測試相關的包,可以避免被其他開發人員錯誤的使用。
  • system需要配置systemPath標籤使用,用來指定使用倉庫中沒有的包。 與其相比,上傳私服是一種更好的方式,因為system標記的包,並不會將其依賴的包傳遞給使用者, 這意味著你需要自己處理好依賴關係。
5.4 systemPath標籤

僅在依賴項範圍是system時使用,否則構建將失敗。
systemPath標籤中的路徑必須是絕對路徑,因此建議使用maven變數拼裝路徑, 例如${project.basedir}/lib。

5.5 optional標籤

在上面介紹scope標籤的provided取值時,我們講了它可以實現讓使用都按需決定是否引入依賴, 這樣以達到避免引入不需要的依賴。
事實上provided更多的是用來要求使用方或執行時系統提供具體的包,而不是用於這種用途。
配置依賴的可選應該使用optional標籤來實現。

5.6 exclusions標籤

dependency標籤下還存在exclusions標籤,我們在前面講到了依賴傳遞可能帶來的依賴衝突問題, exclusions標籤就是排除掉不想被傳遞過來的依賴。

這裡需要注意的是,A->B->C時,如果你在引入依賴A的時候,將B排除,那麼C也將被排除, 除非有其他地方也引入了C(有點像對“樹”進行操作,排除進相當於直接剪掉了一個“子樹”)

<exclusions>
    <exclusion>
        <groupId>org.objenesis</groupId>
        <artifactId>objenesis</artifactId>
    </exclusion>
</exclusions>

exclusion下只有groupIdartifactId兩個標籤,因為只不可能同時依賴一個包的不同版本。

5.7 其他

version標籤除了指定具體版本外,還可以指定範圍版本,有興趣的可以檢視 官網, 不過不推薦在專案中使用。

各小版本在maven專案中的先後釋出順序可以看 這裡

6.dependencyManagement標籤

正如其名字所代表的含義,該標籤的作用就是管理依賴。
在上面的dependency標籤的介紹中,我們看到在dependency標籤下有眾多的子標籤, 想象一下我們有多個服務,如果每個服務都在引入同一依賴時配置好所有內容, 那將是非常麻煩,而且如果需要對某個所依賴的jar包做升級時,我們不得不改每一個pom中的version資訊。

可能有人已經想到,我們可以將這些依賴放到父pom裡面就可以了。這是一種很好的策略, 但讓我們考慮專案A、B,A中引入了a、b三個包,B中引入b、c兩個包, 如果我們使用父pom管理公用依賴,將只有b會被放到父pom裡,而a、c由A、B自己管理。 如果後來B專案的功能增加,也需要引入a包,那麼我們就需要把a包從A專案中提到父pom中, 以保證對依賴的統一管理——因為,這種方式並不能真正實現依賴的統一管理。

現在考慮另一種方式,我們在父pom裡列出專案中所有使用到的依賴包,並配置好所有內容, 但並不真正的對這些包產生依賴,僅僅是列出來。如果那個子pom真正需要用到, 那麼他只需要宣告一些儘可能少的資訊,就可以對對應的包產生依賴 ——這種方案就是使用dependencyManagement標籤

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring.boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.demo.company</groupId>
            <artifactId>someproject-commons-versions</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

dependencyManagement標籤下只有一個dependencies標籤, 裡面的內容和上文中的dependencies標籤幾乎一樣,唯一不一樣的是, 在dependencyManagement標籤下的scope標籤新增一種取值——import。

import只能用於匯入package值為pom的依賴包, 當在專案A的pom中使用import匯入專案B時,A、B的dependencyManagement標籤下的內容將被合併。 這是一個非常有用的特性,這意味著我們可以建立一個專案專門用來做統一版本控制, 在這個專案裡,我們將所有的包的versionscope等內容都定義好, 其他專案只需要在dependencyManagement下匯入該包,而不需要破壞其本身的繼承結構。

雖然dependencyManagement有這樣的方便, 但需要注意的是dependencyManagement下的內容不僅對當前pom、子pom生效, 同樣會覆蓋由於依賴傳遞而引入的jar包。比如在專案A中,我們依賴了aa-1.0、bb-2.0, 而在bb-2.0中依賴了aa-2.0,此時,如果aa的版本是在dependencyManagement中定義, 那麼aa-2.0將被放棄,aa-1.0生效,這樣將導致bb-2.0可能使用了根本不存在的類和api, 最終專案構建失敗。

7.Properties標籤

如其字面意思——屬性。properties標籤下定性的屬性,可以在pom的任何地方被引用, 引用方式為${屬性名},其中屬性名即標籤名,我們可以利用這些屬性管理一組依賴, 或者將一些容易變化的值集中到properties標籤下管理。

<properties>
    <org.powermock.version>2.0.2</org.powermock.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-testng</artifactId>
        <version>${org.powermock.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>${org.powermock.version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>${org.powermock.version}</version>
        <scope>test</scope>
    </dependency>
</dependencies>

除了自定義屬性外,maven外掛也會提供一些屬性,以達到對其進行配置的目的。 同時,maven還提供以下屬性可以在pom檔案中使用: - env.X : 以字首env開頭的屬性,對應於系統環境變數, 比如通過${env.PATH}可以取到環境變數PATH的值。這裡需要注意的是, maven對環境變數的大小寫是敏感的,windows下的環境變數在maven中必須全部大寫。 - project.X : project為字首是屬性是專案的屬性值, 包括了pom中的標籤——比如${project.parent.artifactId}可以獲取 *<parent>* 下 *<artifactId>* 的值;除此之外還有一些其他屬性,具體可參考 [官網說明](http://maven.apache.org/ref/3.6.3/maven-model-builder/#Model_Interpolation)。 值得一提是,該屬性值並不是取當前pom中的標籤,而是取**effective-pom**中的標籤, 也就是說可以取到從父pom中繼承的一些內容。 - settings.X : 與**project.X**類似,還過**settings.X**取的是**settings.xml**中的內容, 具體內容同樣參考 [官網說明](http://maven.apache.org/ref/3.6.3/maven-model-builder/#Model_Interpolation)。 - java系統屬性 : 所有可以通過`java.lang.System.getProperties()`方法獲取的變數, 均可以在pom中獲取,如**${java.home}**。具體可參考 oracle官方文件

三、配置構建任務

在上一章的內容是pom的一些基礎標籤的使用,靈活使用這些基礎標籤就可以達成專案的基本構建任務。

除了基本構建外,我們還往往在構建時需要進行打包前依賴衝突檢測、釋出前進碼檢查、 釋出時同步釋出原始碼包、自動生成並部署專案站點等任務,maven作為強大的構建工具, 通過構建外掛的方式,將所有的這些任務全部沿著maven構建生命週期自動執行。

1.maven構建的生命週期

maven構建的生命週期定義了打包和釋出專案時需要經歷的流程,maven內建的生命週期有 clean(清理)、default(打包釋出)、site(生成站點文件)三種。

這三種生命週期由一些階段(phase)組成,三種生命週期對應的階段如下:

1.1 clean生命週期
PhaseDescription
pre-clean execute processes needed prior to the actual project cleaning
clean remove all files generated by the previous build
post-clean execute processes needed to finalize the project cleaning
1.2 default生命週期
PhaseDescription
validate validate the project is correct and all necessary information is available.
initialize initialize build state, e.g. set properties or create directories.
generate-sources generate any source code for inclusion in compilation.
process-sources process the source code, for example to filter any values.
generate-resources generate resources for inclusion in the package.
process-resources copy and process the resources into the destination directory, ready for packaging.
compile compile the source code of the project.
process-classes post-process the generated files from compilation, for example to do bytecode enhancement on Java classes.
generate-test-sources generate any test source code for inclusion in compilation.
process-test-sources process the test source code, for example to filter any values.
generate-test-resources create resources for testing.
process-test-resources copy and process the resources into the test destination directory.
test-compile compile the test source code into the test destination directory
process-test-classes post-process the generated files from test compilation, for example to do bytecode enhancement on Java classes.
test run tests using a suitable unit testing framework. These tests should not require the code be packaged or deployed.
prepare-package perform any operations necessary to prepare a package before the actual packaging. This often results in an unpacked, processed version of the package.
package take the compiled code and package it in its distributable format, such as a JAR.
pre-integration-test perform actions required before integration tests are executed. This may involve things such as setting up the required environment.
integration-test process and deploy the package if necessary into an environment where integration tests can be run.
post-integration-test perform actions required after integration tests have been executed. This may including cleaning up the environment.
verify run any checks to verify the package is valid and meets quality criteria.
install install the package into the local repository, for use as a dependency in other projects locally.
deploy done in an integration or release environment, copies the final package to the remote repository for sharing with other developers and projects.
1.3 site生命週期
PhaseDescription
pre-site execute processes needed prior to the actual project site generation
site generate the project's site documentation
post-site execute processes needed to finalize the site generation, and to prepare for site deployment
site-deploy deploy the generated site documentation to the specified web server

每個生命週期擁有的階段互不重疊,且在構建時每個生命週期中的階段按順序進行。 這意味著當我們使用mvn命令構建時,如果指定了某一階段,maven會先執行所有前置階段, 再執行指定的階段——這使得maven的使用相當容易,使用都只需要記住常用的幾個階段 (如clean、deploy、site)即可單獨完成專案的構建和釋出。

2.外掛的“構建階段”

除了maven內建的階段外,maven允許外掛自定義階段,比如maven-dependency-plugin 外掛中常用的copy-dependencies階段。外掛階段與內建階段一樣可以使用mvn命令執行, 唯一不同的是需要在階段前加上外掛的座標資訊,如mvn dependency:copy-dependencies (使用非maven官方外掛時,需要將dependency換成groupId:artifactId的形式)。

在使用這些外掛前,我們需要在pom檔案中引入外掛:

<build>
  <plugins>
    <plugin>
      <artifactId>maven-dependency-plugin</artifactId>
      <version>2.8</version>
    </plugin>
  </plugins>
</build>

事實上基本構建外掛maven都有內建,super pom中甚至進行了缺少配置,不需要我們再手動新增

雖然maven允許外掛自定義階段,但不允許修改內建的生命週期,

3.build標籤

build標籤下定義了構建的相關資訊和配置:

<build>
  <defaultGoal>install</defaultGoal>
  <directory>${basedir}/target</directory>
  <finalName>${artifactId}-${version}</finalName>
  <resources>
    <resource>
      <filtering>true</filtering>
      <directory>${basedir}/src/main/resources</directory>
      <includes>
        <include>**/application*.yml</include>
        <include>**/application*.yaml</include>
        <include>**/application*.properties</include>
      </includes>
    </resource>
    <resource>
      <targetPath>META-INF/plexus</targetPath>
      <filtering>false</filtering>
      <directory>${basedir}/src/main/plexus</directory>
      <includes>
        <include>configuration.xml</include>
      </includes>
      <excludes>
        <exclude>**/*.properties</exclude>
      </excludes>
    </resource>
  </resources>
  <testResources>
    ...
  </testResources>
  <filters>
    <filter>filters/filter1.properties</filter>
  </filters>
</build>
3.1 defaultGoal

預設構建目標(階段),如果配置該值,在執行mvn命令時可以不指定構建階段。

<defaultGoal>clean package</defaultGoal>
3.2 directory

構建生成的相關檔案的存放位置,預設配置為${project.basedir}/target

3.3 finalName

構建生成jar包(或其他型別的包)的名稱,預設為${artifactId}-${version}

需要注意的是,finalName並非如字面意思一般是打包後的最終名稱, 實際打包外掛可能會在後面新增字尾:如果我們配置finalName配置為demo, 同時為打包外掛配置classifier標籤為jdk17(上面我們講到過), 那麼實際生成的jar名稱為demo-jdk17.jar。

3.4 resources 與 testResources

resources標籤用來指定資原始檔的存放路徑,這些檔案不會參與編譯,會直接拷貝到相應的路徑下。

  • targetPath: 拷貝目標路徑,在default週期的process-resources階段,資源將被拷貝到對應的路徑下。 如果不指定,檔案將被拷貝至根目錄下。
  • filtering: 是否啟用過濾,如果啟用,則允許在resource指定的檔案中使用佔位符替換 ,預設不啟用。這裡的替換範圍包括所有在pom中可以使用的變數,替換方式為${placeholder} ——這是官方文件的說法,實際在spring boot專案中的有效使用格式為@placeholder@
  • directory: 資原始檔的源路徑,預設為${project.basedir}/src/main/resources
  • includes: 源路徑下需要包含的檔案,允許使用萬用字元的方式匹配
  • excludes: 源路徑下需要排除的檔案,允許使用萬用字元的方式匹配

testResources中的內容與resources一致,不同的是其對應的生命週期和directory的預設值。

3.5 filter

filter可以用來引入一些properties檔案,在這樣檔案裡定義的屬性, 也會加入到開啟了filtering的資原始檔的佔位符替換範圍。

3.6 plugins 與 pluginManagement
3.6.1 plugins

plugins標籤用於引入和配置打包外掛。

<build>
  ...
  <pluginManagement>
    <plugins>
        ...
    </plugins>
  </pluginManagement>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <version>2.6</version>
      <extensions>false</extensions>
      <inherited>true</inherited>
      <configuration>
        <classifier>test</classifier>
      </configuration>
      <dependencies>...</dependencies>
      <executions>...</executions>
    </plugin>
  </plugins>
</build>
  • extensions: 是否載入該外掛擁有的擴充套件包(後方會講到),預設為false
  • inherited: 是否允許子級pom繼承,預設為true
  • configuration: configuration下的內容較為特殊,maven並未約束其子級標籤的範圍。 實際上子標籤對應是的外掛的可配置屬性,因此configuration下的內容需要通過閱讀外掛的使用文件來進行配置 這裡需要注意的是,無論configuration出現在父級pom還是在pluginManagement中, 其中的定義屬性最終會在合併後傳遞給外掛。 (合併策略參考官方網站)
  • dependencies: 與專案的dependencies結構一致,不同的是這些依賴不作用於專案,而是作用的外掛, 通過指定依賴,我們可以用來指定外掛依賴的包的版本等資訊。
  • executions: 對外掛提供的maven階段進行配置,該標籤下的內容較為複雜,實際使用也比較多,值得單獨一講

executions下包含的是execution列表,每個execution可以理解為一個任務。

<plugin>
  <artifactId>maven-antrun-plugin</artifactId>
  <version>1.1</version>
  <executions>
    <execution>
      <id>echodir</id>
      <goals>
        <goal>run</goal>
      </goals>
      <phase>verify</phase>
      <inherited>false</inherited>
      <configuration>
        <tasks>
          <echo>Build Dir: ${project.build.directory}</echo>
        </tasks>
      </configuration>
    </execution>
  </executions>
</plugin>
  • id: 任務的id,用來區分不同的execution,該值會在該任務執行時打印出來, 格式:[plugin:goal (id)]
  • goals: 需要外掛執行的構建目標(階段),每個外掛具體提供的構建目標可以參考官方文件, 或者通過maven-help-plugin檢視外掛的資訊: mvn help:describe -Dplugin=com.github.spotbugs:spotbugs-maven-plugin:3.1.12.2
  • phase: 指定需要繫結的maven內建構建階段。在這裡需要注意,外掛的構建階段為goal, 內建構建階段為phase,因為外掛的階段是沒有生命週期的。phase標籤的作用, 就是將execution對應的任務繫結到maven的某個內建構建階段,在maven生命週期時, 當執行到該phase時,會自動執行該execution下的goals,從而對maven構建進行擴充套件。 比如我們可以將pmd外掛的pmd、check關聯到verfiy階段,這樣當我們執行mvn verify時, 會自動進行程式碼問題的檢查。
  • inherited: 是否允許該execution被子pom繼承,預設為true
  • configuration: 與外層的configuration內容一致,不過僅對該execution生效
3.6.2 pluginManagement

pluginManagement的作用與dependencyManagement的作用類似,我們可以在父pom裡配置所有外掛, 但不實際引入外掛,子pom按需在plugins標籤下引入具體外掛,這樣就可以達到外掛配置集中管理的效果 —— 同dependencies下引入dependencyManagement中的依賴一樣, plugins下引入pluginManagement中的外掛只需要宣告groupIdartifactId

<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <artifactId>maven-antrun-plugin</artifactId>
        <version>1.1</version>
        <executions>
          <execution>
            <id>echodir</id>
            <goals>
              <goal>run</goal>
            </goals>
            <phase>verify</phase>
            <inherited>false</inherited>
            <configuration>
              <tasks>
                <echo>Build Dir: ${project.build.directory}</echo>
              </tasks>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </pluginManagement>
  <plugins>
    <plugin>
      <artifactId>maven-antrun-plugin</artifactId>
    </plugin>
  </plugins>
</build>

4.profiles標籤

profiles允許我們定義多個profile,並設定每個profile的啟用條件,以達到自如切換多套配置的目的。

<profiles>
  <profile>
    <id>test</id>
    <activation>...</activation>
    <build>...</build>
    <modules>...</modules>
    <repositories>...</repositories>
    <pluginRepositories>...</pluginRepositories>
    <dependencies>...</dependencies>
    <reporting>...</reporting>
    <dependencyManagement>...</dependencyManagement>
    <distributionManagement>...</distributionManagement>
  </profile>
</profiles>
4.1 id

如字面意思,即套profile的標識,我們可以通過mvn命令的-P選項直接通過id啟用對應的profile: mvn -P test clean deploy

4.2 activation

profile的自動啟用條件。

除了通過手動指定profile外,還可以根據一些條件,自動啟用profile

<profile>
  <id>test</id>
  <activation>
    <activeByDefault>false</activeByDefault>
    <jdk>1.5</jdk>
    <os>
      <name>Windows XP</name>
      <family>Windows</family>
      <arch>x86</arch>
      <version>5.1.2600</version>
    </os>
    <property>
      <name>sparrow-type</name>
      <value>African</value>
    </property>
    <file>
      <exists>${basedir}/file2.properties</exists>
      <missing>${basedir}/file1.properties</missing>
    </file>
  </activation>
</profile>

其中activeByDefault表示是否為預設啟用,預設值為false。如果配置為true, 當沒有配置檔案被啟用時,啟用該配置檔案

其他的標籤是一些具體啟用條件,在maven3.2.2版本只要滿足一項啟用條件就成立(或關係), 在maven3.2.2以後必須滿足所有項啟用條件才成立(與關係): - jdk: 指定構建時的jdk版本,除了指定具體版本外,也可以指定範圍 - os: 作業系統資訊,具體約束條件內容可檢視這裡 - property: 將該屬性的key-value滿足條件時該項成立。這裡的屬性的範圍與pom檔案一致。 - file: 檔案存在或缺失,配置路徑須為絕對路徑。該項需要注意的是,file標籤下的屬性替換, 只允許出現basedir、系統變數、maven命令傳遞變數這三種。

在我們配置好profiles,如果想知道在當前環境下哪個配置將被啟用, 可以通過mvn help:active-profiles檢視。

4.3其他標籤

modulesdependenciesdependencyManagement標籤我們在前文中已經講過, profile下這些標籤的可配置內容與其在根標籤(project)下的一致, profile下的配置可以覆蓋根標籤下的配置。

repositoriespluginRepositoriesreportingdistributionManagement 這些標籤在根標籤下也存在,只是我們還沒有聊到,與上面的這些標籤一樣, 這些標籤的可配置內容與其在根標籤下的一致,profile下的配置可以覆蓋根標籤下的配置。

build標籤較為特殊,其在profile下的可配置內容只包含前文中我們提到的所有內容, 但在根標籤下的build標籤還有一些獨佔內容我們沒有提到。

5.project標籤下的build標籤

project標籤下的build標籤下還有幾個標籤,這些標籤僅出現在project標籤下的build中, 無法在profile中的內容配置。

這些標籤包括目錄結構標籤——sourceDirectorysourceDirectorytestSourceDirectoryoutputDirectorytestOutputDirectory,以及extensions標籤。

5.1 *Directory標籤

這些標籤定義了專案的目錄結構,標籤的含義和其英文名字一致,標籤的值必須為絕對路徑 (一般使用`${project.basedir}進行拼接)。

這樣需要注意的是除了上述的幾個標籤外,我們還可以找到一個scriptSourceDirectory標籤, 該標籤實際沒有用處,已經被官宣廢棄。

5.2 extensions標籤

extensions下可以引入一些jar對maven能力進行擴充套件,比如引入wagon-ssh新增對ssh協議的支援, 與plugins中的外掛不同,擴充套件程式不需要提供構建目標(gloas),只是提供能力基礎, 因此需要另行引入外掛配合使用。

我們舉一個利用ssh協議進行包自動部署的例子:

<build>
  <extensions>
    <groupId>org.apache.maven.wagon</groupId>
    <artifactId>wagon-ssh</artifactId>
    <version>2.10</version>
  </extensions>
  <plugins>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>wagon-maven-plugin</artifactId>
      <version>1.0</version>
      <configuration>
        <serverId>devServerRoot</serverId>
        <formFile>target/demo.jar</formFile>
        <url>scp://[email protected]:/root/applications/demo</url>
        <commands>
          <command>/root/applications/demo/restart.sh</command>
        </commands>
        <desplayCommandOutputs>true</desplayCommandOutputs>
      </configuration>
    </plugin>
  </plugins>
</build>

其中serverId中的的devServerRoot於server標籤中的idserver標籤下可以定義伺服器的賬號密碼資訊,一般在settings.xml中配置,也可以在pom中配置。

<server>
  <id>devServerRoot</id>
  <username>root</username>
  <password>password</password>
</server>

5.Reporting標籤

reporting下的內容與site生命週期相關,reporting標籤下可以定義一些外掛用於生成報告文件, 比如javadoc。
build類似,reporting也提供了外掛的配置能力,與build不同的是, reporting下的外掛無法被繫結到指定的maven階段(預設繫結site階段)

<reporting>
  <plugins>
    <excludeDefaults>false</excludeDefaults>
    <outputDirectory>${basedir}/target/site</outputDirectory>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-javadoc-plugin</artifactId>
      <version>3.1.1</version>
      <reportSets>
        <reportSet>
          <id>sunlink</id>
          <reports>
            <report>javadoc</report>
          </reports>