Maven 笔记

Maven 是一个强大的 Java 项目构建工具。当然,你也可以使用其它工具来构建项目,但由于 Maven 是用 Java 开发的,因此 Maven 被更多的用于 Java 项目中。本文的目的是帮助你理解 Maven 的工作机制。因此教程主要关注 Maven 的核心概念。一旦你理解了这些核心概念,当你想了解更多的细节时,再去查看 Maven 文档,或者从网上搜索,就变得容易多了。事实上,Maven 开发者认为 Maven 不仅仅是一个构建工具。你可以去阅读它们的文档 Maven 哲学,看看它们是怎么想的。但现在,我们就把 Maven 当作一个构建工具,当你理解和开始使用 Maven 后,你就明白 Mavan 到底是什么了。

Maven 简单介绍

Maven 是 Apache 开发的 项目管理自动构建 工具,使用 Java 语言开发,主要也是用于管理 Java 项目(特别是 Java Web 项目)。

Maven 核心概念

生命周期(lifecycle)、阶段(phase)、插件(plugin)、目标(goal)

生命周期 是由一系列 阶段 组成的,maven 中定义了 3 个生命周期:cleandefaultsite。不同的生命周期之间是互相独立的、互不影响的。clean 生命周期负责项目清理,default 生命周期负责项目构建,site 生命周期负责项目报表。

生命周期内部的不同 阶段 之间是有顺序的,后面的阶段依赖于前面的阶段。比如存在阶段 A、B、C,当我们执行阶段 C 时,实际上是执行的 A、B、C 三个阶段,以此类推,当我们执行阶段 B 时,实际上执行的是 A、B 两个阶段。

生命周期和阶段都是抽象概念,因为它们实际上并不做任何事,真正做事的是插件和目标(实现)。

插件 的概念很好理解,插件中有一个或多个 目标(goal,可以理解为功能),也可以将插件理解为多个目标的集合。插件中的目标是实际干活的东西,一个阶段可以与一个或多个目标绑定,这样当我们调用某个阶段时,maven 会自动执行对应的目标组。如果一个阶段没有与任何目标相绑定,那么调用这个阶段实际上不会做任何事,即它是一个“空调用”。

我们可以这样理解,phase 是接口/抽象类,goal 是具体的实现类;接口仅仅起到一个规范的作用,实际的操作都是由实现类来完成的,而我们调用它们时,通常也是调用接口,而不是实现类,因为我们要依赖于抽象,而不是依赖于细节。在 maven 中也是一个道理,我们通常不会去调用 goal,而是调用 phase;因为单独调用 goal 会破坏 maven 的不同阶段之间的依赖关系,而调用 phase 的话则不会,这样我们就能够做到调用 C 阶段时,同时完成了调用 A、B、C 三个阶段。如果你仅仅是调用 C 阶段对应的 goal,那么实际上 maven 并不会调用 A、B 阶段对应的 goals。这样往往会导致某些问题。

maven 为了做到开箱即用,将常用的 phase 映射到了默认的 goal,这样我们在使用 maven 时就不需要自己定义常用的 phase 与 goal 之间的映射关系了。比如 mvn clean,调用的是 clean 生命周期中的 clean 阶段,而 clean 阶段默认映射到 maven-clean-plugin:3.0.0:clean 目标,即调用的是 clean 插件的 clean 目标,实际的清理工作都是由具体的目标来执行的,一个阶段对应的目标可以有 0 个、1 个、多个;如果有 0 个目标,那么这个阶段不会做任何事,如果有一个目标,那么这个阶段就是调用对应的那个目标来干事,如果有多个目标,那么 maven 会按照 pom.xml 中的目标声明的顺序来依次调用它们。

所以,调用 mvn clean 实际上就相当于调用 mvn clean:clean,前面是插件名称,后面是目标名称。当然严格来说不是这样的,因为 clean 之前还有 pre-clean 阶段,所以 maven 会先执行 pre-clean 对应的目标,但是因为 pre-clean 没有默认映射到的目标,所以基本上还是符合上面所说的。

总结:maven 的所有工作都是由具体的插件的目标来完成的,我们使用 maven 时,通常不会直接调用插件的目标,而是调用 maven 定义的阶段(可以由多个阶段,它们会按照顺序依次执行),因为阶段实际上会被映射到具体的目标,这样做的目的是为了减轻使用者的负担;而且可以依靠阶段之间的顺序来完成一系列的事,这是直接调用目标所做不到的事。比如执行 mvn package,调用 default 生命周期的 package 阶段,因为 package 之前还有很多其他阶段,所以 maven 会先执行它前面的阶段,然后才会执行 package 阶段(再罗嗦一下,执行阶段就是执行阶段对应的目标,一个阶段可以对应零个、一个、多个目标,如果是零个目标,那么这个阶段不会做任何事)。而如果我们直接执行 mvn jar:jarmvn war:warmvn ear:ear 这几个 package 阶段对应的目标,那么它实际上并不会执行 package 阶段前面的任何阶段,而是单纯的执行这几个目标罢了(破坏了阶段的顺序)。

maven 允许我们自定义 lifecycle、phase、plugin、goal(这是当然的),即使是我们自定义,我们也应该调用 phase,而不是直接调用 goal,因为这会直接破坏 maven 内部的不同阶段的依赖关系。

  • 执行指定的 phase:mvn <phase-name> ...
  • 执行指定的 goal:mvn <plugin-name>:<goal-name> ...
  • 获取指定 plugin 的帮助信息:mvn <plugin-name>:help ...
  • 执行指定的 phase、goal:mvn <phase-name>... <goal-name>...

上面的 mvn <plugin-name>:<goal-name> 中的 pulgin-name 其实是 plugin prefix。
mvn 命令行中还可以指定相关的 options 选项,它们可以放在 phase、goal 前面或后面。

maven 配置文件

maven 的配置文件为 pom.xml(项目对象模型),它位于项目的根目录。和 make 一样,pom.xml 与 makefile 承担一样的角色,所有与 maven 项目相关的配置都是位于 pom.xml 项目对象模型文件中。

pom.xml 文件概览

最小的 pom.xml

其中,groupIdartifactIdversion 被称为 maven 的坐标,maven 可以通过这三个信息(被称为三元组)唯一的定位任何一个 maven 项目。modelVersion 是 pom.xml 版本号,固定为 4.0.0

pom.xml 的继承
pom.xml 和 Java 中的 OOP 一样,存在继承关系,并且它们都是单继承的。和 Java 一样,如果一个 pom.xml 没有明确指定它的父 pom,那么该 pom.xml 的父 pom 就是 super pom(和 Java 中的 java.lang.Object 类一样,它是 Java 中所有类的基类)。

同样的,有继承就有重写(覆盖),子 pom 的相同配置可以覆盖父 pom 中的相同配置,这在 Java 中叫做 Override,在 maven 中叫做配置覆盖。maven 很好的利用了这一点,它在 super pom 中定义了很多默认的配置,这样我们就可以编写简短的 pom.xml 来完成很多复杂的任务。

在 maven 中,术语 effective-pom 表示当前项目的父 pom 和当前的 pom 相结合的 pom(即经过覆盖后的实际生效的 pom 配置),我们可以通过运行 mvn help:effective-pom 来查看当前项目的 effective-pom 配置。比如我们可以创建一个最小的 pom.xml,然后执行这个命令,就能知道 maven 中的 super pom 的具体配置信息,如下。

详解 pom 的三元组

  • groupId:组名,在某种程度上可以将 groupId 理解为 package 包名。
  • artifactId:项目名,其实就是 maven 项目的根目录的文件夹名称而已。
  • version:项目的版本号,版本号很好理解,有了版本号更利于项目的讨论。

而 modelVersion 模型版本在目前来说,始终是 4.0.0,这是目前唯一的版本号。

maven 中使用格式 groupId:artifactId:version 来描述一个具体的项目,比如:
org.apache:tomcat:8.5,表示 org.apache 开发的 tomcat,它的版本号是 8.5。

clean 生命周期的常用阶段

  • clean:执行项目的清理工作。

default 生命周期的常用阶段

阶段 描述
验证 validate 验证项目是否正确且所有必须信息是可用的
编译 compile 项目代码和测试代码的编译是在此阶段完成的
测试 test 使用适当的单元测试框架(junit)运行测试
包装 package 将项目打包为 JAR/WAR/EAR 包,以方便部署
检查 verify 对集成测试的结果进行检查,以保证质量达标
安装 install 安装打包的项目到本地仓库,以供其他项目使用
部署 deploy 拷贝项目包到远程仓库中,以共享给其他开发人员

site 生命周期的常用阶段

  • site:生成项目的站点文档(或者是项目报表)
  • site-deploy:将文档部署到指定 web 服务器上

maven 仓库概念

maven 中的任何一个依赖、插件、项目都是从仓库中获取的,maven 中有 3 种类型的仓库:

  • 本地仓库(local)
  • 中央仓库(central)
  • 远程仓库(remote)

maven 在获取任何一个依赖、插件、项目时都会按照上述顺序依次查找,如果都没有找到则报错。

本地仓库
在 windows、linux 中,本地仓库的位置位于 $HOME/.m2/respository/ 目录,如果不存在这个目录,那么 maven 第一次运行时会自动创建它。当 maven 需要获取任何一个构件时,它都将首先从本地仓库中搜寻,如果没有找到,则再去中央仓库中搜寻,如果中央仓库也没有,则会去远程仓库中搜寻,如果还没有那么就只能爆出错误了;如果在某个步骤中搜寻到了,那么 maven 会将这些构件保存到本地仓库中,这样以后再获取这些构件时就不用去别的地方找了,因为他们直接就在本地仓库中被找到了,这也是问什么第一次运行 maven 命令时很慢,而之后再次运行同样的命令就很快的原因了。

中央仓库
maven 中央仓库是由 maven 社区提供的,中央仓库中基本涵盖了目前所有流行的开源构件,一般来说项目中所依赖到的构件都可以在中央仓库中获取到。但因为中央仓库位于网络,所以如果想要获取中央仓库上的构件,你首先得确保你的主机能够连上互联网。而且因为 maven 中央仓库在国外,所以国内访问时难免很慢,如果希望获取构件时可以快一点,可以考虑配置阿里云 maven 镜像之类的。

远程仓库
如果我们还配置了 maven 远程仓库(一般在公司内部),那么当 maven 无法从中央仓库中获取构件时,会尝试从远程仓库中获取,远程仓库一般是公司内部维护的,相比中央仓库,速度更快,而且好管理,并且不需要将相关的构件开源,因为中央仓库上的构件都是要开源的,而公司内部的话,不需要开源,就是自己用而已。

注意,上面的描述有一部分是错误的,maven 中其实只有本地仓库和远程仓库两种。本地仓库就是家目录下的那个文件夹,而远程仓库默认是 maven 的中心仓库,如果我们想提高从远程仓库中获取 maven 构件的速度,可以修改 maven 的远程仓库地址,比如改为阿里云的镜像,这样 maven 在无法从本地仓库中获取构件时会从阿里云镜像仓库中获取而不是 maven 的默认中央仓库。除了这种国内镜像的玩法,我们还有一种玩法,那就是将 maven 的远程仓库改为本地局域网(公司内部),然后这个局域网的仓库(人们喜欢将它称为私服)内部是这么一个工作方式,当 maven 无法从本地仓库获取构件时,会请求远程仓库,即私服,而私服默认安装好里面也是没有任何构件的,所以私服会请求 maven 的中央仓库,然后在缓存到私服,私服在返回给 maven 客户端。同时,我们可以在 maven 私服中上传我们公司内部使用的 maven 构件,然后 maven 客户端就可以从私服中获取公司内部使用的 maven 构件,而不必将 maven 构件开源,上传到 maven 中央仓库。

私服的好处有两个:一是能够缓存中央仓库的 maven 构件,这样能够减少网络请求的负担;二是能够上传公司内部使用的专属 maven 构件,方便管理和使用。

maven 插件机制

maven 中有两个重要的概念:phase(阶段)、goal(目标)。phase 是生命周期中的概念,goal 是插件中的概念。phase 是抽象的,goal 是具体的。phase 实际上不会做任何事,它仅仅起到一个接口的作用,而实际上的操作都是由插件的目标来完成的,goal 实现了 phase 对应的接口,我们调用 phase 时,其实就是调用 phase 对应的 goal(s)。

生命周期是阶段的集合,插件是目标的集合,生命周期是抽象概念,插件是具体概念。maven 实际上是一个依赖于插件的执行框架,插件是一等公民,没有了插件,maven 什么都不是,因为他做不了任何事,所有的工作其实都是由插件完成的。

maven 提供了以下两种类型的插件:

  1. build plugin:项目构建相关的插件,在 <build> 标签中定义。
  2. report plugin:项目报告相关的插件,在 <reporting> 标签中定义。

maven 原型详解

maven 使用原型(archetype)插件来创建项目,注意是插件哦,因为 maven 里面万物基于插件。原型是什么东西呢?你可以将 archetype 理解为 template 模板,就像 html 模板一样,如果我们要开发一个网站,一般我们会先去网上找的适合的 html 模板,然后在模板的基础上进行修改,最后就做成了我们的网站了。

在 maven 中也是一样的,使用 maven 创建项目一般也是通过 archetype 来创建的,archetype 就是一个模板,archetype 基本上都是 java 的最佳实践,是有良好设计的模板,通过使用 archetype,大家可以很容易理解一个项目,因为这是最佳实践,而且大家的项目结构都是一样的,便于沟通和交流。

archetype 是一个插件,它是 maven 官方提供的,通过运行 mvn archetype:generate 命令,mvn 会列出目前所有的模板,截止 2018-10-24 日,已经有 2263 个 archetypes 了,通过这些模板,我们能够很快的构建一个项目的骨架,然后就能够专心的进行 Java 开发,最后使用 mvn 的各种阶段,进行项目的管理。

maven 官方提供的几个原型

  • maven-archetype-webapp:Java Web 项目(Java Web)
  • maven-archetype-quickstart:Java 普通项目(Java SE)

我们先来看看 quickstart 原型:

我们来看看 pom.xml 的内容:

其实我们现在用不到 test,所以我们来精简一下:

然后我们再来看下(我删掉了 src/test 目录,因为用不到):

细心的同学可能发现了,mvn 在执行 package 时总是会提示一个字符编码的警告,实际上我们可以通过 pom.xml 文件的一个配置来解决它:

然后,我们再来看下:

没有烦人的警告信息了,真爽(强迫症福音啊)。

但是,我们还没有彻底的干掉 test 步骤,修改 pom.xml:

再来看一下:

好吧,虽然还是有 test 的一些东西,但是最起码比之前好一点,对 maven 理解更加深入后再说吧。

java web 原型
现在我们来创建 java web 的原型,进行 java web 的开发:

同样的,我们先来精简一下 pom.xml 文件的内容:

然后,重新 clean、package 看下:

maven 属性相关

pom.xml 中的 <properties> 表示用于定义 pom 的属性,这个属性和 Java 中的 property 是一个意思,你可以将 property 理解为传递给 pom 的参数。在 <properties> 标签中定义的属性可以在 pom.xml 的其他地方进行引用,语法为 ${property-name},其实 properties 属性就是 mvn 命令行选项 -Dname=value 传递的属性,它们是同一个“属性”,都可以在 pom.xml 中引用它,语法一样。很容易知道,mvn 命令行指定的属性的优先级比 pom.xml 文件中的 <properties> 标签内指定的属性的优先级更高,所以如果它们之间有相同的属性设置,那么实际生效的是命令行中指定的那个属性值。

我们来通过一个简单的例子,了解 properties 属性的用法(将它理解为变量也许好一点):

我们创建了一个 antrun 任务,并将它与 clean 生命周期的 clean 阶段绑定了起来,所以当我们调用 clean 阶段时,ant 任务将会被执行,ant 任务的内容很简单,就是输出 ${output.string} 属性的值,我们在 <properties> 元素下面定义了 out.string 属性,当然我们也可以通过 mvn 命令行选项 -D 来定义。OK,我们来演示一下:

看到了区别吗?arg 传递的 property 优先级比 pom.xml 中硬编码的 property 优先级更高,所以当我们定义了 arg 形式的 output.string 属性时,mvn 将会应用它。现在我们来将 pom.xml 中的 <properties> 标签注释掉,来看一下执行结果:

注意,第一次运行时,output.string 属性并没有被定义任何值,但是 maven 也并没有报错,只是原样的输出 [echo] ${output.string},所以我们可以知道,当 pom.xml 中引用的属性没有被定义时,maven 会原样的把 ${property-name} 表达式输出,而不会报错。第二次执行时,我们传递了这个属性给 mvn,所以正常输出。

maven 依赖管理

依赖管理是 maven 的一大特色,当我们的项目使用 maven 管理时,我们无需关心如何获取项目的依赖,我们只需简单的在 pom.xml 中告诉 maven 我需要什么依赖(maven 坐标,即 groupId、artifactId、version),然后 maven 就会依次从 [本地仓库]、[中央仓库]、[远程仓库] 中获取对应的依赖,每当 maven 从中央仓库、远程仓库中获取到依赖后,它都会首先将依赖保存到本地仓库中(~/.m2/repository),当我们 maven 下次再次尝试获取依赖时,首先搜寻本地仓库,发现已经有了,而且坐标是一致的,所以就拿来直接用了,而不用通过网络去其他地方获取依赖。

通过这种集中管理依赖的机制,我们可以同一台主机的多个项目之间共享依赖项目,因为他们都是从本地仓库中获取的,这样可以便于管理,也能够节省存储空间。最原始的获取依赖的方式是通过 baidu、google,然后去官网下载对应的依赖包,然后保存到本地的某个路径,最后还要配置 classpath 变量,才能使用。

我们来通过 archetype 原型来创建一个 javase 项目,它会依赖到 ognl-v2.7.3.jar,如何找到 maven 中央仓库中的依赖包名称呢(maven 坐标),很简单,maven 官方提供了一个搜索页面,很好用:https://search.maven.org/,我们只需要打开这个页面,键入需要获取的依赖的名称就行,然后找到对应的依赖项,单击一下,右边就会出现 maven 的 pom.xml 配置,直接拷贝过去就行了,真心方便。

javase 的项目原型我们之前已经创建好了,目录为 ~/maven-workspace/javase,我们 cd 进去,然后配置 pom.xml,将 ognl 的依赖信息填上去,最终的 pom.xml 文件内容如下:

这里说明一下 <exec.mainClass> 属性,因为 maven 下载到的 ognl.jar 依赖包是放到本地仓库中的,所以当我们将项目打包之后,执行 java -cp target/*.jar com.zfl9.Main 时,会提示找不到 Ognl 的类。因为我们没有将本地仓库中的 jar 包添加到 CLASSPATH 环境变量中,所以就出现了这个问题。

那么 exec.mainClass 属性和这个问题有什么联系呢?出现这个问题的原因是因为我们没有将依赖到的 jar 包加入到 CLASSPATH 变量,其实 maven 早就想到了这个问题,所以我们可以使用 maven 的 exec 插件来执行我们的 jar 包,具体的用法是在 package 之后,调用 mvn exec:java -Dexec.mainClass=com.zfl9.Main,因为 Main 入口类一般是比较固定的,不太会改动,所以我就把这个属性放到了 pom.xml 中,这样我就只需要执行 mvn clean package exec:java 就能一键运行了。是不是很方便?!那么 exec 插件还有什么参数呢?比如如果我想给 Main 传递参数怎么办?很简单,即 mvn exec:java -Dexec.workingdir=/tmp -Dexec.args="-X myproject:dist",也就是通过 exec.args 属性来传递参数,很简单吧。

当然除了这种官方的优雅解决方式外,我们还可以通过另一种不是那么优雅的方式,具体怎么做我就不详细说了,因为这不是最佳方式,只能说是一种 hack 行为而已,参考链接:maven 执行 jar 提示没找到对应的类

抛开这个,我们其实完全可以去掉这个 exec.mainClass 属性,因为我们可以在 jar 包的 META-INF 中的 MANIFEST.MF 文件中配置 Main-Class 属性,这样我们就能直接运行 jar 包了,语法为:java -jar /path/to/file.jar,很是方便。

那我们该如何配置 maven,让它在执行 package 打包的时候自动添加 Main-Class 配置行呢?稍微动动脑子想想就知道,我们应该对 maven-jar-plugin 这个插件的 jar 目标进行配置,怎么配置呢?如下。实际上不行,在执行到 exec:java 目标时,exec 插件会提示 MainClass 未定义,然后就退出了。这也许是 exec 插件的设计问题,但是我们可以通过配置 exec:java 目标,来告诉他要执行的 Main-Class(不过这样好像还不如直接用上面的方法,直接定义 mainClass 属性,然后 exec:java 目标会引用他),不管了,方法如下:

但其实吧,这样反而是自找麻烦,我觉得还不如用回最开始的方案,定义 exec.mainClass 属性算了,即:

然后,我们来测试一下:

我们来试试如何传递参数给 Main 类吧,先定义一个 Main 类:

然后是修改后的 pom.xml 文件(因为我已经不需要 ognl 依赖包了):

我们先不传递任何参数试试,看看 Main 类是否正常运行:

提示 missing arguments,没问题,那么我们传递参数试试:

没问题,正常显示,那么如果我们想传递转移序列呢?在之前我们可以使用 $'' 来转移,那么现在呢?当然也可以啊,来我们试试:

看到了区别没?没加 $'' 时,shell 并不会对字符串中的转移序列进行转义,而是原样输出,当我们添加了 $'' 之后,就正常解析了。

OK,现在我们还需要介绍另一个知识点:依赖的作用范围(pom.xml 中的 <scope> 标签)。

  • compile:编译范围,默认值。在 编译测试运行 3 种环境都有效。
  • test:测试范围,仅在 测试 环境中有效。
  • provided:提供范围,在 编译测试 环境中有效。如 servlet-api,运行时容器由提供。
  • runtime:运行范围,测试运行 环境中有效。如 JDBC 驱动,只要能在运行前提供就行。
  • system:系统范围,项目所依赖的 jar 包不在 maven 本地仓库中,而是本机其他位置。

一般来说,不建议使用 system 依赖范围,因为则会造成项目的不可移植。因为 system 依赖范围相当于你静态的指定了 jar 包的绝对路径,这就是硬编码了,绝对会造成项目的不可移植性。所以应尽量避免。

那么就剩下 compile、test、provided、runtime 四种了,test 很好理解,就是测试环境中会用到,就是在编译测试代码和运行测试代码时这种依赖才会被用到,其他时候不会被用到。那么 compile 范围也很好理解,则是默认值,也符合我们对依赖的一般定义,它就是从编译到运行期间都需要用到的依赖包,比如 ognl.jar 就是这种,编译的时候需要用到,运行时也需要用到(测试期间其实就是包括编译期、运行期)。而 provided 就是这个依赖包在运行时不需要,运行期间,对应的依赖会由容器提供(比如 java 应用服务器会提供 servlet-api,所以 servlet-api 就是 provided 范围),但是编译和测试期间(其实是测试的编译阶段)需要用到 servlet-api,不然 javac 编译不了啊。而 runtime 依赖范围和他有点相似(应该说相反),runtime 表示项目的对应依赖在编译期间(当然包括测试的编译期间)不需要用到,但是在实际运行的时候需要用到,比如 JDBC 的驱动实现,编译的时候我们只需要知道 JDBC-API 就行,则是 jdk 提供的,不需要额外的依赖,但是实际运行时是需要环境提供 JDBC 驱动的,不然无法运行。

maven 插件配置

关于 maven 的插件,我们先来介绍几个基本知识。

我们都知道 Maven 本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由 maven-compiler-plugin 插件完成的。进一步说,每个任务(这里所说的任务就是阶段,phase)对应了一个插件目标(goal),每个插件会有一个或者多个目标,例如 maven-compiler-plugin 的 compile 目标用来编译位于 src/main/java/ 目录下的主源码,testCompile 目标用来编译位于 src/test/java/ 目录下的测试源码。

用户可以通过两种方式调用 Maven 插件目标。第一种方式是将插件目标与生命周期的阶段(lifecycle phase)绑定,这样用户在命令行只是输入生命周期阶段而已,例如 Maven 默认将 maven-compiler-plugin 的 compile 目标与 default 生命周期的 compile 阶段绑定,因此命令 mvn compile 实际上是先定位到 compile 这一生命周期阶段,然后再根据绑定关系调用 maven-compiler-plugin 的 compile 目标。第二种方式是直接在命令行指定要执行的插件目标,例如 mvn archetype:generate 就表示调用 maven-archetype-plugin 插件的 generate 目标,这种带冒号的调用方式与生命周期无关(因此也不会触发 maven 各个阶段的依赖机制)。

认识上述 Maven 插件的基本概念能帮助你理解 Maven 的工作机制,不过要想更高效率地使用 Maven,了解一些常用的插件还是很有必要的,这可以帮助你避免一不小心重新发明轮子。多年来 Maven 社区积累了大量的经验,并随之形成了一个成熟的插件生态圈。Maven 官方有两个插件列表,第一个列表的 GroupId 为 org.apache.maven.plugins,这里的插件最为成熟(官方维护的插件),具体地址为:http://maven.apache.org/plugins/index.html。第二个列表的 GroupId 为 org.codehaus.mojo,这里的插件没有那么核心,但也有不少十分有用,其地址为:http://mojo.codehaus.org/plugins.html

插件前缀 prefix
在前面的 maven 学习中,你经常会看到诸如 mvn <plugin-name>:<goal-name> 的使用方式,实际上这里的 plugin-name 应该改为 plugin-prefix,即插件的前缀,比如 maven-compiler-plugin 插件的前缀是 compiler,所以我们可以直接使用 mvn compiler:compile 来编译程序主代码,调用 mvn compiler:testCompile 来编译程序的测试代码。看到没,我们并没有写出 plugin 的坐标(基本坐标,三元组),但是 maven 好像知道我们哪个插件,这里面是有运作的机制呢?

其实就是插件的前缀在起作用,maven 为了简化使用,奉行“约定优于配置”原则,对于官方插件,groupId 的命名是有规则的,即 maven-${prefix}-plugin,对应的默认 prefix 就是 ${prefix},所以我们可以知道,maven-compiler-plugin 插件的前缀就是 compiler;而对于第三方插件(org.codehaus.mojo),groupId 的命名是这样的:${prefix}-maven-plugin,对应的默认 prefix 就是 ${prefix},所以 exec-maven-plugin 插件的前缀就是 exec(这个插件熟悉吧,基本是我们使用的第一个第三方插件,也是最常用的一个),这种约定优于配置的原则是的我们很容易的使用 maven 插件。

那你有没有想过,为什么 maven 要提出 plugin-prefix 这种概念呢?其实很简单,如果没有插件前缀,那么我们在 maven 命令行中如果想要调用某个插件的目标,必须将插件的坐标写出来,然后才能调用,比如不使用前缀调用 compiler:compile 目标,需要这么写:mvn org.apache.maven.plugins:maven-compiler-plugin:compile,注意我们省略的 version 坐标。这样虽然能让 maven 很好的工作,但是却不是很好用,因为命令行太长的程序会让人产生负面情绪。这就是插件前缀的作用。

当然,除了使用 maven 的自动插件前缀映射机制外(约定优于配置),maven 也允许我们自己定义插件的前缀字符串,这个内容不属于入门知识,就不介绍了。默认的映射机制已经很好用了, 我觉得没有必要自找麻烦。

插件前缀还有一个好处就是,当我们在 maven 中调用一个不在本地仓库中的插件前缀时,maven 会自动去远程仓库中获取,而 maven 是如何知道我们使用的插件前缀的对应插件坐标的呢?当然是前面介绍的两个映射规则了。比如我们可以在 maven 命令行中这样调用:mvn eclipse:help 来获取 maven-eclipse-plugin 插件的帮助信息。

配置 pom.xml 中的插件
插件的配置比较简单,这是最常见的 pom.xml 插件的配置:

插件元素中的基本信息也是 maven 的坐标(三元组在 pom.xml 配置中经常出现,所以请牢记它们:groupId、artifactId、version),然后再常见点的就是 <configuration> 标签、<executions> 标签。configuration 很显然是用来配置插件的,而 executions 标签则是配置插件执行相关的(目标与阶段的绑定,等等)。注意,configuration 标签可以在 executions 标签外部,也可以在 executions 标签内部,前者为全局配置,后者为局部配置(针对不同的 goal),显然,局部配置的优先级要高于全局配置,否则局部配置还有意义么。比如上面的 antrun 的 configuration 元素就可以位于 executions 元素内部,如果同时存在,那么局部配置的优先级高。

这里告诉你一个小技巧,如果你不知道当前的 plugin 的版本号(不填会报错,但是要填的话又不知道具体版本号是多少),可以使用 mvn 命令行执行 mvn <plugin-prefix>:help 来查看对应插件的帮助信息,里面一般都有版本号的,比如 mvn antrun:help 就能看到 antrun 插件的版本号。

然后还有一个最佳实践就是,指定依赖和插件的版本号建议不要直接指定,而是将版本号放到 properties 元素内部,然后在需要用到的地方使用 ${prop-name} 来引用,这样是为了好后期维护,而不需要全文搜索替换版本号,这样太麻烦了,而且容易出错。

maven 学习总结

典型的 pom.xml

maven 常用命令 [阶段]

  • mvn clean:执行项目清理工作
  • mvn validate:检查项目信息是否正常
  • mvn compile:编译项目源代码和测试代码
  • mvn test:执行项目的测试(junit 单元测试)
  • mvn package:打包项目为 jar/war/ear 压缩包
  • mvn verify:对测试结果进行检查,确保满足质量要求
  • mvn install:将项目打包,然后安装到 maven 本地仓库
  • mvn deploy:将项目打包,然后部署到 maven 远程仓库共享
  • mvn site:生成项目的相关站点文档(一般领导喜欢看这个东西)
  • mvn site-deploy:将生成的相关站点文档部署到指定 WEB 服务器

maven 常用命令 [其他]

  • mvn <plugin-prefix>:help:查看指定插件的简要帮助
  • mvn <plugin-prefix>:help -Ddetail:插件指定插件的详细帮助
  • mvn help:describe -Dplugin=<plugin-prefix>:查看指定插件的简要帮助
  • mvn help:describe -Dplugin=<plugin-prefix> -Ddetail:查看指定插件的详细帮助

maven 常用命令 [运行]

  • mvn exec:java -Dexec.mainClass=com.zfl9.Main:运行项目(指定 Main 类)
  • mvn exec:java -Dexec.mainClass=com.zfl9.Main -Dexec.args=args...:运行项目(带参数)

maven 常用命令 [帮助]

  • mvn help:system:查看系统的环境变量和 java 属性
  • mvn help:effective-pom:查看当前项目的有效 pom.xml
  • mvn help:effective-settings:查看当前 maven 的有效设置

maven 阿里云镜像加速
修改 $MAVEN_HOME/conf/settings.xml,在 mirrors 元素中添加:

maven 强制 jdk1.8 版本
修改 $MAVEN_HOME/conf/settings.xml,在 profiles 元素中添加:

maven webapp 项目配置

maven 补充知识

maven 的 archetype 补充说明
maven 的 archetype 是项目的模板(就是一个目录结构,里面可以有一些附带的文件),常用的两个模板就是 quickstart 和 webapp,前者是普通的 java 项目,后者则是 java web 项目,它们的主要区别就是 quickstart 的 packaging 为 jar(默认就是 jar),而 webapp 的 packaging 则为 war,且 webapp 的 src/main 目录下还存在一个 webapp 目录,这个目录就是用来存放 java web 项目文件的地方,如 index.jsp、WEB-INF/web.xml 等。

需要强调的是,maven 的 archetype 仅仅是一个项目模板,一个目录结构以及一些默认的文件,我们完全可以自己手动创建项目目录,创建 pom.xml,创建 src/main/java、src/main/resources、src/main/webapp 等目录,并且决定当前项目是否为 web 项目的根本仅仅是 packaging 的类型不同,如果为 jar 就是普通项目(默认),如果为 war 那么就是 web 项目,maven 会读取 src/main/webapp 目录下面的 web 资源文件,项目的类型与它所使用的什么 archetype 没有一点关系,总之你记住一点,archetype 就是项目模板,仅此而已。

maven 的 java 版本及文件编码
在 pom.xml 中设置两个 property 属性,即可使用 java 8 来构建项目,而不是默认的 java 6:

在 pom.xml 中设置两个 property 属性,即可告诉 maven,始终使用 UTF-8 作为文件的字符编码:

为了方便,我觉得将这 4 个属性放到 maven 的 settings.xml 全局配置文件中比较方便,毕竟这些很少变动:

构建 java web 项目时,如果没有 web.xml 文件,mvn package 会报错
要解决其实很简单,直接在 pom.xml 中添加一个 property 属性即可,如下:

maven 聚合与继承
所谓聚合就相当于 idea 的 project 有多个 module 一样,好处是便于管理一个大项目的多个子模块,如只需要在父模块中执行一次 mvn package 就可以编译全部子模块的代码并打包,而不用分别进入每个子模块再去执行 mvn package 打包命令。我们知道,随着用户需求的不断增加,软件系统也变得越来越复杂,所以现在稍微大一点的项目都会采取模块化的方式来进行构建和管理,使用模块化方式可以消除很多重复代码,并且也能够让项目结构更加清晰,不容易出错,而 maven 自然也提供了模块化的支持。

在说 maven 的聚合和继承之前,先来了解一下 idea 的 project 和 module 的概念以及应用。idea 是 java 开发人员非常熟悉的一个 IDE,相比 eclipse 也更加智能,更有科技感;eclipse 中有 workspace 和 project 两个概念,一个 workspace 可以有多个 project;而在 ieda 中,同样有这些概念,只不过名称不同罢了,idea 的 project 其实就是 eclipse 的 workspace,而 idea 的 module 则是 eclipse 中的 project;idea 的一个 project 就是一个独立的 windows 窗口,一个 project 中可以有多个 module,它们都位于同一个 windows 窗口中,便于集中化管理,也方便查看。所以熟练使用 idea 的 project 和 module 也非常重要。

而 maven 其实也能够实现类似的功能,即将一个大的项目模块化处理,这其中就涉及到 maven 的聚合和继承。通常的一个建议是,同一个项目的不同模块,应该有相同的 groupId、version,并且它们的 artifactId 也应该有相同的前缀,并建议使用连字符隔开,比如大名鼎鼎的 springframework:

我们也应该遵循这种命名规范,同一个项目的不同子模块,它们的 groupId 要相同,version 要相同,artifactId 前缀要相同。并且建议将子模块放到项目目录中,也就是父子结构,这样别人一看到你的项目就知道进行了模块化处理,不建议使用平行目录结构,这纯属是自找麻烦。

OK,我们以一个简单的例子,来介绍 maven 的继承和聚合;假设现在有一个项目:myapp,然后有 3 个子模块,myapp-api、myapp-web、myapp-mail,则创建这样一个目录结构,并创建好 pom.xml 文件:

很容易看得出,myapp 目录就是用来存储所有子模块的项目目录,其中只有一个 pom.xml 项目对象模型文件,我们可以在里面配置一些共用的 properties、dependencies、dependencyManagement、plugins、pluginManagement 元素,然后就可以直接在子模块中使用这些配置信息(继承),进行统一的管理(聚合),比如可以让所有子模块都使用相同版本的依赖、插件,而不用担心因为版本不一致出现问题;同时,如果要编译、测试、打包、安装、发布整个项目,也不需要分别进入子模块的目录,执行 mvn 命令,而是直接进入 webapp 目录,执行 mvn 命令就可以进行统一的处理(聚合),非常方便。完整的目录结构如下(注意顶级目录下只需要一个 pom.xml,其它什么 src、target 目录不需要):

myapp => pom.xml

注意这里使用的是 dependencyManagement 而不是 dependencies,它们的区别是这样的:

  • dependencies:子模块会自动引入该元素中定义的依赖,无法去除,所以除非确定每个子模块都需要使用这些依赖,否则不建议放到这里。
  • dependencyManagement:子模块不会自动引入该元素中定义的依赖,要在子模块中使用这些依赖,只需提供 groupId 和 artifactId 即可。

同理,plugins 和 pluginManagement 也是一样的道理,父模块的 plugins 元素中定义的插件,会被所有子模块自动使用,无法去除,而父模块的 pluginManagement 元素中定义的插件,不会被子模块自动引入,除非显式的引入这些插件(也只需要提供 plugin 的 groupId 和 artifactId,其它信息会自动从父模块中继承过来);一个好的做法是,将必选的依赖和插件放到父模块的 dependencies 和 plugins 元素中,而其它的可选依赖则放到父模块的 dependencyManagement 和 pluginManagement 元素中,然后在子模块中引入它们各自所需的可选依赖和插件就行了。

myapp-api => pom.xml

myapp-web => pom.xml

myapp-mail => pom.xml

然后我们简单的创建一个源码类和测试类,以 myapp-api 模块为例:
MyAppApi.java

MyAppApiTest.java

其它的子模块中的源码类和测试类都是差不多的,仅仅为了演示而已;然后我们可以进入 webapp 父目录,执行 mvn 命令,这时候你会发现所有的子模块都被自动管理了,如批量编译、批量测试、批量打包等等,来看一个批量测试的例子:

当然,我们依旧可以进入各自的子模块目录中,执行 mvn 命令,这样只会操作当前的子模块,和平时没啥两样。

只使用继承功能
如果只是为了提取公共的依赖、插件,方便统一管理不同模块的依赖版本、插件版本,那么其实不需要使用 maven 的聚合功能,也就是说,不需要在父模块的 pom.xml 中定义 modules 元素,这样配置之后,在子模块中依旧可以通过 parent 声明来使用父模块中的依赖配置、插件配置、公共属性。这就是只使用 maven 的继承功能,而没有使用 maven 的聚合功能。这种情况下,在父模块中执行 mvn 命令不会起到批量管理的作用,因为没有聚合。

只使用聚合功能
当然我们也可以只使用 maven 的聚合功能(也就是批量管理项目,批量编译、批量测试、批量打包等),改造起来也特别简单,只需要在每个子模块的 pom.xml 中去除 parent 声明,然后定义好各自的 groupId、version、依赖版本等就可以了。这样可以做的仅仅就是进入 webapp 目录,然后执行 mvn 命令来批量处理了。而各自的子模块中并不会继承父模块中的什么东西,因为 parent 元素都没了。