Maven的安装-配置-核心概念

Fenice Liu
Fenice Liu
发布于 2025-02-22 / 42 阅读
0
0

Maven的安装-配置-核心概念

——Java构建工具Maven[01]

在今天的互联网相关开发工作之中以Java为基础的开发环境和各类框架以及SDK百花齐放,如果从头造轮子或者手动管理各种依赖包显然是不可行的,更不要提工程开发之中的CI/CD、测试、文档、容器化部署等等工作了。对比C/C++开发领域的Makefile或者CMake这样的自由度极高但是通常生态缺损管理混乱的情况来说,Java侧的构建工具要用户友好的多。事实上最先可用并且好用的Java构建工具是Ant,然而随着时代的车轮滚滚向前,目前大行其道的是Maven以及正在丰富自身以求获得目光的Gradle。如果从事Java开发工作,那么对于Maven不会用不了解简直是贻笑大方,本文就从最基础的安装和配置,以及最重要的Maven的核心概念介绍这一工具。

1.Maven安装与基本使用

Maven作为一个构建与依赖管理工具,其安装并没有太多的要求,参照官方给出的下载页面即可获取当前最新版本的编译后二进制可执行文件。安装Maven的要求有这样两点:

  • 必须已经安装并且配置了Java(或者OpenJDK)并且版本要高于Java8,这是安装Maven3.9+的要求,并且最好配置了与安装位置相关的环境变量JAVA_HOME ,笔者这里使用的是OpenJDK-17,能够成功使用Maven3.9.9

  • 安装Maven,也就是将编译后的压缩包解压后,假设此时Maven位置为MVN_HOME 那么需要将MVN_HOME/bin 写入到环境变量之中或者写入到MacOS或者Linux系统的/usr/local/bin 或者/usr/bin 这样的命令目录之中

1.1 Maven安装与环境配置

如果要获取其他版本的Maven可以通过Apache的https://dlcdn.apache.org/maven/页面进行下载,例如版本3.9.9对应子目录maven-3/3.9.9/binaries 从中下载zip 格式或者.tar.gz 格式的压缩包后解压即可,如果用户对于自己的网络通信有所怀疑,那么可以通过相同目录下的校验和炎症下载软件的完整性以避免未经许可的更改。当然在Linux发行版例如Ubuntu或者CentOS上可以使用包管理器apt 或者yum 直接进行安装,在MacOS上也可以通过homebrew 包管理器进行安装,但是这样我们就无法非常便捷和随心所欲的控制安装位置和具体Maven的版本。如果使用包管理器:

##Ubuntu下使用APT进行安装
sudo apt update
sudo apt install maven

##CentOS下使用YUM进行安装
sudo yum update
sudo yum install maven

##MacOS下使用HomeBrew进行安装
brew update
brew install maven

我们这里的安装主要考虑通过下载压缩包解压的方式进行,我们在Windows-PowerShell之中通过这样的命令观察Java环境是否恰当:

#查看Java当前的版本确定JDK/OpenJDK版本是否正确
java -version
#查看JAVA_HOME环境变量
$env:JAVA_HOME

下载并且解压Maven-3.9.9的压缩包之后看到目录结构:

#Maven-3.9.9
#│  LICENSE
#│  NOTICE
#│  README.txt
#│
#├─bin
#│      m2.conf
#│      mvn
#│      mvn.cmd
#│      mvnDebug
#│      mvnDebug.cmd
#│      mvnyjp
#│
#├─boot
#│      plexus-classworlds-2.8.0.jar
#│      plexus-classworlds.license
#│
#├─conf
#│  │  settings.xml
#│  │  toolchains.xml
#│  └─logging
#│
#└─lib
#    │  *.jar
#    │  *.license
#    ├─ext
#    └─jansi-native
#        │  README.txt
#        └─Windows
#            ├─arm64
#            ├─x86
#            └─x86_64

其中:

  • LICENSE ,README.txt ,NOTICE 是Maven软件的证书、介绍和注意事项。

  • bin 目录存放的是Maven运行需要的mvn 或者mvnDebug 等指令,在MacOS和Linux系统之中这两个文件是对应的shell脚本,而Windows系统之中对应的是.cmd 批处理命令行脚本——Maven依赖于Java进行工作,这些脚本实际上是.jar 可执行字节码加载到JVM之中的启动文件,所以Maven提供的下载压缩包并不区分系统和CPU运行架构——只要你的操作系统能够运行Java那么也一定能够运行Maven

  • lib 目录之中存放的是Maven运行所需的各种.jar 运行时字节码以及它们的.license 证书文件。这个目录除了上述文件之外还拥有两个子文件夹,一个是lib/ext 存放和JVM运行相关的内容,通常来说是一个空目录结构;对于jansi-native 而言只有Windows平台运行需要的链接库,分别对应64位平台,32位平台,ARM平台的jansi.dll 动态链接库。而对于Linux和MacOS这样的类Unix系统来说则不需要这样的链接库单独存放在Maven的安装内容之中,这通常是通过JNA(Java Native Access)由包管理器或者OS本身实现的。

  • boot 目录之中存放的是plexus 框架下的classworlds 组件的.jar.license需要说明的是:Plexus 是一个轻量级的容器和服务框架,而 Classworlds 是 Plexus 的一部分,它负责 Maven 中的类加载机制的实现。

  • conf 目录之中存放的则是Maven的核心配置文件settings.xml 以及用于配置Maven使用的JDK组件、编译器、其他可能出现的插件和组件的工具链等信息的toolchains.xml 文件

将解压后的Maven安装目录存放到想要的位置之后在系统之中配置该位置为环境变量MAVEN_HOME 后将bin 目录添加到PATH 之中:

#Windows下假设安装位置是C:/Program Files/Maven/3.9.9
#PowerShell只能设置User级别的环境变量,如果需要设置系统级别的环境变量最好通过GUI设置
setx MAVEN_HOME "C:\Program Files\Maven\3.9.9"
setx PATH "$env:PATH;$MAVEN_HOME\bin"

#MacOS下假设安装位置是~/Library/Maven/3.9.9
#首先打开~/.zshrc编辑默认终端环境文件
vim ~/.zshrc
#写入环境变量
export MAVEN_HOME="$HOME/Library/Maven/3.9.9"
export PATH="$MAVEN_HOME/bin:$PATH"
#重新加载终端环境
source ~/.zshrc

#Linux下假设安装位置是/usr/local/maven/3.9.9
#首先打开~/.bashrc或者对应Shell的环境文件
vim ~/.bashrc
#写入环境变量
export MAVEN_HOME="/usr/local/maven/3.9.9"
export PATH="$MAVEN_HOME/bin:$PATH"
#重新加载终端环境
source ~/.bashrc

#执行本指令能够成功输出JDK版本和Maven版本即为成功
mvn -v

1.2 工具链配置toolchain.xml

Maven允许构建过程之中指定不同的工具版本,例如某个工程可能需要应对不容的JRE环境因此需要使用不同的JDK版本进行编译或者说测试不同JDK版本的兼容性,那么Maven就有一个工具链(Toochain)的概念,并且在$MAVEN_HOME/conf~/.m2 下都有这样一个配置文件toolchain.xml 用于规范工具链配置。前者我们一般称作系统工具链配置而后者我们称为用户工具链配置,如果二者同时生效那么将会合并使用,如果配置项产生了冲突那么优先使用用户个人工具链配置覆盖掉系统全局工具链配置。首先来看结构:

<toolchains xmlns="http://maven.apache.org/TOOLCHAINS/1.1.0" 
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/TOOLCHAINS/1.1.0 http://maven.apache.org/xsd/toolchains-1.1.0.xsd">
  <toolchain>
    <type/>
    <provides>
      <version/>
      <vendor/>
      <!-- other key-value -->
    </provides>
    <configuration/>
  </toolchain>
</toolchains>

根节点是一个<toolchains/> 标签,指定了XML文件的Schema和编写规范,这是一个数组型容器节点,内涵多个<toolchain/> 标签,每个标签都代表一个设置好的工具链,这个标签是一个对象型容器节点,如此定义:

  • <type/> 指定了工具链的种类,一般设置为jdk 但是也可以设置为自定义工具链,只不过在Maven之中仅仅原生支持JDK工具链,所以如果要使用自定义工具链必须搭配对应的插件以指导工具链如何运行,关于插件和生命周期的概念是下一章节探讨的核心概念。如果用户的开发环境需要Maven在插件齐备的状况下具有灵活性。

  • <provides>:定义工具链提供的属性,这些属性用于在 Maven 构建过程中匹配工具链,也就是说这些定义的属性会通过Maven的JDK匹配器或者用户安装的插件匹配到工程设置中需要使用的工具链,这同样是一个对象型容器节点:

    • <version/> 是JDK工具链常用的配置项,指定了JDK的版本

    • <vendor/> 是JDK工具链常用的配置项,指定了JDK的厂商,例如oracle或者其他编译版本例如OpenJDK,我们在测试不同厂商的支持性或者针对不同厂商的JWM进行编译工作时就会在此有所区分

    • 这些节点的定义范式为<key>value</key> 也就是指定一个键名后填写字符串值

  • <configuration/> 是一个对象型容器,用于定义JDK或者自定义工具链的具体配置,上文提到的属性是用于Maven匹配工具链所用而此处的配置项是用于匹配工具链成功之后具体针对工具链本身的配置项,需要说明的是此处的配置项与后文详细介绍结构复杂的配置项并无关联,二者并无一致的结构仅仅是命名重叠。

假设我们的Maven使用JDK17运行但是我们的工程基本使用JDK1.8和JDK11:

<toolchains>
  <!-- JDK 1.8 -->
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.8</version>
      <vendor>oracle</vendor>
    </provides>
    <configuration>
      <jdkHome>/usr/local/jdk/1.8</jdkHome>
    </configuration>
  </toolchain>
  <!-- JDK 11 -->
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>11</version>
      <vendor>oracle</vendor>
    </provides>
    <configuration>
      <jdkHome>/usr/local/jdk/11</jdkHome>
    </configuration>
  </toolchain>
</toolchains>

那么在后文我们介绍的POM文档的构建配置部分我们可以这样确定JDK工具链:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-toolchains-plugin</artifactId>
      <version>3.0.0</version>
      <executions>
        <execution>
          <goals>
            <goal>toolchain</goal>
          </goals>
        </execution>
      </executions>
      <configuration>
        <toolchains>
          <jdk>
            <version>1.8</version>
            <vendor>oracle</vendor>
          </jdk>
        </toolchains>
      </configuration>
    </plugin>
  </plugins>
</build>

1.3 Maven跨平台脚本分析

我们在前文提到,Maven下载安装时并不区分系统版本和硬件架构,这在我们熟知的构建工具例如CMake上看起来不可思议,如果我们手上有一个ARM架构的开发板例如CPU使用RK3568并且安装了Ubuntu-22.04那么就是Arm-Arch-64-Linux版本的程序才能够正常使用,或者说我们需要将源代码拉取到本地进行编译之后才可以使用。这是因为Maven本身依赖于Java工作,也就是实际上Maven并不给出直接的可执行程序而是给出JVM能够运行的字节码,我们在这里对于其启动脚本稍做分析(只看Unix-Shell版本,在Windows系统上使用的是BAT批处理文件,但是大同小异)以理解其原理:

# concatenates all lines of a file
concat_lines() {
  if [ -f "$1" ]; then
    echo "`tr -s '\r\n' '  ' < "$1"`"
  fi
}

MAVEN_PROJECTBASEDIR="${MAVEN_BASEDIR:-`find_maven_basedir "$@"`}"
MAVEN_OPTS="`concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"` $MAVEN_OPTS"

# For Cygwin, switch project base directory path to Windows format before
# executing Maven otherwise this will cause Maven not to consider it.
if $cygwin ; then
  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
  MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi

export MAVEN_PROJECTBASEDIR

# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
export MAVEN_CMD_LINE_ARGS

exec "$JAVACMD" \
  $MAVEN_OPTS \
  $MAVEN_DEBUG_OPTS \
  -classpath "${CLASSWORLDS_JAR}" \
  "-Dclassworlds.conf=${MAVEN_HOME}/bin/m2.conf" \
  "-Dmaven.home=${MAVEN_HOME}" \
  "-Dlibrary.jansi.path=${MAVEN_HOME}/lib/jansi-native" \
  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
  ${CLASSWORLDS_LAUNCHER} ${MAVEN_ARGS} "$@"
  • 首先定义函数concat_lines() ,其作用是将字符串所有换行的\r\n 取消变成一行

  • 其次查找Maven项目的根目录$MAVEN_PROJECTBASEDIR 通常就是POM文档所在目录

  • 随后处理工程中.mvn/jvm.config 文件计入整个参数列表$MAVEN_OPTS

  • 如果处于Cygwin 环境下,也就是Windows环境中将路径格式替换为Windows样式

  • 将上文处理的环境变量全部导出到命令行环境变量之中

  • 开始执行Maven核心程序

    • 使用环境变量$JAVACMD 启动JVM虚拟机的java命令

    • 将上文整理的$MAVEN_OPTS 和调试参数$MAVEN_DEBUG_OPTS 输入JVM

    • 加载classworlds.jar 也就是Plexus服务架构到JVM寻址路径

    • 定义Plexus的全局配置文件${MAVEN_HOME}/bin/m2.conf 路径

    • 从环境变量之中加载${MAVEN_HOME} 到JVM命令行参数之中

    • 从环境变量之中加载真正和平台以及操作系统相关的jansi-native 链接库

    • 加载前文设置的工程目录到JVM虚拟机参数之中

    • 启动Plexus 服务架构,执行Maven的功能

2.核心概念:工程结构与生命周期

从前文开始我们就一直在提到构建工具这个概念,何谓构建工具,事实上就是指用于自动化和简化软件构建过程的工具软件,这些工具通常涉及代码编译,依赖管理,单元测试,软件打包,项目部署等等任务,能够大大提升开发效率保证项目构建过程的一致性和可复现特征。我们经常在C/C++开发之中使用到的MakefileCMake 都是构建工具的一种,而在Java开发之中Maven就是占据了这样一个生态位。目前在Java构建工具之中有三大主流工具:

  • Ant是Apache组织较早时期推出的一个基于Java的构建工具,其开发和操作思想类似于Makefile的方式,通过定义任务(Tasks)来执行所有操作;Ant的特征是通过XML配置文件build.xml 配置构建过程,每个任务都要明确指定。其优势在于灵活性极高并且易于扩展。然而由于Ant推出时Java构建工具的概念尚不成熟,所以Ant的缺点也很明显:缺少依赖管理,不得不进行手动导入或者引入插件,由于Ant是一个命令式的工具,故而所有配置都要进行繁琐的编写任务导致冗长且难以维护。最重要的一点是Ant并不定义工程结构和生命周期,也就是在规约方面自由度极大,这导致了Ant工程的构建方式和流程差异度非常大,缺乏标准化。

  • Gradle 则是三者之中最新的由Google推出的现代化工具,其号称结合了Maven以及Ant的优点,使用Groovy或者Kotlin DSL来定义构建脚本。它的优点在于性能上远超其他两个,能够增量构建并行执行,在大型或者超大型的项目之中大大加快了构建过程因此被Android官方确定为默认的构建工具。相比于Maven而言它的灵活性和可扩展性进一步提高。然而广为人诟病的是:Gradle的学习曲线十分陡峭,社区和文档的支持一团乱麻,配置文件复杂不说,基本上Gradle每每提升一个版本其使用的各种API近乎重写,并且Groovy语言在一些开发者的语境下完全是为了一碟醋包了一盘饺子(这种批评在Kotlin开发者之中并不强烈,得益于Kotlin DSL)。

  • Maven 是Apache组织继Ant之后推出的构建工具,目前可以说是Java构建工具之中的中流砥柱,拥有无可置疑的市场占有率——一般而言老项目或者公司要求开发者才会使用Ant,框架需求或者公司强制开发者才会使用Gradle,但是基本上所有的Java开发者都要对于Maven有所了解。对比Ant它提供了良好的依赖管理,并且基于本小节介绍的核心:工程结构和生命周期,Maven将繁复杂乱的工程构建工作简化管理拥有良好的标准化和一致性。而因为高市占率,社区和文档十分丰富,这就让插件生态和IDE集成畅通无阻。非要说Maven有什么缺点,那只能概括为灵活性略差于Ant,配置文件的编写并不那样用户友好,性能对比Gradle在大型项目之中略差

我们可以看到Maven的优点和缺点基本上绕不开两个核心概念:工程项目的目录结构与构建流程的生命周期或者说阶段划分。事实上Maven是著名的“约定大于配置”思想的显著代表,也就是说Maven项目必须遵循一定的约定俗成的规约,在这些规约生效的范围内无论我们使用什么客制化的配置都是无效的。

2.1 Maven工程约定俗成的项目结构

Maven的构建对象可能多种多样,例如最常见的.jar 类型的项目封装或者库封装就是默认工程构建对象,其他的还包括面向Java Web的.war 封装(Web Application Archive)或者应用于企业级应用包含多个模块(Web或者EJB等)的.ear 封装(Enterprise Archive)又或者后文将要介绍到的与Maven工程继承-聚合机制相关的父工程POM类型,也可能是服务于Maven插件体系的Maven-Plugin类型的构建目标。这里我们从最基础的也是默认的.jar 目标的工程结构开始分析:

# Project
# ├── .git/.svn
# ├── pom.xml
# ├── target/
# |   └── target.jar
# ├── src/
# |   ├── site/
# │   ├── main/
# │   │   ├── java/
# |   |   ├── filters/
# │   │   └── resources/
# │   └── test/
# │       ├── java/
# |       ├── filters/
# │       └── resources/
# ├── README(.txt/.md)
# ├── NOTICE(.txt/.md)
# └── LICENSE(.txt/.md)
  • Maven工程的显著特征也是标志性识别文件就是最核心的pom.xml

  • Maven工程并不承担代码版本管理或者IDE的工作,因此相关的文件并不会被Maven所影响,例如说Git的.git 目录和.gitignore 文件,又或者IDEA的.idea 目录

  • README是工程自述文件,LICENSE是工程所用许可文件,NOTICE是工程注意事项,这三个文件可能是.txt或者.md或者无扩展名,这具体取决于用户的配置

  • target目录是Maven所有工程构建的结果输出位置

  • src目录是Maven工程的源码存放位置,test目录是Maven工程的测试代码存放位置,二者具有高度相似的内容结构:

    • java是其中的Java源代码的书写位置,通常来说一个.java源文件对应一个.class字节码编译结果,并且此目录下的内容结构符合Java语言的Package结构,也就是说一个类com.example.project.ClassA存放于com/example/project之中的ClassA.java

    • resources是其中的资源文件夹,各种配置文件,文档或者多媒体文件应当存储其中,并且需要指出的是test目录下的资源文件夹不应当和src下的资源文件夹有所关联,前者只是测试时需要用到的资源而并不会被打包到最终的输出文件之中,因此测试目录之中的资源目录并不会被大多数IDE自动生成,往往需要手动添加。

    • filters目录之中定义了资源文件的过滤器,这个配置并不常见,虽然其处于Maven标准工程目录结构之中但是通常在各大IDE之中都需要用户手动创建而不会自动生成这个目录。

    • src/site存储的是和构架流程之中的Site也就是生成部署文档阶段相关的内容,并不属于构建源码的一部分,大多数工程之中可能并不涉及这个目录而是通过其他方式独立的管理项目文档。

除了.jar 项目之外Maven还包括以下几种主要的构建目标:

  • .war 类型的Web Application项目,项目构建结果是含有源码以及WEB资源的综合封装。特点是具有src/main/webapp 目录,其中存储各类Web服务需要的资源,例如说WEB-INF/web.xml 和其他的JSP资源.jsp 文件

  • .ear 类型的企业级应用Enterprise Application项目,打包结构包含企业应用的封装库和资源文件,这种封装形式并不如.jar或者.war常见,特点是具有src/main/application/*-ear 目录

  • POM 类工程为后文将要介绍到的Maven继承-聚合关系之中的父工程,这里不赘述

  • maven-plugin 为Maven插件项目虽然生成的也是.jar 并且具有相同的结构,但是工程的具体构建方式和结构形式略微具有不同,整体按照默认结构理解即可。

Maven这种构建工具和其他的构建工具具有显著区别的点就在于其定义了一套约定俗成的工程结构,个人认为这是Apache组织在看到Ant项目根本无法管理混乱的开发者和百花齐放的生态环境时产生了某种PTSD,我们在使用Maven的时候一定要谨记一点:Maven是一个约定大于配置的工具,也就是说如果我们想要实现客制化功能那么一定要在Maven的各种规约的约束下执行而非如同Ant和CMake一样随心所欲。

2.2 Maven工程默认的构建流程

Maven作为一个构建工具其立足点就在于“构建流程”或者说“构建生命周期”这一核心概念,也就是Maven按照实际业务逻辑和需求提出了一套构建和分发特定工程或者构件的一套清晰流程,这就是本小节的重点,也是后续介绍到的大部分Maven指令的核心概念以及出处。对于整体构建流程而言大致可以进行这三个大阶段的分类:

  • default Maven默认的从得到工程源代码到部署目标的流程

  • clean Maven用于为下一次构建营造可复现环境的过程,也就是清理现场

  • site Maven用于为已经构建完成的软件进行文档提交工作的步骤

其中如果文档并不复杂我们有可能认为site 是默认构建流程的最后一环,也就是如下这张流程图,由于清理动作通常与构建流程分离,其中并未提及clean 步骤。红色的流程步骤意味着在大多数工程之中可能省略,绿色步骤通常是构建完成后提交到远程服务器和仓库之中的动作,黄色步骤为Maven的核心步骤:

maven-lifecycle.png

  • fetch获取源代码的过程,在实际工程之中我们通常使用版本管理工具例如Git

  • validate 步骤用于验证当前工程结构、项目属性、构建元信息是否完整和正确

  • compile 是Maven的核心步骤之一,也是构建流程最基础的点,将源代码通过编译器构建为可执行程序或者字节码等待测试

  • test 是用于检测编译结果是否符合预期的步骤,我们在src/test/java 之中书写的代码在这个步骤之中用于测试编译后的源代码,同样测试代码也要编译后方可执行,当然这就是子步骤了

  • report 是将检测结果写入文档或者在命令行之中显示的步骤,这一步骤通常是为了给开发人员提供用户友好界面使其能够快速了解本次构建的具体情况,例如代码覆盖率等,这个步骤的内容有时候也在最后的site 之中出现

  • package 是将构建结果会同资源文件、元信息等数据一同封装为一个符合统一格式定义的文件的过程,例如生成.jar.war 文件

  • verify 是运行并检测流程之中的其他步骤的结果以验证是否达成某种目标的环节,这个环节在工程构建之中并不常见,通常融入到前后几个步骤之中完成

  • install 这个步骤通常是将构建目标写入到本地仓库以备使用或者作为其他工程或者构件的依赖进入依赖管理体系运行的环节

  • depoly 这个步骤一般是推送到远程仓库、进行容器化部署的环节,意在使得构建结果脱离本地开发环境投入后续生产或者存储环节

  • site 这个步骤是为项目或者本次构建生成解释文档或者参考文档而存在的,本步骤大多时候独立于整个构建流程而存在,在Maven之中也是较为独立的功能

以上步骤定义在Maven的核心流程之中,可以通过命令行进行尝试:

# 执行单一verify和test步骤
mvn verify
mvn test
# 依次执行多个符合顺序的步骤,命令行之中可以仅仅写开始和结束步骤
mvn compile test
mvn clean deploy

我们可以看到Maven定义了一套完整的规约以保证整个构建流程处于一定的标准化之中但是又不损坏多种多样的构建生态,那么这个约束-灵活的机制具体是什么实现的呢?我们知道在设计模式之中有模板方法模式,观察者模式,策略模式能够大致完成这样的功能,总的来讲就是定义一系列的访问端点和钩子去实现。Maven的处理思路也是万变不离其宗的,它通过“插件”去代表一个构建过程之中的流程。对于每个插件plugin 来说它可以拥有多个目标goal ,如同Ant一样每个目标都对应了一个特定的任务,而这个任务就可以是一个特定的构架阶段,我们可以说一个插件的一个目标可以对应0个或者多个构架阶段(如果不绑定任何的构建阶段那么这个插件对应的任务就是在生命周期外执行的而不是生命周期的一部分,这也提供了生命周期之外和其他构建工具或者策略进行灵活耦合的可行性),反过来说一个构建阶段也不一定会对应一个插件的特定目标。

我们考虑有这样一个插件dependency 其中有一个目标是copy 那么如果我们想要完成从构建动作clean 开始,执行这个目标最后直到打包动作package停止,也就是说首先执行设定好的目标clean 之后执行这个插件的目标,随后顺序执行生命周期直到package 动作。那么这样的自定义流程可以通过命令行这样实现:

mvn clean dependency:copy package

如果一个插件的某一个目标对应多个构建阶段,那么当我们调用这个插件的特定目标的时候就会顺序的执行这多个构架阶段,如果一个构建阶段对应多个插件的目标那么这些目标就会顺序执行,当然了如果构架阶段没有指定任何插件目标,那么即使我们执行了这个构建阶段也不会产生任何实质上的动作。值得一提的是:并非每个构建阶段都会在命令行或者什么等效的操作之中被相当频繁的执行,Maven对于一个构建动作实际上进行了相当详细的构建阶段的划分,例如说常见的前缀pre-post-process- 就细分了一个动作为许许多多个构建阶段,这些阶段并不会显示的被调用,也就是说他们是不常用的,但这并不代表它们的存在没有意义。

Maven这样定义大的构建动作和每个动作对应的可能出现的阶段phase ,这就是生命周期的规约

  • clean 生命周期,主要用于清理构建环境以准备下一次构建提供可复现环境

    • pre-clean 在清理工程结构之前需要执行的动作

    • clean 清理上次构建或者构建流程计划外产生的各种中间文件

    • post-clean 在清理工程结构之后需要执行的动作

  • default 生命周期,核心生命周期,完成编译、测试等必要性的软件构建动作

    • validate 验证工程源码完整性和构建所需的元信息完整提供

    • initialize 初始化构建状态,例如设定各种中间属性和创建所需目录

    • generate-sources 生成供编译使用的源代码,例如从Git远程仓库拉取到本地

    • process-sources 处理上一步骤的源码,例如对于其中的值进行本地化或过滤

    • generate-resources 生成打包阶段需要的资源文件,例如处理各种图标文件

    • process-resources 处理上一阶段的资源文件,例如格式化或者复制到某处

    • compile 核心步骤,编译源码为可执行或者投入JVM的字节码文件

    • process-classes 编译后处理字节码,例如进行针对JVM的优化和其他处理

    • generate-test-sources 对于测试用源代码进行编译所需生成工作

    • process-test-sources 对于测试用源代码进行编译所需处理工作

    • test-compile 编译测试用源码,为单元测试进行准备

    • process-test-classes 对编译后的测试用字节码进行优化和其他处理

    • test 使用单元测试框架运行测试以检验源代码是否符合规范或者完成了功能

    • prepare-package 在打包前进行的准备工作,例如确定版本号

    • package 将通过测试的源代码和资源文件进行封装

    • pre-integration-test 集成测试前的准备工作,例如设置测试服务器环境

    • intergration-test 集成测试,在安装/部署动作前验证封装是否正常工作

    • post-integration-test 集成测试后的收尾工作,例如清除临时测试环境现场

    • verify 检测通过了集成测试的封装文件是否符合安装/部署的要求和签名

    • install 将完成了上述步骤的构件传输到本地仓库之中以备使用

    • deploy 将构件部署到服务器环境之中运行或者推送到远程仓库之中

  • site 生命周期,独立于主要生命周期外,为构件或者工程生成文档

    • pre-site 在执行文档生成动作前的准备工作,例如环境变量设置

    • site 为构件或者项目生成文档

    • post-site 生成文档后的收尾动作,例如清理环境和准备部署文档

    • site-deploy 部署文档到指定服务器或者展示平台

如果我们翻看Maven的源码我们不难发现在META-INF/plexus/components.xml 之中定义了上文提到的三个默认的生命周期和默认绑定的插件目标的关系:

<!-- clean lifecycle -->
<phases>
  <phase>pre-clean</phase>
  <phase>clean</phase>
  <phase>post-clean</phase>
</phases>
<default-phases>
  <clean>
    org.apache.maven.plugins:maven-clean-plugin:3.2.0:clean
  </clean>
</default-phases>

<!-- default lifecycle -->
<phases>
  <phase>validate</phase>
  <phase>initialize</phase>
  <phase>generate-sources</phase>
  <phase>process-sources</phase>
  <phase>generate-resources</phase>
  <phase>process-resources</phase>
  <phase>compile</phase>
  <phase>process-classes</phase>
  <phase>generate-test-sources</phase>
  <phase>process-test-sources</phase>
  <phase>generate-test-resources</phase>
  <phase>process-test-resources</phase>
  <phase>test-compile</phase>
  <phase>process-test-classes</phase>
  <phase>test</phase>
  <phase>prepare-package</phase>
  <phase>package</phase>
  <phase>pre-integration-test</phase>
  <phase>integration-test</phase>
  <phase>post-integration-test</phase>
  <phase>verify</phase>
  <phase>install</phase>
  <phase>deploy</phase>
</phases>

<!-- site lifecycle -->
<phases>
  <phase>pre-site</phase>
  <phase>site</phase>
  <phase>post-site</phase>
  <phase>site-deploy</phase>
</phases>
<default-phases>
  <site>
    org.apache.maven.plugins:maven-site-plugin:3.12.1:site
  </site>
  <site-deploy>
    org.apache.maven.plugins:maven-site-plugin:3.12.1:deploy
  </site-deploy>
</default-phases>

2.3 在配置文件之中绑定生命周期

首先,对于Maven本体或者说核心组件并不支持的一些扩展封装形式,我们有可能需要扩展默认的生命周期以获得符合对应封装形式的构件流程,这时候就需要在后文重点介绍的pom.xml 文件的build 节点之中打开扩展功能,例如对于Plexus的插件我们可能需要提供阶段plexus-applicationplexus-service

<build>
  <!-- other cofiguration -->
  <extension>true</extension>
</build>

除了使用Maven默认提供的组件和规约确定的生命周期之外,我们可以通过配置文件修改默认的构件阶段和插件目标的绑定关系,也可以通过引入符合我们设想的构建流程的插件和相关插件目标完成这一工作。当我们引入某一个插件的时候,我们首先要明白插件的本质就是将某些可执行代码绑定到Maven并且提供goals 访问接口的一种特殊的Maven构件。一个插件一定包含一个或者多个目标,在插件的元信息之中通常会指明某个目标应当绑定到哪个生命周期的哪个阶段之中,如果仅仅想要执行插件默认绑定的构建阶段只要引入插件就行了,在插件本身构建时已经通过插件的pom.xml 或者plugin.xml绑定完成。不过对于我们想自定义绑定阶段的情况,只是将插件添加到工程之中是不够的,开发者必须明确插件的各个目标将以怎样的方式在构建流程之中执行。

如果多个插件目标被绑定到同一个构建流程之中的阶段,那么Maven会首先执行内置的绑定目标,随后按照POM配置文件顺序执行用户附加绑定的插件目标通过特定的配置文件的考量,我们可以指定某个插件目标的运行配置并且绑定到我们想要的构建阶段之中去,假设说我们有一个自定义开发插件display 其中有一个目标为time 是展示现在的时间,我们希望在处理源代码的时候能够按照固定格式显示,那么:

<build>
  <!-- other configuration -->
  <pluginManagement>
    <!-- other plugin settings -->
    <plugins>
      <!-- other plugin -->
      <plugin>
        <groupId>com.my-company</groupId>
        <artifactId>display-maven-plugin</artifactId>
        <executions>
          <execution>
            <configuration>
              <format>YYYY:MM:DD-hh:mm:ss</format>
              <time-zone>+08:00</time-zone>
            </configuration>
            <phase>generate-sources</phase>
            <goals>
              <goal>time</goal>
            <goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

对于这样的绑定关系,在Maven的源码之中不难发现META-INF/plexus/default-bindings 之中规定了默认的绑定关系,这也是各个阶段在运行我们的execution 之前默认执行的各个插件目标,首先是最常用的.jar 构建:

<phases>
  <process-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:resources
  </process-resources>
  <compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile
  </compile>
  <process-test-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:testResources
  </process-test-resources>
  <test-compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.13.0:testCompile
  </test-compile>
  <test>
    org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test
  </test>
  <package>
    org.apache.maven.plugins:maven-jar-plugin:3.4.1:jar
  </package>
  <install>
    org.apache.maven.plugins:maven-install-plugin:3.1.2:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:3.1.2:deploy
  </deploy>
</phases>

可以看到默认绑定并不会完全覆盖Maven规约的所有阶段,如果我们在以上的规约下执行构建阶段generate-sources 那么就会无事发生,对比前者.war 如此绑定:

<phases>
  <process-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:resources
  </process-resources>
  <compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile
  </compile>
  <process-test-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:testResources
  </process-test-resources>
  <test-compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.13.0:testCompile
  </test-compile>
  <test>
    org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test
  </test>
  <package>
    org.apache.maven.plugins:maven-war-plugin:3.4.0:war
  </package>
  <install>
    org.apache.maven.plugins:maven-install-plugin:3.1.2:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:3.1.2:deploy
  </deploy>
</phases>

可以看到由于.war 只是增多了WebApp的资源文件,因此其只有打包阶段不同于默认的构建绑定策略,而相当不同的就是.ear 企业应用包的绑定关系:

<phases>
  <generate-resources>
    org.apache.maven.plugins:maven-ear-plugin:3.3.0:generate-application-xml
  </generate-resources>
  <process-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:resources
  </process-resources>
  <package>
    org.apache.maven.plugins:maven-ear-plugin:3.3.0:ear
  </package>
  <install>
    org.apache.maven.plugins:maven-install-plugin:3.1.2:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:3.1.2:deploy
  </deploy>
</phases>

可以看到企业应用包增加了格式化生成资源的步骤,但是由于企业应用包的繁多变种删除了编译和测试的阶段而直接跨越到了打包-安装-部署。我们跳过.ejb.rar 这两个在实际开发之中并不常用的打包模式,最后来展示Maven插件的构建绑定:

<phases>
  <process-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:resources
  </process-resources>
  <compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile
  </compile>
  <process-classes>
    org.apache.maven.plugins:maven-plugin-plugin:3.13.1:descriptor
  </process-classes>
  <process-test-resources>
    org.apache.maven.plugins:maven-resources-plugin:3.3.1:testResources
  </process-test-resources>
  <test-compile>
    org.apache.maven.plugins:maven-compiler-plugin:3.13.0:testCompile
  </test-compile>
  <test>
    org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test
  </test>
  <package>
    org.apache.maven.plugins:maven-jar-plugin:3.4.1:jar,
    org.apache.maven.plugins:maven-plugin-plugin:3.13.1:addPluginArtifactMetadata
  </package>
  <install>
    org.apache.maven.plugins:maven-install-plugin:3.1.2:install
  </install>
  <deploy>
    org.apache.maven.plugins:maven-deploy-plugin:3.1.2:deploy
  </deploy>
</phases>

可以看到其显著特征就是出现了编译后增加描述文件和在打包阶段有两个不同的插件目标这就是前文介绍到的一个构建阶段可以对应多个不同的插件目标的实际应用案例。

2.4 Maven核心概念总结

在本章节的最后我们用这样一张结构图去概括整体的构建流程和插件绑定的关系:

maven-project_structure.png

从上图之中可以观察到这样一些Maven的核心概念:

  • 绿色节点代表的是Maven Core的约定俗成部分,蓝色的代表的是通过settings.xmlpom.xml 可配置部分,约定大于配置

  • 如果不考虑构建工具实际上用户编写工程使用到的全部局限于紫色部分,也就是源代码和相关资源,Maven提供了此外的所有功能

  • 黄色节点代表的是工程构建之中通过约定和配置获取到的结构和资源,例如依赖或者插件,使用Maven的用户可以不关心这些

  • 红色节点代表的是工程构建的三大目标,也就是构建本身,安装和部署的集成,以及工程文档的生成

  • 接下来最关键的部分是蓝色节点代表的配置,也就是全局或者用户的settings.xml 和服务于单一工程的pom.xml

3.配置文件settings.xml

配置文件settings.xml 之中的元素用于定义Maven运行时的各种核心参数,需要明确的是这个文件是作用于整个系统之中的Maven命令和Maven工程的,因此这个文件的内容不应当和任何一个特定工程的上下文形成绑定关系。并且考虑到系统之中可能存在多个用户而Maven被安装到系统级别命令的情况:

  • $MAVEN_HOME/conf/settings.xml 用于配置整个系统之中的Maven行为

  • $HOME/.m2/settings.xml 用于配置当前调用mvn 命令的用户的Maven行为

前者被称为系统设置或者说全局设置,后者被称为用户设置,当这两个文件同时存在时具体的Maven行为实际上是二者的合并,如果它们对于某个元素值有不同的重复定义那么后者也就是用户设置占据主导性地位。并且如果用户要创建后者的配置文件,最快捷和安全的方式实际上是直接复制全局配置文件到对应位置并且更改。整个XML文件的结构和顶层元素的定义如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!-- settings为根节点, 定义命名空间的属性以及本XML文件的格式 -->
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">

  <localRepositry/>

  <interactiveMode/>

  <offline/>

  <pluginGroups/>

  <servers/>

  <mirrors/>

  <proxies/>

  <profiles/>

  <activeProfiles/>

</settings>

3.1 单值配置项

节点<localRepositry/> 代表依赖库文件在本地存放的位置也就是所谓的本地仓库保存目录的路径。这个元素必须是一个字符串代表一个路径,当然这个路径不必在编辑设置时已经存在,它会在Maven首次使用仓库时创建。它的默认值是${user.home}/.m2/repositry 也就是存放在用户目录下的Maven隐藏目录之中,这个值可以改变为任何当前执行Maven用户有权限读写的目录,例如:

<localRepositry>{$user.home}/Documents/Codes/MavenRepositry</localRepositry>

节点<interactiveMode/> 代表Maven的交互模式是否开启,这是一个bool值,只能写入true 或者false 代表交互模式是否开启,这个元素的默认值是true 也就是开启交互模式。当我们执行一些操作例如mvn release:prepare 时可能需要我们手动确认发布操作或者输入发布版本的相关Meta信息,这种需要用户在命令行之中手动输入的场景就是”Interactive Mode",如果我们想要在构建过程之中全自动运行,使用配置文件或者默认缺省值那么就可以将其设置为关闭状态:

<interactiveMode>false</interactiveMode>

节点<offline/> 代表Maven是否运行在离线模式下,这同样是一个bool值,代表是否关闭Maven的远程功能。当我们构建的项目的依赖管理之中需要一些在本地仓库之中不存在的依赖库时,Maven就需要连接远程仓库下载依赖,通过设置这个标志位我们就可以禁止Maven连接网络下载远程依赖,这个值的默认选择为false ,当然也可以手动关闭联网功能,也就是:

<offline>true</offline>

3.2 插件配置功能

节点<pluginGroups/> 是一个数组类容器节点,其中可以包括多个pluginGroup 节点,后者是一个单值节点其取值应当是一个字符串,代表使用插件的组名称。在后文之中我们会详细讨论GAVP坐标机制和插件机制,Maven是一个扩展性极强的构建工具,用户或者其他开发者可以针对于Maven开发插件从而实现更加强大和灵活的功能。在Maven之中这两组插件是默认记录在搜索路径之中的:

  • org.apache.maven.plugins

  • org.codehaus.mojo

那么如果我们要用的插件并不是这两个组织开发或者不处于这两个组织的官方插件列表之中该怎么办呢?也就是说我们可能碰到两种状况:使用特定的第三方库插件并且这些插件没有存放在Maven中央仓库(默认远程仓库)之中而是存在于自定义仓库之中;或者说我们自己维护了一个插件库。那么假设我们的组织域名为:plugins.example.com 我们要使用这个插件库之中的example-maven-pluginrun 动作:

mvn com.example.plugins:example-maven-plugin:run

可以看到命令冗长,并且包含了groupId 如果我们将这个字段写入节点:

<pluginGroups>
  <pluginGroup>com.example.plugins</pluginGroup>
</pluginGroups>

那么当我们省略groupId 字段时Maven就会自动在这些已经登记到pluginGroups 的插件组之中进行搜索,那么命令就可以简化为这样的省略了groupId的形式:

mvn example-maven-plugin:run

3.3 联网服务器配置

Maven是一个全流程或者说全生命周期的构建工具,在这个生命周期之中有两个阶段或者说功能是有极大可能性无法在本地完全完成的:依赖管理部分可能出现本地仓库之中缺少所需依赖库的情形,就需要从远程仓库下载依赖库;部署阶段可能出现本地是开发环境或者说本地环境是Jenkins这种CI/CD服务器而实际运行项目的是存在于互联网上的另一台服务器或者另一个远程Maven仓库。这就需要Maven连接到远程服务器而这些服务器的连接信息就定义在节点<servers/> 之中,这个节点也是一个数组型的容器节点,可以包括多个<server/> 节点代表对于不同服务器的连接配置信息

节点server 是一个对象型的容器节点,可以包含如下几个字段节点:

  • id 字段,这个字段是必须的,这代表了这个服务器连接配置的名字。需要注意的是这个名字并不必须是服务器的地址,也就是说这只是一个标记连接信息的字符串,在后文介绍的pom.xml 之中的repositories 字段和distributionManagement 字段之中指定的连接名称应当与此字段一一对应,并且具体的服务器地址在该字段之中配置。

  • username 用于通过远程服务器认证登录的用户名字段,如果该服务器是开放的没有认证要求或者可以匿名使用的那么此字段省略

  • password 对于认证手段使用用户名-密码形式的服务器,应当在此字段之中写入与用户名配对的密码。远程仓库或者部署服务器有很多种认证方式,有的可能是通过SSH-Key或者GPG密钥认证,如果使用这些方法认证此字段应当省略,否则由于此字段的优先度最高,那么Maven会优先使用密码认证的方式登录而忽略掉后续配置的认证方式。

  • privateKey 如果远程服务器使用SSH-Key认证,那么连接服务器时应当提供私钥与服务器之中存放的公钥进行匹配从而进行非对称加解密,这个字段应当填写为私钥文件在文件系统之中的位置例如${user.home}/.ssh/id_rsa

  • passphrase 当使用私钥连接时,创建私钥时我们可以输入一个密码口令从而必须通过此口令才能正确得到私钥内容,如果配置了密钥口令就必须将口令填写到这个字段之中;反之如果私钥文件并未配置口令那么此字段应当直接忽略而非留空。

  • 字段filePermissions 和字段directoryPermissions 代表了连接远程服务器时使用到的权限控制代码。如果我们连接的远程服务器时需要创建文件夹或者创建新文件时,大多数时候在部署阶段,我们需要指定这个文件的访问权限。这个字段填写的是一个三位权限代码,其格式与Unix系统的权限定义相同:

    • 4代表能否读取,2代表能否写入,1代表能否执行。只读代码就是4,读写代码就是6,可执行代码就是7

    • 第三位数代表Anyone权限,第二位数代表同Group用户权限,第一位数代表Owner权限

    • 例如664代表本用户与同组用户都可读写,所有人都能够读取但是无法写入或者删除,任何人都禁止执行

  • 字段configuration 是一个容器类节点,其中存储的内容是连接特定服务器时除了以上字段需要的额外客制化字段,因此本字段并无具体的要求严格的格式定义。

假设我们通过HTTP协议连接服务器,服务器具有客制化字段,必须要在HTTP报文的HEADER之中提供一个额外的字段,并且出于安全考虑这种连接必须通过一个代理服务器作为跳板进行访问,而且我们要设置读取和连接超时时间为30s那么:

<configuration>
  <connectionTimeout>30000</connectionTimeout>
  <readTimeout>30000</readTimeout>
  <httpHeaders>
    <header>
      <name>X-Custom-Header</name>
      <value>CustomValue</value>
    </header>
  </httpHeaders>
  <proxy>
    <host>proxy.example.com</host>
    <port>7890</port>
    <username>Username</username>
    <password>Password</password>
    <protocol>http</protocol>
  </proxy>
</configuration>

在Maven-2.1.0+的版本之中,Maven考虑到在连接服务器时可能遭遇嗅探攻击或者伪装网络攻击,因此对于明文传输的的密码或者私钥的加密口令(如果不加密还被嗅探攻击那真的活该)是需要进行加密传输的,也就是Password Encryption 功能,这部分功能将在后文之中介绍仓库存储、管理以及同步机制时详细介绍。因此对于一个可能的服务连接配置可能是:

<servers>
  <server>
    <id>CompanyServer</id>
    <username>Employee006</username>
    <!-- Cause using ssh key, password field omit -->
    <privateKey>{$user.home}/.ssh/id_rsa</privateKey>
    <passphrase>MyPassword0123456</passphrase>
    <filePermissions>664</filePermissions>
    <directoryPermissions>775</directoryPermissions>
    <!-- Empty custom configuration, omit -->
  </server>
</servers>

3.4 镜像仓库配置

Maven作为一个构建工具包含了依赖管理的职责,也就是说我们在构建时总是需要从远程仓库下载本地仓库缺少的依赖库,这个默认的仓库服务器被我们称作“Maven中央仓库”,但是很可惜的是这个仓库并不在中国甚至不在亚洲,因此常常出现下载速度过慢或者需要配置网络魔法的情形——针对这样的情况我们就需要配置仓库镜像——也就是内容与对象仓库完全一致但是网络地址不同。节点mirrors 就是用于设置这样的功能,这个节点是一个数组型容器节点包含多个mirror 节点,后者是一个对象容器节点,其中包含四个字段:

  • id 字段代表镜像仓库的专属名称,是一个字符串,这个字段同样也是并不要求是服务器的地址,这个字段应当与server.id 字段一致从而建立镜像服务器和该服务器连接配置之间的关系。

  • name 字段代表镜像仓库的显示名称,是一个字符串,这个字符串并不会与其他配置文件之中的条目或者字段相互联系,它完全不影响Maven的工作,只是一个对于人类程序员而言用户友好操作界面的一部分

  • url 字段代表镜像仓库的具体访问网络地址

  • blocked 字段代表该尽享仓库是否处于禁用状态,bool值,默认为false

  • mirrorOf 字段代表这个镜像仓库对什么原有仓库进行了镜像,例如说如果我们连接一个对于默认中央仓库的镜像仓库,此字段就是central ,然而Maven在此处提供了更加灵活和复杂的配置,有时候一个镜像仓库可能并不会镜像所有的依赖或者说我们仅仅想让这个镜像仓库作用于一部分特定的依赖,那么此处可以配置这样的功能,这部分复杂功能将会在后文之中的依赖管理部分详述。

如果我们配置国内访问速度较快的阿里云Maven中央仓库镜像:

<mirrors>
  <mirror>
    <id>aliyun-maven</id>
    <name>Aliyun Central Mirror</name>
    <url>https://maven.aliyun.com/repositry/public</url>
    <mirrorOf>central</mirrorOf>
    <!-- 'blocked' can omit -->
    <blocked>false</blocked>
  </mirror>
</mirrors>

3.5 网络代理配置

当我们连接的服务器不在中国大陆或者说被GFW禁止访问,或者说这个服务器是必须要通过某个代理网络才能访问的情况而我们又不想或者说不能设置系统级别的全局代理的情况下我们就需要对Maven设置代理,也就是proxies 节点,这也是一个数组型的容器节点,包括多个对象型节点proxy 具有以下字段:

  • id 代理的唯一标识符,虽然并不和Maven的其他部分形成联动,此字段必须设置并且禁止与其他的代理条目之中的id 发生重复

  • active 是一个布尔值,代表代理是否开启,常见的用法是对于多个同生效范围的代理或者多个同协议的代理同时写入设置但是仅仅开启一个,默认值为开启。

  • protocol 字符串值,代表了连接代理的协议${protocol}://${host}:${port} 就是结合以下两个字段形成的代理服务器访问地址,这个位置常见的值是http ,https ,socks5 这样的协议,但是对于一些代理服务器的部署工作,也可以设置ftp这样的协议。

  • host 字符串值,代表了代理服务器的网络地址

  • port 数字值,处于0~65535之中,代表了代理服务器提供代理服务的端口号

  • username 字符串值,接入代理服务使用的用户名,如果服务器没有认证或者代理可以匿名访问,此字段省略

  • password 字符串值,接入代理服务使用的密码,如果服务器没有认证或者代理可以匿名访问,此字段省略

  • nonProxyHosts 字符串值,代表哪些服务器的网络访问并不需要通过此代理生效,对于这个字段有如下规则:

    • 多个条目通过字符| 分隔,这个过滤器是逻辑或,例如baidu.com|bing.com 代表对这两个域名同时不生效

    • 有通配字符:* 代表任意长度的字符,? 代表单个字符,例如*.company.com 代表所有次级域名和主域名

    • 此字段用于设置代理“黑名单”模式,如果要设置“白名单”模式,应当设置server.configuration.proxy 而非此字段

一个可能的实例配置是:

<proxies>
  <proxy>
    <id>default</id>
    <active>true</active>
    <protocol>https</protocol>
    <host>127.0.0.1</host>
    <port>7890</port>
    <!-- none authentication, omit username & password -->
    <nonProxyHosts>*.company.com|mirrors.aliyun.com</nonProxyHosts>
  </proxy>
</proxies>

3.6 配置文件功能

Maven作为一个全流程生效并且具有丰富插件生态的构建工具,其灵活程度是不足以单纯通过构建流水线和上文提到的全局配置完成的,因此如同其他的经典构建工具例如CMake一样,Maven提供一种对于不同场景进行不同精细化操作的方法也就是配置文档或者说环境文档Profile。虽然这种设置通常因为工程具有很大的差异,因此常常在下文要重点分析的pom.xml也就是工程的本地配置文件之中生效,但是对于多个工程或者某一类场景的配置应当进行抽象提升,也就是放在settings.xml 之中生效,有这样两类节点相关:

  • 节点profiles 是一个数组型容器节点,包括多个profile 节点,定义不同的环境配置文件

  • 节点activeProfiles 是一个数组型容器节点,包括多个单值activeProfile 节点,定义哪些环境配置文件常生效

我们首先来看profile 类型的节点,这是一个容器对象型节点,主要包括这样几个子节点:

  • id 节点,这是本条环境配置的唯一标识符,不可重复定义,并且这个字段将会和具体工程之中pom.xml 之中定义在工程上下文之中的同字段Profile相联系,是一个字符串类型的单值节点。

  • activation 节点,这是本条环境配置在何时生效的描述节点,是一个容器对象型节点,一个Profile的激活有以下几个特性:

    • Profile可能通过activation 节点满足开启条件,从而生效;也可以通过设置文件之中上文提到的activeProfiles 节点之中强制使某个环境配置生效;也可以通过命令行直接指定生效例如mvn -P testProfile 就会让id=testProfile 条目的配置生效。

    • Profile如果在设置文件setting.xml 之中生效并且在pom.xml 的工程配置中也生效,那么前者形成的设置效果将会和后者的设置效果合并生效,如果二者产生矛盾,那么settings.xml 之中的配置占据主导地位。

    • 由于Maven在此处的逻辑功能复杂,用户可能感到困惑,通过插件maven-help-plugin 可以列出当前工程配置下有哪些环境配置正在生效,这样的检测并不会开启构建动作:mvn help:active-profiles

  • properties 节点,这是本条环境配置生效后在构建过程之中所定义或者更改的环境变量或者代码运行时变量的值,这个节点也是一个数组型容器节点,包括多个property 节点能够同时生效改变相对变量或者常量的值

  • repositories 节点,这是本环境配置生效后针对Maven仓库的精细化控制,同样是数组型容器节点。

  • pluginRepositories 节点,这是本环境配置生效后针对Maven插件仓库的精细化控制,同样是数组型容器节点

首先讨论profile.activation 节点的详细情况,这个节点是一个对象型容器节点,包含多个触发条件,在这些触发条件之中除了一个默认开启的属性具有一票决定权之外,其他的激活条件是与逻辑的关系,即所有的触发条件全部满足才能够开启本环境配置,这些条件为:

  • 条件activeByDefault 此条件较为特殊,这是一个布尔值,如果设置为true 那么本环境配置在没有明确指定激活条件的时候自动开启,也就是默认启动,但是一旦指定了其他的开启条件这个一票决定就不生效。

  • 条件jdk 确定在何种JDK版本下这个环境配置生效,这里的条件仅仅控制大版本也就是说如果我们设置JDK版本为17那么对于17.0.1也生效,此处虽然可以注入多个JDK版本条件,但是由于它们是与逻辑生效,故而设置多个JDK版本就代表着这个环境配置永不生效。并且这个字段可以设置范围值,也就是说[8,12) 这样的配置可以实现从JDK8开始(包括)到低于JDK12的所有版本JDK的筛选,如果不设置版本下限或者上限可以写作,17] 或者[11, 这样的形式。

  • 条件os 确定在何种操作系统之下激活此环境配置,这个条件可以细化为子条件:

    • 子条件name 确定何种操作系统名称生效

    • 子条件family 确定何种类型的操作系统生效,例如说此条目设置为Linuxname 确定发行版

    • 子条件arch 确定何种类型的硬件CPU架构生效,例如设置ARM/x86

    • 子条件version 确定操作系统的具体版本,可以细化到Build序号

  • 条件property 确定在何种变量满足特定值的条件时生效,可以多个property 同时存在,设置名称取值和常见设置有:

    • name 子条件确认属性值的名称,value 子条件确认属性值的取值

    • name 子条件可以是mavenVersion 确保Maven的版本合适

    • name 子条件可以是env.XXX 从环境变量之中提取特定的值判断

    • name 子条件可以是其他Maven脚本确定的字符串,只要能进行取值判断即可,一般来说这样的值通常通过命令行输入或者在具体工程的pom.xml 之中进行详细配置

  • 条件file 确定哪些文件存在或者哪些文件不存在时生效,可以有若干这两种配置:

    • exists 子条件,单值字符串代表文件路径,文件存在时生效

    • missing 子条件,单值字符串代表文件路径,文件不存在时生效

  • 条件dependency 确定哪些依赖库存在时激活配置文件,通过GAV坐标确认具体依赖的坐标,当然此条件可以多重存在。但是一般这种配置在pom.xml 之中详细配置而在此处只是进行简单的过滤操作。

一个可能的activation 组合条件可能为:

<activation>
  <!-- 有具体条件, 不设置默认条件 -->
  <activeByDefault>false</activeByDefault>
  <!-- JDK 17激活 -->
  <jdk>17</jdk>
  <!-- x86 Windows 10生效 --》
  <os>
    <name>Windows 10</name>
    <family>Windows</family>
    <arch>amd64</arch>
  </os>
  <!-- Maven 3.9.9生效 -->
  <property>
    <name>mavenVersion</name>
    <value>3.9.9</value>
  </property>
  <!-- 定义生产环境生效 -->
  <property>
    <name>env.CONTEXT</name>
    <value>production</value>
  </property>
  <!-- 限制配置文件 -->
  <file>
    <exists>${project.basedir}/distribution.properties</exists>
    <missing>${project.basedir}/endpoint.properties</missing>
  </file>
</activation>

从节点properties 开始具体设置的就是当环境设置生效时Maven的行为,此条目设置当配置生效时对构建工程的过程作何种属性值的设置。这个条目是一个数组型容器,其中有多个条目,条目的普遍格式为:

<properties>
  <!-- Set value ${NAME} = ${VALUE} -->
  <${NAME}>${VALUE}</${NAME}>
</properties>

其中设置的具体值${VALUE} 不必多说,而设置的值${NAME} 可以是:

  • env.${NAME} 这样可以设置系统环境变量例如PATH就是${env.PATH}

  • project.${NAME} 这样可以设置pom.xml之中的工程变量例如工程版本${project.version}

  • settings.${NAME} 这样可以设置settings.xml 之中的变量例如${settings.offline}

  • java.${NAME} 这样可以设置Java代码之中通过java.lang.System.getProperties() 获得的变量例如${java.home}

  • 任意变量影响到后续变量使用或者文件路径例如user.install 设置构建目标安装目录

故而一个可能的设置是:

<properties>
  <user.install>${user.home}/example-directory</user.install>
  <project.version>3.0.1-snapshot<project.version>
  <settings.interactiveMode>false</settings.interactiveMode>
</properties>

而节点repositories 作为一个数组型容器可以包含多个repository 用于定义如何从远程仓库之中拉取依赖库或者拉取什么依赖库,这样就可以精细控制依赖库的组成进行多种多样的测试和构建工作,repository作为对象容器包含的内容可能有:

  • 条目id 设置仓库的名称,这是唯一标识符,能够像是前文的server.id 一样生效并且和构建的具体工程之中的pom.xml 形成信息传递通道,进而更加细致的配置远程Maven仓库的行为。

  • 条目name 设置仓库的显示名称,属于程序员用户友好界面,无实际意义

  • 条目url 设置仓库的网络地址

  • 条目layout 设置仓库的具体格式,这个配置与GAV坐标系统有关,这种配置的分野出现在Maven从1.x跨越到2.x大版本的情况,例如说对于同样的一个依赖库com.example.department:project-1.0.0.jar 有这样两种存储方式:

    • 对于1.x版本的目录结构为legacy :com/example/department/project/project-1.0.0.jar

    • 对于2.0+版本的目录结构为default :com/example/department/pejct/1.0.0/project-1.0.0.jar

  • 对于同一个依赖库而言,一般来说我们分为稳定版本releases 和测试快照版本snapshots ,这就是两种不同的条目,分别配置了对于同一个Maven依赖仓库而言不同类型的文件应当如何配置,它们内部的细分条目结构完全一致:

    • enabled 子条目设置仓库之中的该通道是否可用,例如生产环境要求稳定可以关闭掉整个快照通道的使用

    • updatePolicy 子条目设置对于目标依赖库的更新策略,有这样几种配置:

      • always 每次执行构建总是进行更新

      • never 第一次拉取依赖库之后从不从远程更新本地文件

      • daily 默认值,距离上次更新超过一天即更新本地文件

      • interval:X 代表更新间隔不再是一天而是X分钟

    • checksumPolicy 子条目设置Maven部署构建结果到远程仓库时,为了保证数据传输的完整性需要同时传输校验和到部署目的地,那么对于目标依赖库管理过程之中的校验和错误或者丢失的状况有三种不同的处理方式:

      • fail 为默认值,如果校验失败Maven会失败并且抛出错误,停止构建

      • warn 警告策略,如果校验失败Maven给出一条警告信息但是并不停止

      • ignore 忽略策略,完全忽略校验和失败的错误,并且不停止构建过程

那么对于一个可能的仓库配置为:

<repositories>
  <repository>
    <id>codehausSnapshots</id>
    <name>Snapshot Repository</name>
    <releases>
      <enabled>false</enbaled>
    </releases>
    <snapshots>
      <enaled>true</enabled>
      <updatePolicy>always</updatePolicy>
      <checksumPolicy>warn</checksumPolicy>
    </snapshots>
    <url>https://snapshots.maven.codehaus.org/maven2</url>
    <layout>default</layout>
  </repository>
  <repository>
    <id>centralRelease</id>
    <name>Release Repository</name>
    <snapshots>
      <enabled>false</enbaled>
    </snapshots>
    <releases>
      <enaled>true</enabled>
      <updatePolicy>interval:60</updatePolicy>
      <checksumPolicy>fail</checksumPolicy>
    </releases>
    <url>https://maven.aliyun.com/repository/public</url>
    <layout>default</layout>
  </repository>
</repositories>

然而在Maven的仓库管理之中不只有依赖库管理,还包含插件管理,对于某个特定的工程可能使用特定的插件,或者对于全局构建而言我们需要用到业务相关的插件组,那么这个时候就要用到节点pluginRepositories 这是一个包含若干pluginRepository 的数组型容器节点,只不过对于此节点来说其格式和内容与repositoriesrepository 非常相似因此不做赘述。

settings.xml 配置之中的最后一个节点是activeProfiles 也是一个包含单值字符串节点activeProfile 的数组型容器节点,其中的每个子条目的字符串值设定了什么Profile无视activation过滤激活条件直接常生效

<activeProfiles>
  <activeProfile>${PROFILE_1}</activeProfile>
  <activeProfile>${PROFILE_2}</activeProfile>
</activeProfiles>

4.核心文件pom.xml与POM概念

众所周知,前文也介绍过,Maven工程的核心标志就是根目录下有一个pom.xml 文件,那么究竟什么是POM呢?POM,Project Object Model,也即工程对象模型,是Maven之中的最基础工作单元pom.xml 是一个包含了工程配置细节的XML格式文件,用于指导整个构建和依赖管理流程。当我们执行任何的mvn 指令去完成依赖解析和构建阶段的时候都要检查并且按照当前目录下pom.xml 的指导。在这里的POM有两个特殊概念:

  • 超级POM,Super POM 也就是一个Maven系统的默认POM,所有工程的POM的任何配置除非用户显式设置都继承自这个超级POM,也就是所谓的默认配置。

  • 最小化POM,Minimal POM 也就是支持一个工程构建的最小化配置,这个范围内的所有XML元素用户都必须手动显式的指定否则Maven无法正常工作。

最小POM的一个实现如下所示:

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.app</groupId>
  <artifactId>example-project</artifactId>
  <version>1.0</version>
</project>

通过如上的最小功能POM配置我们可以知道:

  • POM文档的根节点是<project/> 标签,所有内容都必须在这个标签中出现

  • 必须指定POM文档的版本也就是<modelVersion/> 标签,在Maven3.x下使用4.0.0

  • 必须指定下文第一个小节要详细讨论的GAV坐标

而作为所有POM默认值的超级POM在Maven3.x之中统一为:

<project>
  <modelVersion>4.0.0</modelVersion>

  <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>
    </pluginRepository>
  </pluginRepositories>

  <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>
      <plugins>
        <plugin>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>3.1.0</version>
          </plugin>
          <plugin>
            <artifactId>maven-assembly-plugin</artifactId>
            <version>3.7.1</version>
          </plugin>
          <plugin>
            <artifactId>maven-dependency-plugin</artifactId>
            <version>3.7.0</version>
          </plugin>
          <plugin>
            <artifactId>maven-release-plugin</artifactId>
            <version>3.0.1</version>
          </plugin>
      </plugins>
    </pluginManagement>
  </build>

  <reporting>
    <outputDirectory>${project.build.directory}/site</outputDirectory>
  </reporting>

  <profiles>
    <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-no-fork</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>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>

对于一个完整的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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- Basic Configuration -->
  <groupId/>
  <artifactId/>
  <version/>
  <packaging/>
  <properties/>
  <dependencies/>
  <dependencyManagement/>
  <parent/>
  <modules/>

  <!-- Build Configuration -->
  <build/>
  <reporting/>

  <!-- Meta-Ino Configuration -->
  <name/>
  <description/>
  <url/>
  <inceptionYear/>
  <licenses/>
  <organization/>
  <developers/>
  <contributors/>

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

  <!-- Project Profiles -->
  <profiles/>

</project>

可以看到固定的标签和节点为:

  • project 标签作为整个POM文档的根节点出现,并且需要设定整个文档的命名空间也就是xmlns ,文档结构合法性xmlns:xsi ,以及解析文档结构所需要的Schema文件xsi:schemaLocation

  • modelVersion 标签确定了POM文档的模型版本,在Maven3.0.0+之上一致使用最新版本4.0.0。

4.1 坐标与属性配置

一个pom.xml 就像是Ant之中的build.xml 一样,包括了一个工程之中的所有必要信息以及构建过程之中的所有配置和插件信息。如果说Maven之中上文提到过的构建流程和生命周期提供了How和When的信息,POM文档之中的配置就提供了Who,What,Where的信息,当然这并不是说POM无法影响构建流程,通过上文的介绍我们了解到使用不同的插件绑定或者是使用maven-antrun-plugin 这种特殊插件我们就能够自定义构建流程和生命周期甚至能够将Ant的Task融入到POM之中处理。

对于一个在Maven体系之中需要投入使用的构件或者工程,最终要的是什么呢?当然是能够融入Maven的工程生态之中,这也决定了我们要怎样寻找到其他开发者制作的代码轮子作为依赖或者插件,这就是Maven之中的核心之一:GAV坐标系统。这个坐标系统将会对一个发布或者快照模式的构件决定一个唯一的字符串去确定它在整个Maven生态系统之中的索引,这个坐标主要由三部分构成:

  • "G"代表Group ID,也就是标签groupId ,这是一个单值字符串类型的节点,代表着构件目标所属的组织,例如所有的Maven核心构件和系统组件都具有同一个组织标识符org.apache.maven 。这里需要指出的是Maven鼓励开发者们通过严格遵循Java原生包名的逻辑进行命名,也就是将开发者所属组织的域名倒置以命名,这样的好处有:已知由于互联网DNS寻址的规律,域名一定是唯一的,这就避免了不同开发者使用同一包名以致于造成歧义的尴尬问题;其次是如果将其中的. 符号替换为文件系统目录层级的分隔符将会非常有利于包的存储,这也是Maven仓库存储构件的通用方式;最后这样有利于识别出包和开发者之间的层级结构关系。当然对于一些特殊的包或者说早期的包来说它们并不符合这个规范例如junit 工具,故而对于用户Maven只是鼓励使用这种方式而并非强制。

  • "A"代表Artifact ID,也就是标签artifactId ,这是一个单值字符串类型的节点,代表着构件目标本身的名称。在Maven系统之中所有的构建产出都称作构件(Artifact),这个字段具有严格的命名要求,这个字符串只能包含小写字母,阿拉伯数字和连字符号,通常来说考虑到在一个组织之中开发者在相互讨论时不会频繁使用自己所在组织的名称,因此我们建立一个代表构件本身的名称字段。对于不同部门的构件不应当放在一个组织ID之中处理或者干脆通过连字符在构件ID之中区别。例如说对于开发和文档两个部门的构件:

    • com.company.project.dev:database-driver 代表开发部门的数据库驱动包

    • com.company.project.dev:web-backend 代表开发部门的前端WebApp后端

    • com.company.project.doc:web-api-doc 代表文档部门的Web API说明文档

  • "V"代表构件版本,也就是标签version ,这是一个单值字符串类型的节点,代表着构件目标的版本。我们都知道随着业务逻辑的不断变化和新技术的应用,我们对于同一个功能使用的实现代码会不断产生变化,这就需要通过版本号去进行区分。这个字符串需要遵照严格的规范,但是这个规范仍然是Maven鼓励开发者遵守而非强制开发者遵守的,应当按照Semantic Version的规范:

    • 整体应当符合X.Y.Z 的格式,它们依次是majorVerminorVerpatchVer

    • 最后的Z代表Patch Version也就是补丁版本,如果发布了某个小版本的软件构件后对于代码需要进行小的调整例如某个函数的实现和修改BUG应当更改此处的补丁版本号以适配版本变化,标定功能的微小变化。

    • 中间的Y代表Minor Version 也就是次级版本,如果发布了主要版本后过了一段时间增加了某些API的功能或者调整了较大的实现结构以获得了增补或者功能削减较大的新构件,那么应当更改次级版本的编号标记版本。

    • 最初的X代表Major Version 也就是主要版本,如果软件的功能或者代码出现了架构上的变更或者出现了里程碑式的调整又或者其外特性例如API出现了极大的更改以至于上一版本无法依赖此版本,就应当更改主要版本号。

    • Maven之中将构件的目的或者说功能指标划分为两个阶段,也就是发布版本release 或者测试/快照版本snapshot ,对于前者而言不需要对于版本号做什么额外的更改,但是对于快照版本需要版本号后附加-SNAPSHOT这样以来在用户提交该构件到Maven仓库的时候仓库就会自动将此构件存入快照通道,同样如果选择快照版本做依赖,Maven将会选取最新提交的快照

通过以上的GAV坐标我们就能够通过$<groupId>:$<artifactId>:$<version> 唯一的定义一个构建在仓库之中的标识符并且在所要求构件存在的情况下通过这个坐标系统在仓库之中定位到具体的文件。至此已经完成了一个最小化POM需要的一切信息,然而通过前文的介绍我们了解到Maven的构建目标和打包方式是多种多样的,这就要讲到<packaging/> 标签,这个标签也是一个单值字符串标签,它代表了当前的工程结构、构建生命周期、默认插件绑定以及打包结构符合哪种构建目标:

<packaging>${type}</packaging>
  • jar 构建目标为Java类库项目,这也是<packaging> 标签的默认选项,如果缺省这个标签就会自动设置为Java类库项目

  • war 构建目标为Java驱动的Web Application项目,包含Java代码,Web资源以及Servlet或者JSP驱动的WebApp代码和元信息

  • ear 构建目标为JavaEE企业级应用,通常包括多个模块用于部署到JBoss或者WebLogic这样的JavaEE容器之中

  • ejb 构建目标为Enterprise JavaBeans模块,通常来说这样的构件会和其他EJB模块一同部署在JavaEE容器之中组成App

  • rar 构建目标为企业级模块的容器适配器,用于对Java Container Architecture(JCA)适配对外接口连接其他组件使用

  • pom 无构建目标,表示构建项目为父项目或者聚合项目,这类项目专门负责Maven工程聚合而不产生具体构件

  • maven-plugin 构件虽然仍旧为一个Java类库,但是专用于Maven的插件扩展,拥有独特的生命周期、绑定关系和项目结构

除了packaging 之外最后一个基础标签是<properties/> ,我们在书写POM文档时可能多处需要使用到同一个变量或者这些不同的字段具有一定的联系,需要保持相同的取值,例如Spring框架下的工程在设置Spring组件版本时就会碰到这个问题;又或者Maven或者其他的什么插件可以根据某个变量的设置进行不同的功能设置,这时候我们就可以通过这个标签完成这样的统一字段管理任务<properties/> 是一个数组型容器节点,其中可以包括多个值的设置,格式为:

<properties>
  <${propertyName}>${propertyValue}</${propertyName>
</properties>

例如这样的配置:

<properties>
  <!-- 设置源码编译版本以及目标使用版本的JDK -->
  <maven.compiler.source>1.8</maven.compiler.source>
  <maven.compiler.target>1.8</maven.compiler.target>
  <!-- 设置源代码的编码格式 -->
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <!-- 设置自定义常量值 -->
  <author>Fenice Liu</author>
</properties>
  • 设置${java.value} 能够在Java代码之中通过java.lang.System.getProperties() 获取值

  • 设置${env.value} 能够更改项目之中指定环境变量的值,既可以在Maven之中生效也可以在Java代码中生效

  • 设置${settings.value} 能够更改配置文件settings.xml 之中在全局配置和用户配置合并后最终的值

  • 设置${project.value} 能够更改当前POM之中的值例如project.version 就可以替换<version/> 标签

  • 可以自定义常量,例如上文的<author/> 可以通过${author} 在定义后POM的任意位置引用

4.2 依赖管理配置

Maven作为当前最受欢迎的Java构建工具,其最重要的基石就是依赖管理功能,依赖管理在开发历史上具有长期处于复杂、繁荣、混乱的“光辉历史”,英文之中有一个"Armageddon"的词汇用于描述某种巨大的灾难,同样的依赖各种不同的.jar 工作的Java构建工作在依赖管理方面被称作Jarmageddon或者干脆说“Jar Hell"就行了,这种混乱来源于多个方面,大致出现以下几种令人不适的情况:

  • 某个工程需要依赖多个.jar 工作,然而这些依赖库依赖更多的.jar 工作,形成了一颗庞大的依赖树,程序员难以手动管理如此巨量的依赖关系,并且使用一个库时往往需要首先搞清楚其依赖关系而不能只关注功能代码,否则就会ClassNotFound

  • 依赖树实际上并不安分,它并非一颗干干净净的树状结构——如果依赖A和B同时依赖了库C那么就会造成枝干之间互相搭桥的菱形依赖关系,这可能造成相当程度上的混乱——如果你决定弃用B而删除了B及其所有依赖那么当你使用A的功能时就会出故障

  • 版本之间难以分辨,从互联网上下载的依赖库如果其维护者并不遵守一些约定俗成的命名方式或者下载路径那么版本会混乱无比,并且如果出现了上一条之中的菱形依赖但是二者依赖的版本不同就会造成C库的类重复定义的麻烦,这其中的麻烦难以论述

  • 库的命名规约并不是所有人都在遵守的,我们只能说程序员是自由的,通常来说混乱的库名称管理会造成许多重要的库和第三方魔改版本之间的名称雌雄莫辨,甚至说有别有用心之人能够通过这种不起眼的差异利用开发人员的疏忽通过依赖的方式侵犯软件安全

具体详述Maven依赖管理和仓库的机制并不是本文的内容,本文旨在对于Maven的基础概念和核心机制的生效进行一个大致的总结,这部分内容会放在后续文章内详述,那么在POM之内依赖主要与两个字段相关,首先就是<dependencies/> 标签。这个标签是一个数组型容器标签,可以包括多个<dependency/> 标签:

<dependencies>
  <dependency>
    <groupId/>
    <artifactId/>
    <version/>
    <classifer/>
    <type/>
    <scope/>
    <optional/>
    <exclusions>
      <exclusion/>
    </exclusions>
  </dependency>
</dependencies>
  • 子节点<dependency/> 是一个对象型容器节点,其中我们看到了我们熟悉的GAV坐标内容字段结构,对于一个依赖而言这个GAV坐标是必须提供的,这用于确定在Maven仓库之中如何查找到对应的依赖文件并且下载安装到本地,这也是我们的构建产品标准的GAV坐标的核心作用生效的体现。

  • GAV坐标作为依赖的查找索引就隐含了一件事Maven工程的自动依赖管理只能够覆盖通过Maven构建出来的构件作为其他工程的依赖,而在Maven体系之外的依赖则部分的需要手动管理——这并不是一件坏事,不需要担心这会妨碍到依赖管理的灵活性,实际上我们通过外部依赖导入和私有仓库能够相当程度上规避掉这种管理带来的强制性而尽情的享受这种管理带来的便利性

  • 标签<classifier/> 是一个可选标签,往往并不需要出现,这个标签指示了我们需要的依赖具体是什么变种,例如说默认为library-1.0.0.jar 但是我们有时候并不希望拿到已经构建好的版本,我们可能需要使用其带有源码的版本或者是文档版本,又或者由于JDK版本的迭代虽然都是同一个version 坐标但是对于不同的JDK具有不同的功能和API的小小差别,又或者这个依赖是通过作用于系统底层的syscall 实现了一些功能,那么我们就需要区分OS的版本种类……这时候我们就可以通过这个单值字符串标签指定,例如javadoc指定的就是library-1.0.0-javadoc.jar

  • 标签<type/> 某种程度上对应了我们构建时的packaging 选择,这是一个单值字符串字段默认为jar ,选择不同的依赖类型会在仓库之中对应不同的文件扩展名对应该工程POM之中的打包方式,是否添加到CLASSPATH 变量,其默认的类别标签取值等等方面,具体不在本文之中展开。

  • 标签<scope/> 代表了所需依赖的具体生效范围,这是一个单值字符串字段,但是其取值范围在以下几种情况之中枚举出现:

    • compile 默认选项,编译时生效

    • provide 与上一个选项略有不同,配置此选项说明在运行时由环境提供此处的依赖仅仅参与编译阶段

    • runtime 代表编译时不需要,但是在运行时需要并且需要通过环境提供

    • test 代表此依赖属于测试阶段使用,在软件正常工作流之中不需要

    • system 代表此依赖虽然需要使用,但是无论是编译时还是运行时通过OS提供

  • 标签<systemPath/> 代表了上一个标签之中标记为system 的依赖在OS之中的存储位置,默认在${java.home}/lib 之中寻找,但是如果通过设置文件settings.xml 或者POM之中的properties 机制修改那么将按照最后修改值计算路径。以上提到的修改方式是针对于所有依赖生效的,对于单个的依赖可以在此标签之中指定位置。

  • 标签<optional/> 是一个单值布尔量,代表了此依赖对于外部是否是可选的,对于这样一个场景:A依赖于B编译但是不依赖于B运行,而C依赖于A构建那么对于C而言依赖库B就并不一定要引入,也就是说对于C来说B是一个可选依赖,那么在C的POM之中就应当说明这一点以告知不必浪费资源管理A的可选依赖B。

  • 标签<exclusions/> 是一个数组型容器标签,含有多个<exclusion/> 标签,该标签是一个对象类容器节点,其中可以提供一个依赖的GA坐标:

    • 对于上文提到的菱形依赖问题,如果我们目前有依赖A而A又依赖于B工作,处于某种原因(可能是版本冲突或者使用特制的其他Group提供的具有等效性的依赖库能够替代B等等)那么我们就希望依赖A的同时不将A对应的B引入依赖体系,那么此时就应当使用这个<exclusion/> 标签将其排除

    • 同一个依赖之中我们可能期望排除多个二级依赖,例如对于A而言我们希望排除他的依赖B和C而留下D,那么我们就应当写两条关于B和C的排除而放任D通过自动的依赖机制进入依赖树。不同于标签optional 仅仅处理哪些“可选项”,此标签强制排除明确指示不计入依赖树的软件包,无论它是否是本工程构建成功的重要因素

    • 我们可以在GA坐标之中使用通配符,例如说我们将某个groupId 进行显示声明排除而将其artifactId 设置为* 那么就能够排除这个Group的任何构件进入依赖树,如果我们将GA同时设置为通配,那么就将此依赖的二级乃至更多的依赖分支同时排除,也就是在此处截断依赖树。通配符符合字符串通配工作原理。

    • 这种改变仅仅作用于父容器节点dependency 而非全局,也就是如果A和B同时依赖于C,但是我们仅仅在A之中排除了C没有在B之中排除C那么它将会仍然通过B的依赖树分支进入依赖管理机制。

标签<dependencyManagement/> 同样是一个数组型容器节点,并且其内容与上文刚刚介绍的依赖标签并无不同,他的主要目的是提供一种集中管理依赖版本的方式,避免在多个模块中重复声明相同的依赖版本。它并不会直接将依赖项添加到构建路径中,而是用于定义一个“版本管理”机制,确保所有子模块使用一致的依赖版本。这个标签主要用于工程聚合-继承的机制之中同一管理子工程的依赖机制,在此不做赘述。

4.3 工程聚合配置

前文讨论了Maven的依赖管理,但是实际上我们在常规的依赖树管理之中忽略的一点就是工程与工程本身的联系,而不是工程构件与工程构件的关系。Maven之中一个强大的依赖管理扩展功能就是工程的继承和聚合关系,也就是所谓的Inheritance&Aggregation。那么对于一般的工程而言我们就需要定义两种新的工程:

  • 作为被继承的原型而存在的工程,为子工程指定依赖管理或者配置上的规约,这样的工程被称为继承父工程

  • 含有多个其他的工程作为模块(Module)的仅仅负责各个模块之间组合和协调关系的工程,这样的工程被称为聚合父工程

对以上两种工程其显著的识别特征就是<packaging/>设置为pom 类型。我们首先了解一下父工程,一个工程只要为POM打包方式并且被其他工程引入为父工程依赖,那么其POM文档的配置就会被部分的作为子工程的默认值而存在,但是如果子工程对于这些可继承字段显式的重写了新的值,那么父工程的值作为默认值就会被显式写入的新值所覆盖。可继承的值都有:

  • groupId ,version

  • description ,url ,inceptionYear ,organization ,licenses ,developers ,contributors ,mailingLists

  • scm ,issueManagement ,ciManagement

  • properties ,reporting

  • dependencies ,dependencyManagement ,repositories ,pluginRepositories

  • build.plugin.executions ,build.plugin.configuration 等配置,根据配置匹配程度决定

然而以下字段即使父工程POM文档已经设置,并不会被继承到子工程之中:

  • artifactId

  • name

  • prerequisites

  • profile 只是环境配置不会继承但是已经满足profile.activation 的条件仍然会生效

那么我们拥有了一个这样的POM父工程之后可以在子工程之中使用<parent/> 标签实现继承:

<parent>
  <groupId/>
  <artifactId/>
  <version>
  <relativePath/>
</parent>

对于其中的GAV坐标我们不再赘述,但是对于<relativePath/> 需要作出说明:这个标签是一个可选项,如果没有这个标签那么Maven就会在本地仓库和远程仓库之中搜索对应的GAV目标。但是往往我们的父工程也是暂存在本地的,这样的话我们就可以通过两个POM目录之间的相对OS文件系统路径直接搜索,如果在本地搜索到对应的父工程就会免去仓库寻找依赖的步骤。例如在某个文件夹下有两个工程,他们分别是父工程目录BaseProjectImplProject 那么在后者的POM之中这个标签可以设置为../BaseProject

一个继承关系之中非常有用的依赖管理就是前文省略介绍的<dependencyManagement/> 标签组,如果父工程之中已经确定了使用一个依赖的具体GAV标签,那么在子工程之中仅仅需要指定依赖的GA坐标而Version就会自动匹配……一方面来说这种机制十分有助于对于一组工程进行标准化和集中化的依赖管理,但是从某些特定的应用角度这种机制对于不甚了解Maven的人又是危险的。例如说某个工程继承自A,这个子工程使用了依赖B和C,对于C而言需要一个较新的版本,但是A之中记录的C有一个较老的版本,那么这样来说子工程通过继承关系就会得到一个较老的版本C,那么对于子工程来说就很不妙了,这时候通过一些插件的依赖树管理方案可以解决,但是仍然要小心处理。这部分和依赖管理以及依赖冲突有关系的内容同样在后续文章之中介绍。

对于父工程继承到子工程之中的一些配置无法使用简单的语言进行简短的描述,他们具有复杂的关系和机制,例如其中的插件配置和复杂的构建部分的继承关系我们将会在后续有关于依赖管理机制的文章之中详细介绍。然而仅仅有父工程的设置选项会导致Maven的工程继承树碎片化而不能具有统一的抽象结构,因此这就像是Java之中所有的类一定继承自java.lang.Object 类一样,前文提到的Super POM就是所有没有设置<parent/> 标签的工程的默认继承模版,当然如果工程A是一个父工程,B继承自A,那么对于A之中没有进行配置但是在继承机制之中应当传递给B的选项,就会通过Super POM先传递到A再传递到B。

以上介绍的继承功能只是已知父工程后通过子工程的POM配置实现继承POM文档默认配置,尤其是依赖管理的版本管理和约束条件配置的功能,Maven还提供了一个聚合工程的方法,也就是反过来在已知子工程的条件下使用聚合进行多模块统一管理的功能,这就要说到基本标签之中的最后一个标签<modules/> 。这是一个数组型容器标签其中具有多个<module/> 子节点,这些子节点是单值字符串节点,代表了子工程的POM或者POM存在的目录相对于父工程POM的OS文件系统相对路径(当然也可以是一个绝对路径)从而绑定多个子工程。

<modules>
  <module>../another-project-in-relative-path</module>
  <module>another-project-inner-this-directory</module>
  <module>~/java-projects/another-project-with-absolute-path</module>
</modules>

因此我们可以知道聚合关系之中的子工程的路径可以是:

  • 与当前聚合工程目录平级的另一个目录

  • 在当前聚合工程内部的一个新的容纳了子工程内容和POM的目录

  • 在OS能够访问的文件系统的任何一处的Maven工程

  • 既可以指定POM存在的目录也可以直接指定POM的路径

看起来这样的结构似乎与工程集成完全重合,或者说有很大一部分冗余,这里需要说明:

  • 二者具有根本目的上的不同

    • Inheritance主要实现避免配置的多次重复书写和更改配置时多次修改同一内容不同文件的尴尬,当系统之中具有多个工程使用同一套环境和依赖关系时能够组织化规模化的一次性进行配置并且进行依赖管理上的版本统一,核心在于标准化管理配置和依赖。

    • Aggregation主要实现构建过程的操作统一和批量化,也就是说在一个子系统之中具有若干个相互独立但是需要配合工作的模块时通过一个聚合工程能够批量化的构建所有子模块,避免了手动执行各个模块的清理、构建、测试以及各个子系统集成测试。

  • 二者具有数据流通性和POM书写的不同

    • Inheritance关系之中,子工程必须明确<parent/> 节点,设置对应的父工程,而父工程可以对此“一无所知”,很多时候父工程可以作为安装到某个仓库之中的“标准规约”分发给不同的子工程作为依赖使用

    • Aggregation关系之中,父工程必须明确<module/> 节点,添加对应的子工程,而子工程可以对此“一无所知”,这种父工程通常不会安装到某个仓库之中而纯属在本地使用,因为它通常依赖本地的文件结构生效

  • 二者可以联合使用:假设我们目前有工程A,B,C三个,他们都属于某个大项目的一部分,功能上互相独立但是往往需要配合工作完成一个大系统的分工与合作。这时候为了使项目依赖版本统一,大工程配置通行于所有的子系统,我们建立一个继承父工程S1作为这几个子系统的parent 出现;但是同时我们需要进行测试和编写的是整个大系统功能和业务逻辑,因此每次对于三个子工程进行重复的构建阶段控制十分痛苦,我们建立一个聚合父工程S2作为这几个子系统的统一接口,S2之中需要添加ABC为module这样就能够让两个设计方向不同的父工程同时生效而同时享受两种不同的便捷工程管理模式。

整个工程关系上的聚合-继承关系可以这样总结:

maven-inheritance_aggregation.png

4.4 工程构建配置

目前的Maven3.X版本之中我们使用的POM模型4.0.0的XSD,在这个原型规约之中整个标签<build/> 负责工程之中具体和构建阶段以及生命周期绑定有关的部分,这个标签对应的功能和各种设定可以分为两个大部分:

  • BaseBuild 部分,也就是在project.build 这个顶层节点和profile.build 这个内嵌节点之中同时生效的部分,在这两个层次之中都可以完成配置,因此我们说它是通用的构建配置项

  • ProjectBuild 部分,也就是仅仅在project.build 这个节点之中才能生效的部分而在profile 环境配置之中非法或者合法但无效的配置项。由于这个配置作用于整个工程的全局体系,我们说它是全局构建/工程构建配置项

也就是<build/> 标签可能出现的位置是:

<project>
  <build>
    <!-- Part: BaseBuild -->
    <!-- Part: ProjectBuild -->
  </build>
  <profiles>
    <profile>
      <build>
        <!-- Only: BaseBuild -->
      </build>
    </profile>
  </profiles>
</project>

我们首先探讨BaseBuild 对应的节点结构,它们仅仅涉及基础和通用的配置,这种配置是可以脱离当前工程上下文存在的,也就是它更多的依赖的是构建阶段和生命周期的概念而非某个已经实例化为工程POM的文档的上下文,结构为:

<build>
  <defaultGoal/>
  <directory/>
  <finalName/>
  <filters>
    <filter/>
  </filters>
  <resources>
    <resource>
      <targetPath/>
      <filtering/>
      <includes>
        <include/>
      </includes>
      <excludes>
        <exclude/>
      </excludes>
    <resource>
  <resources>
  <testResources/>
  <plugins>
    <plugin>
      <groupId/>
      <artifactId/>
      <version/>
      <extensions/>
      <inherited/>
      <configuration>
        <items/>
        <properties/>
        <classifer/>
        <!-- other custom configuration -->
      </configration>
      <dependencies/>
      <executions>
        <execution>
          <id/>
          <inherited/>
          <phase/>
          <goals>
            <goal/>
          </goals>
          <configuration/>
        </execution>
      </executions>
    </plugin>
  </plugins>
  <pluginManagement/>
</build>

4.4.1 简单构建配置项

  • 标签<defaultGoal/> 是一个单值字符串节点,代表未指定构建阶段时默认执行的构建阶段动作,例如设定为install 那么当我们执行命令行mvn 就代表执行此处设置的默认构建动作mvn install

  • 标签<directory/> 是一个单值字符串节点,代表构建文件默认的存放目录,Maven本身默认存放${project.basedir}/target 通过这个节点的设置可以改变构建中间文件和目标文件的存放目录

  • 标签<finalName/> 是一个单值字符串节点,代表构建最终文件的名称,我们前文介绍了GAV坐标,默认名称为${artifactId}-${version} 这样的通用名称。但是如果我们使用的插件对名称进行了修改就会更改这个默认名称:例如在Maven官方提供的插件maven-jar-plugin 之中如果设定classifertest 那么最后的构建文件名称就会后缀-test 。而这个标签的核心功能就是直接指定最终结果的文件名。

  • 标签<filters/> 是一个数组型容器节点,包含若干个单值字符串节点<filter/> ,后者的值指向一个处于默认的filters/ 目录之中的.properties 文件用于从中读取出对应的属性值进入POM文档体系。在前文介绍Maven工程约定目录结构的章节之中我们提到过这部分资源,我们默认它处于src/main/filters 之中。

4.4.2 资源构建配置项

构建配置项之中的另一个重要功能就是确定工程使用的各种资源文件,例如各种矢量图和图标以及IconFont文件,Web Application的各种前端显示组件和配置等。资源文件通常来说并不是代码,它们在编译时并不使用但是需要严格的与工程代码绑定,用于生成代码、编译参考、运行时调用读取写入等等功能。对于整个<resource/> 来说它是一个对象结构,它的子节点属性有:

  • <directory/> 是一个单值字符串节点,指定了资源存在的路径,例如说我想要获取一个dir1/dir2/example.xml 我需要填写dir1/dir2 ,这个字段的默认值就像是前文介绍的默认工程结构一样为src/main/resources

  • <filtering/> 是一个单值布尔量节点,假设我们的构建配置选项之中已经配置了一个或者多个*.properties 文件,那么其中设置的属性值有可能在资源文件之中做变量匹配,那么这时候我们如果不想进行资源文件的变量匹配而是选择保持整个资源文件的完整性,那么这个选项就设置为false

  • <targetPath/> 是一个单值字符串节点,确定了资源文件最终在目标构建结果文件的打包中输出的位置,例如.jar 之中我们将配置项存放在META-INF之中

  • <includes/> 作为一个数组型容器节点包含多个单值字符串节点<include/> ,每一个子节点都对应一条包含规则,在这个列表之中的文件会被计入资源文件,这个值可以是一个文件名也可以是一个通配路径,例如*.svg 会匹配所有矢量图

  • <excludes/> 作为一个数组型容器节点包含多个单值字符串节点<exclude/> ,同样的每个子节点对应一条排除规则,表示匹配到的文件不应当作为资源文件出现,这个值同样可以是一个文件名也可以是一个通配路径

以上就是<resource/> 标签包含的内容,多个此类标签可以组合到<resources/> 节点之中生效,对于同类的容器<testResources/> 是一样的,只不过这个容器之中的资源标签代表的是用于测试的资源而非用于最终源码构建和打包的资源。

4.4.3 插件构建配置项

在前文就提到,生命周期执行的具体构架阶段动作实际上是和插件的目标相互绑定的也就是说真正的生命周期控制就是出现在插件构建配置选项之中,这个部分才是整个构建选项最有用最核心,灵活度最高的部分<plugins/> 是一个数组类型容器节点,内含有多个<plugin/> 标签,其中:

  • 需要如同依赖项和构建信息一样提供GAV标签,只不过此处对于插件而言有一个特殊的配置,如果不提供G坐标那么org.apache.maven.plugins 将会默认填充

  • <extensions/> 是一个单值布尔量节点,代表是否引入插件自定义的扩展生命周期和构建阶段操作,默认值为不开启

  • <inherited/> 是一个单值布尔量节点,代表这个插件的配置当本工程是一个POM父工程并且继承到子工程之中时父工程这个插件设置是否集成到子工程之中,这个特性和前文提到的工程继承POM属性对应,默认为开启继承

  • <dependencies/> 与前文出现的依赖管理节点之中的同名标签结构相同,只不过这组依赖并不是工程本身的依赖而是工程在此处用到的插件的依赖列表,用于使对应插件或者插件的某个目标正常工作,每个插件的依赖单独配置。

  • <configuration/> 标签是插件配置之中最复杂和最灵活以及最核心的配置项。每个配置针对于每个插件甚至每个插件的某个执行设置单独生效,但是这些多层次的配置标签并不会在POM之中出现相互矛盾和模糊设置的问题。如果一个子工程继承了父工程的POM而父工程配置了插件的configuration 那么它也会继承。

    • 首先说明继承规则,如果一个节点在父工程之中定义在子工程之中未定义那么就会使用父工程的定义。对于数组而言只要子工程定义了同名的数组容器那么子工程配置的数组内容就会完全取代掉父工程的数组内容而非合并两个数组,但是对于对象容器而言,如果子工程只是额外定义了不同的对象成员而没有覆盖父工程的对象成员,那么没有覆盖的部分仍然将集成到子工程之中。

    • 我们可以使用标签定义属性combine.childrencombine.self 定义具体的合并继承规则,而非仅仅利用默认的规则

      • 对于一个标签而言可以这样写<items combine.children="append">...</items>

      • combine.children 用于规定这部分内容将如何从父工程的POM之中进行合并而combine.self 用于指定本工程POM之中的标签项将会如何与父工程进行合并,二者作用方向不同,但是同样都会沿着继承链传递下去,因此这种操作在多层继承关系之中要审慎的进行,否则可能造成沿着继承链的污染产生

      • 选项"merge" 为默认情形,也就是上文所描述的继承与合并规则

      • 选项"append" 常用于combine.children 之中,代表父工程POM的数组将会和子工程POM的数组合并使用而非使用子工程数组内容替换

      • 选项"override" 常用于combine.self 之中,代表无论父工程有没有设置本对象的成员,本工程的POM设置都会完全替换父工程的对象设置

      • 如果combine.self="override"combine.children="append" 同时出现那么并不会造成处理机制上的歧义和错误,Maven优先使用前者生效

    • configuration 节点并无一定规矩,实际上这个节点主要面向各种各样的插件需要提供的配置项生效而非直接针对工程POM文档生效,因而面对不同的插件需要进行的配置也是不同的,有些插件可能需要配置对象<properties/> 标签组而有些插件可能需要配置数组<items/> 标签组,或者两者兼有或者需要配置其他独立命名的标签。因此在书写这个节点或者说使用一个插件的时候需要根据插件的说明文档进行设置,也许需要设置构建和测试的JDK版本或者代码文件编码规范……

  • <executions/> 是一个数组型容器节点,包含多个<execution/> 对象节点,规定了一个插件的某个目标或者多个目标或者多个插件的多个目标绑定到哪个构建阶段参与生命周期和将会以怎样的运行参数参与到构建阶段之中,这个对象节点包含有:

    • <id/> 为一个单值字符串标签,代表了这个执行动作的名称,不可与同一插件的其他执行动作的名称相互重复,这个名称将会在Maven执行这个动作的之后进行显示:[${plugin}:${goal} execution:${id}]

    • <inherited/> 决定了这个执行动作的配置是否继承给子工程

    • <phase/> 为一个单值字符串字段,必须是项目生命周期之中的一个阶段,代表着这个插件的执行动作将会绑定到哪个阶段之中执行

    • <goals/> 这是一个数组型容器节点,包含多个单值字符串节点<goal/> ,这个单值节点指定了本插件的哪个目标能够在这个执行动作之中运行,如果在条目之中将多个目标写入那么就能够实现一个构建阶段的多个目标,而如果多个插件的配置中同时绑定到同样一个构建阶段,那就实现了多个插件目标在一个构建阶段之中的同时执行(它们仍然有执行顺序)

    • <configuration/> 如同前文介绍的配置标签,具有相同结构,不过这个配置节点仅仅服务于这个执行任务,决定这个执行条件的一些参数

考虑这样的一个插件配置场景:

如果我们希望添加插件maven-autrun-plugin 并且在verify 生命周期阶段之中执行这个插件的run 目标,而且我们希望这个动作执行时显示当前工程进行构建时的中间文件存放目录,那么我们就需要配置插件字段。

<plugins>
  <!-- Other Plugin Configuration -->
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.1</version>
    <executions>
      <execution>
        <id>EchoBuildDirWithAnt</id>
        <goals>
          <goal>run</goal>
        </goals>
        <phase>verify</phase>
        <configuration>
          <tasks>
            <echo>Build Directory: ${project.build.directory}</echo>
          </tasks>
        </configuration>
      </execution>
    </executions>
  </plugin>
</plugins>

对于<pluginManagement/> 而言,其内容和配置方案与上文介绍的<plugins/> 标签组完全一致,但是这个标签并不是用于配置工程本身的插件如何执行,而是当本工程作为父工程其POM文档被继承时作用于子项目中的POM文档的插件管理。但是仍然需要强调的是:我们在这里写入的所有插件配置都会继承下去,如果其中有一个插件的目标绑定到了生存周期中的一个阶段,那么这个阶段就会出现在所有的子工程之中,即使子工程本身并不需要这个生命周期阶段,所以和继承有关的任何操作都需要审慎执行。

4.4.4 全局构建配置项

最后一部分配置是仅用于顶级标签project.build 之中的配置项而并不能作用于环境配置的profile.build 选项,这部分选项作用的目标是整个工程因此不再后者的配置部分中出现。这部分配置选项有6个,但是可以根据作用分为一组和目录有关的(5个)配置项和单独的一个extension 配置项,我们首先来看目录组:

<build>
  <sourceDirectory/>
  <scriptSourceDirectory/>
  <testSourceDirectory/>
  <outputDirectory/>
  <testOutDirectory/>
</build>
  • <sourceDirectory/> 单值字符串标签,设定当前工程的源代码存储目录,默认值${project.basedir}/src/main/java

  • <testSourceDirectory/> 单值字符串标签,设定当前工程的测试代码目录,默认值${project.basedir}/src/test/java

  • <outputDirectory/> 单值字符串标签,设定当前工程的字节码输出目录,默认值${project.basedir}/target/classes

  • <testOutputDirectory/> 单值字符串标签,设定测试部分字节码输出目录,默认值${project.basedir}/target/test-classes

  • <scriptSourceDirectory/> 此设定已经在当前版本之中被删除,不应当使用

最后就是<extensions/> 标签,这也是一个数组型容器标签包含多个<extension/> 对象型容器标签,事实上这个标签就是提供一个GAV坐标值,代表着仓库中的某个构件。这些构件或者提供基础驱动功能或者提供扩展性的功能,它们不需要作为依赖耦合到构件之中也不需要作为插件绑定到生命周期,仅仅作为本工程构建的“环境支持”而存在,因此仅仅在此处添加GAV坐标即可自动由Maven下载、管理、生效。

4.5 工程文档配置

<reporting>
  <outputDirectory/>
  <excludeDefaults/>
  <plugins>
    <plugin>
      <groupId/>
      <artifactId/>
      <version/>
      <reportSets>
        <reportSet>
          <id/>
          <inherited/>
          <reports>
            <report/>
          </reports>
          <configuration/>
        </reportSet>
      </reportSets>
    </plugin>
  </plugins>
</reporting>

前文提到,在Maven的生命周期之中有一个环节site 用于生成工程项目的对应文档或者参考,那么整个<reporting/> 用于定义这个阶段Maven的行为。这个节点内容标签的定义方式和作用方式尤其是<plugins/> 标签组和前文提到的机制非常相似。首先来看其他两个简单标签的作用:

  • <outputDirectory/> 是一个单值字符串节点,用于设置生成文档的存储位置,默认值${project.basedir}/target/site

  • <excludeDefaults/> 是一个单值布尔量节点,我们用于生成文档的插件在生成文档的时候,例如JavaDoc,可能会对于很多类和API进行默认的生成或者配置了默认的生成文档规则,这部分内容有可能我们实际并不需要,因此只要将这个字段设置为true 就会避免这部分内容自动生成。

既然常规构建过程需要通过插件绑定目标到生命周期实现,那么生成文档的生命周期自然也需要通过插件实现,因此<plugins/> 插件组就可以完成这个工作。这里需要说明的是这里的插件配置和<build/> 中的插件配置额关系:后者能够完成的工作和运用的设置机制前者完全具备,但是前者作为文档生成的部分功能和机制在后者的配置之中是不存在或者不生效的。另外,前文讲到的<executions/> 由于这里没有更加复杂的生命周期存在,被<reportSets/> 标签组替代。这个标签组同样也是一个数组型容器节点,包含若干个子节点<reportSet/> ,其中的配置项与<executions/> 基本相同。

值得一提的是,不同于<execution/> ,这个标签之中取消了<phase/> 标签,并且将标签组<goals/> 替换为了<reports/> 标签组用于指定需要生成的文档类型以供生成文档的插件使用。同样的,复杂的<configuration/> 在这个标签组中仍然可用

4.6 工程Meta信息配置

<project>
  <name/>
  <description/>
  <url/>
  <inceptionYear/>
  <licenses>
    <license>
      <name/>
      <url/>
      <distribution/>
      <comments/>
    </license>
  </licenses>
  <organization>
    <name/>
    <url/>
  </organization>
  <developers>
    <developer>
      <id/>
      <name/>
      <email/>
      <url/>
      <organization/>
      <organizationUrl/>
      <roles>
        <role/>
      </roles>
      <timezone/>
      <properties/>
    </developer>
  </developers>
  <contributors>
    <contributor/>
  </contributors>
</project>
  • <name/> 是一个单值字符串标签,设置工程的“通俗名称”

  • <description/> 是一个单值字符串标签,设置工程的简短说明文本作为描述

  • <url/> 是一个单值字符串标签,设置工程的网站主页访问链接

  • <inceptionYear/> 是一个单值数字标签,设置工程最初运作的年份

  • <licenses/> 为一个数组型容器,包含多个<license/> 标签,用于设置当前工程使用的协议或者授权证书,例如工程使用了若干开源库那么必须说明自己的协议证书以遵守开源用户协议,这个标签内容具有:

    • <name/> 协议证书的名称

    • <url/> 协议证书的发行机构做出的规约或者原始版本的网络链接

    • <comments/> 协议证书的注释,说明证书的其他简要信息

    • <distribution/> 指示这个工程应当如何分发给用户,常用的设置项有两种,首先是repo 代表通过Maven仓库系统进行分发,其次是manual 代表手动安装

  • <organization/> 代表发布工程的组织信息,这是一个对象型节点,成员:

    • <name/> 组织名称

    • <url/> 组织的网站主页访问链接

  • <developers/> 是一个数组型容器节点,包含多个对象型容器节点<developer/> 这个节点用于描述开发者信息,成员详细情况为:

    • <id/> 开发者ID,一般来说在网络上或者所在组织内应当是独一的

    • <name/> 开发者的姓名,或者开发者意愿显示给公众的名字

    • <email/> 开发者的电子邮箱地址

    • <url/> 开发者的个人主页网络访问地址

    • <organization/> 开发者所属的组织,可能有时候一个组织推出的工程或者干脆开源工程其开发者来自不同的组织,这时候就需要注明开发者的组织名称。

    • <organicationUrl/> 开发者所属组织的网络访问链接

    • <roles/> 是一个标签组,其中的<role/> 字符串标签指示开发者的角色

    • <timezone/> 用户的时区例如中国常用Asia/Shanghai

    • <properties/> 属性标签组,用于添加若干自定义属性

  • <contributors/> 是一个数组,包含多个对象型容器节点<contributor/> 代表了非开发者但是对本工程具有贡献作用的个人,例如测试者或者开源代码编辑者。这个子标签内的所有信息都和<developer/> 标签完全一致。

4.7 其他工程环境配置

POM工程文档的最后一部分配置项主要有关于工程的版本控制、仓库管理、构建后部署任务,状态指示,运维过程中的持续集成和错误管理等事务。这里为了完整的介绍POM将其一一列举,事实上其中很多部分并不常用也并不需要,例如:

  • 本地建立Git仓库用于进行整个工程的版本控制和分支管理任务

  • 在远端采用GitHub/GitLab/GitLab私有部署,管理项目Issue列表和发布项目文档

  • 在远端采用Nexus建立私有Maven仓库管理项目构件和各个版本

  • 在远端采用Registry容器建立私有Docker仓库(或者K8S的对应方案)

  • 在远端采用Jenkins作为CI/CD集成工具并且通过WebHook实现各组件集成流水线

  • 在远端建立项目对外网站,采用WebHook连接以上各个组件更新状态和提供下载

本小节主要介绍的配置项及其结构有:

<project>
  <issueManagement>
    <system/>
    <url/>
  </issueManagement>

  <ciManagement>
    <syestem/>
    <url/>
    <notifiers>
      <notifer>
        <type/>
        <sendOnError/>
        <sendOnFailure/>
        <sendOnSuccess/>
        <sendOnWarning/>
        <configuration>
          <address/>
          <!-- other configuration -->
        </configuration>
      </notifier>
    </notifiers>
  </ciManagement>

  <mailingLists>
    <mailingList/>
      <name/>
      <subscribe/>
      <unsubscribe/>
      <post/>
      <archive/>
      <otherArchives>
        <otherArchive/>
      </otherArchives>
    </mailingList>
  </mailingLists>
  
  <scm>
    <connection/>
    <deveploperConnection/>
    <tag/>
    <url/>
  </scm>

  <prerequisites>
    <maven/>
  </prerequisites>

  <repositories>
    <repository>
      <releases>
        <enabled/>
        <updatePolicy/>
        <checksumPolicy/>
      </releases>
      <snapsots/>
      <name/>
      <id/>
      <url/>
      <layout/>
    </repository>
  </repositories>
  <pluginRepositories/>

  <distributionManagement>
    <repository>
      <uniqueVersion/>
      <id/>
      <name/>
      <url/>
      <layout/>
    </repository>    
    <snapshotRepository/>
    <site>
      <id/>
      <name/>
      <url/>
    </site>
    <relocation>
      <groupId/>
      <artifactId/>
      <version/>
      <message/>
    </relocation>
    <downloadUrl/>
    <status/>
  </distributionManagement>
    
</project>
  • <issueManagement/> 用于使用Bugzilla或者其他类似的工具追踪系统出现的问题,给测试者或者使用者提供一个问题反馈平添并由开发者处理。其中子节点<system/><url/> 都是单值字符串标签用于指定系统名称和访问链接。

  • <ciManagement/> 用于匹配CI(Continuous Integration)持续集成系统辅助开发工作,这种系统用于将新改动的代码不断集成到部署环境之中去达成敏捷开发快速部署等等目的,这种系统一般采用触发器或者定时器机制开启集成流水线。其中:

    • <system/><url/> 为单值字符串标签,指定CI系统名称和访问链接

    • <notifers/> 是一个数组容器节点,含有多个notifer 用于配置通知除法器,当系统构建阶段执行完毕之后会根据执行结构进行通知,可以配置:

      • <type/> 单值字符串节点,例如mail 或者webhook 等代表通知类型

      • <sendOnxxx/> 发生某某事件进行是否通知,它们都是单值布尔量节点,对应了构建执行的错误,失败,成功,警告四种结果,配置多个通知器能够选择在发生了何种事件时进行什么通知,触发CI系统不同的功能

      • <configuration/> 通知器配置,此处的配置节点与前文的格式并无关联

  • <mailingLists>是一个数组容器节点,包含多个<mailingList>用于配置项目工程的开发者,测试者以及重要使用者的电子邮件联络方式,其中:

    • <name/>是当前邮件列表的名字,有助于区分开发人员/用户或者不同部门

    • <subscribe/><unsubscribe/> 记录两个电子邮件地址,用于向用户提供本邮件列表的订阅/取消订阅链接地址

    • <post/> 如果要向这个邮件列表的订阅者发布消息应当在这个标签指向的电子邮件之中发送消息,但是为了隐私管理与恰当的使用一般来说邮件列表不会公开的配置发布邮箱地址——这可以有效组织无聊的人乱发布消息

    • <archive/> 和容器<otherArchives/> 用于配置当前邮件列表历史发布的消息存储的服务器访问地址,这样有助于还原沟通记录和工作留痕

  • <scm/> 用于配置SCM(Software Configuration Management)或者说就是版本管理和控制工具,一般来说是SVN或者Git的功能,指示本工程的版本控制仓库和访问的功能,只不过像是前文所述,一般来说开发者本地和服务器会部署Git进行管理:

    • <connection/><developerConnection/> 用于指定版本控制工具的访问链接也就是Git概念之中的仓库访问地址,前者需要读取权限,例如开源项目可能允许任何人进行访问,后者需要写入权限,包括商业或者私有项目的读取权限也和后者一样需要有用户认证审核,例如GPG密钥或者SSH密钥等

    • <tag/> 指定了这个工程目前处于整个版本控制树的哪个分支的哪个节点,例如如果Git具有Branch v1并且有Tagv1.0.0 此处便可以填写,如果采用默认的根那么直接填写HEAD 即可达成目的

    • <url/> 提供一个所有人都可以访问的链接,开源项目可能直接指向GitHub

  • <prerequisites/>用于指定项目最低使用的<maven/>版本,如果本地使用的Maven版本不满足这个要求及时提醒用户,但是从Maven3.X以来对于前后版本的兼容性已经做出很大的改善,版本之间的API和兼容性差异已经不那样明显,所以这个标签基本上在当前的Maven生态之中快要被启用了,不过Maven4.0发布在即,已经处于最后的测试阶段,因此可能在将来兼容性变差的时候这个标签又能够使用。

  • <repositories/><pluginRepositories/> 是两个数组容器节点,用于配置工程特有的仓库和插件仓库,这些仓库将会与settings.xml 之中配置的仓库与镜像合并使用最终构成依赖树从互联网获取支持的来源。它们都包含<repository/> 仓库,前文在介绍settings.xml 之中的profile 时有所提及,不如再次明确:

    • <id/> 是仓库在Maven系统之中的唯一识别字符串,我们配置的镜像加速仓库和服务器访问凭据都要绑定到这里生效

    • <name/> 是用于为人类程序员提供良好交互的显示名称字符串

    • <url/> 确定了仓库的访问地址

    • <layout/> 确定了仓库的文件存储系统是按照现代的default 进行还是按照古早Maven2.X时期的legacy 进行,有助于从仓库之中获取正确的文件

    • <releases/><snapshots/> 对应仓库的稳定版本和测试版本两个通道的依赖如何获取,他们具有相同的内容配置项:

      • <enabled/> 单值布尔量,确定是否允许开启此通道的依赖下载

      • <updatePolicy/> 用于确定如何更新依赖,前文介绍过按照时间间隔进行文件下载同步,立即同步,关闭同步的三种不同配置

      • <checksumPolicy/> 用于确定当校验出问题时如何处理当前依赖,参见前文

  • <distributonManagement/> 标签用于管理工程的分发配置

    • <downloadUrl/> 是一个单值字符串节点,指定了工程构建结果的下载地址

    • <status/> 是一个单值字符串节点,用于指定工程目前的状态,具有枚举值:

      • none 默认值,没有特殊的状态需要说明

      • converted 代表项目管理者已经将此工程的POM文档从Maven2.X迁移至3.X

      • partner 这个构件已经从另一个联合仓库之中被同步

      • deployed 最常用的标签,代表这个构件已经被部署到Maven仓库

      • verified 这个工程已经被完全验证过,可以认为已经完成

    • <relocation/> 代表当前的工程迁移到了GAV坐标,这是为了一些组织和开发者可能面对的项目树管理重构和部门重组,因此这个标签之中需要提供一组新的GAV坐标用于指示重定向,以及一个<message/>说明情况。

    • <site/> 用于指示文档生成后部署到何处分发,因此具有一个<id/>作为唯一标识符又有一个<name/> 用于展示信息,<url/> 用于指示文档部署链接

    • <repository><snapshotRepository> 用于配置分发所用的仓库和测试版本分发所用的仓库,这个仓库配置与前文提到的仓库配置略有不同:

      • <uniqueVersion/> 单值布尔量,用于指定仓库之中分发这个构件的地址是否应当生成一个独一无二的版本号亦或者使用给定的版本号

      • 与前文配置相同的标签有:<id|name|url|layout/>

5.学习资料

虽然笔者已经做到尽量审慎的解读官方文档,但是由于在一篇博文内需要解释的内容过多,而且有些选项笔者也不常用,因此非常有可能本文的内容出现错误解释、歧义、张冠李戴或者遗漏等情况。如有此等情况,笔者会尽快勘误,不足之处万望海涵!以下展示一些笔者认为对于了解和学习Maven的实际用法,相关生态,核心机制相当有效的文档和视频课程:


评论