Maven 工具
十一月的第一周,来学习和整理 Maven。
用维基的话讲,Maven 是一个项目管理以及自动构建的工具(初听还是有些一头雾水的)。我开始工作之后,几乎天天和 Maven 打交道,但是仿佛又对它很生疏,特用一周的时间来学习与整理相关知识。
Maven 是 Apache 公司所创造的项目管理工具。项目管理这个词,在计算机编程方面没那么容易解释,你可以理解为编程中除了写代码那部分之外的工作,例如编译、打包、引入依赖资源等等这类打杂的事情。
在 Maven 出现之前,Ant 是 Java 世界的主流项目管理工具,所有人在介绍 Maven 的时候,总是会先讲几句 Ant 如何拧巴、如何不优雅,然后才话归正题说起 Maven。我并没有用过 Ant,我猜测很多像我一样的萌新也是直接上手 Maven,而从没使用过 Ant 的,我们这类人每次看 Maven 的教程都会一脸懵逼:“Ant 是什么?我们这说 Maven 呢你提它干嘛?”
看了许久,大概能猜到 Maven 和经常被鞭尸的 Ant 的区别:Ant 是一个很传统的项目管理工具,想实现什么,自己写去,麻烦也得自己写;Maven 是一个【约定大于配置】的项目管理工具,帮你写好了大部分东西、配好了大部分操作,你稍微一改就能用了(前提是你要遵守约定)。
在 Maven 的官网中,有一篇名为《Maven 的哲学》的文章,里面粗略介绍了 Maven 的设计哲学。其中有这么一句话:
Maven is about the application of patterns in order to achieve an infrastructure which displays the characteristics of visibility, reusability, maintainability, and comprehensibility.
我认为这句话里最重要的词是“patterns”,这个词直译是模式、范例。我觉得呢,pattern 是一个很有哲学思辨性的词,印象中它最早出现应该是在柏拉图的理念论里面,柏拉图认为有两个世界,一个是理想世界,一个是现实世界,现实世界中的一切都以理想世界中的“范型”(pattern)为模板创造出来。
不要有抵制心理,这是个很简单的概念,举个例子:世界上有那么多人,每个人都各有不同,但大家都属于同一类事物:人。按照柏拉图的理解,一定有另一个世界,那里有人这个模板,我们这个世界上的所有人,都是从另一个世界上的那个模板拷贝而来的。如果用编程术语来描述的话,这就是对象与类之间的关系,一个是抽象化的,一个是实例化的。
Maven 自述哲学思想的这段话,我理解的意思是,Maven 设置了一个公用模板,如果你使用 Maven 来管理项目,你就可以使用那个默认的模板,稍微改一点东西就可以了。这是一种和 Spring Boot 一样的配置思路,即约定大于配置,我们约定好了,你就别自己配置了。这样做有两种好处,一种是减少开发时间,一种是标准化、规范化,以方便大家交流。
但我是觉得吧,Maven 所指的“patterns”,说的不光是约定大于配置这件事,它当然包括这个,但是不只是。因为 Maven 本身不光是一个解决项目配置问题的工具,它做的事情是项目管理,管理这个概念就广泛了很多,包括依赖、编译、部署等等,当然,其中也包括配置。我理解的 Maven,它在配置方面采用约定大于配置的思路,但是在其他地方,也是本着【模块化】、【范型化】、【标准化】这一类的思路来处理的。
这个慢慢体会。
对我而言,学习 Maven 首先遇到的问题是,我分不清楚那一堆项目管理的功能,究竟是 Maven 的,还是 IDE 的(此处应该配一个扶额焦虑的表情)。在我写这篇博文之前,很多 Maven 的设计与功能,我都以为是 IDEA 帮我做的,毕竟我每一次构建项目,都是打开 IDEA 软件,使用 Maven 来构建工程的,而且一切过程都是全程默认下一步做的,搞得我现在都分不清这两个工具的功能边界了……
用 Maven 构建项目,首先注意到的,是文件的目录结构。多建几次就会发现,每次新建出来的项目(project),或者是模块(module),文件的目录索引都是一样的,基本都长下面这个样子:
这种目录结构是 Maven 构建出来的,统一化的文件索引方式,好处之一在于,各开发者在开发时,存放文件的位置几乎没有差别(而且符合直觉),能够维持团队高效运作;好处之二在于,固定的文件存放路径,能够让第三方工具 Maven 帮助我们管理项目,而不是我们自己做繁复的操作。
这种目录结构,最重要的是三部分:src
、target
、pom.xml
。他们分别对应着:源代码、编译文件、配置文件。稍详细一点的信息看下面:
1 | ├── src src即source,代表源文件目录 |
我觉得这部分没什么好关注的,看几次就记得很牢固了,这种目录结构还是很符合直觉的。
上文说到,重要的有三部分:src
、target
、pom.xml
,这前两个着实没什么可介绍的,一个放源码,一个放编译文件,但第三个 pom.xml
(Maven 的配置文件),还是要单独学习的。
POM 的全程是 Project Object Model,项目对象模型。Maven 是一个项目管理工具,它要面对的是项目,它面对项目的管理方式是 POM,把项目当做对象一样管理。这种面向对象的处理方式,跟 Java 是一个路数的,只不过 Java 的编码语言是 Java,Maven 的编码语言是 xml。
初看 Maven 的配置文件(pom.xml),xml 这种语言在字符数量上实在是令人畏惧,这一大堆的代码让人一头雾水,感觉要配置的东西很繁琐,很混乱。
以下是一段配置得较为简单,但还是能够运行的 pom.xml 文件代码:
1 | <project |
我在四处找 Maven 教程和总结贴的时候,看到了一篇博文(但是我后来找不到了,抱歉不能链接来源了),他提供了一种新的认知思路:既然 POM 就是将项目视作对象,以面向对象的方式进行管理,那么我们可以把 xml 的代码转换成 Java 代码来理解。上面的那段 xml 代码,如果是用 java 代码来看待的话,应该是这样子的:
1 | public class Project { |
这样就清晰很多,便于初次看到 xml 代码来理解了。
下面分类型,挑选一部分经常要使用的配置项来总结一下。
pom.xml 文件能够配置的地方特别多,在此全部总结出来不现实,以后遇到了现查就可以了。参考这篇《POM 标签大全详解》去查阅更多配置解释(在网页内容的下半部分)。
项目基本信息
以这段代码为例:
1 | <project |
从头文件说起,这部分不是 Maven 的 pom.xml 文件所专有的,而是一切 xml 文件都要配置的。
xmlns
全称是 xml name space,即 xml 命名空间。
命名空间的意思是,有两个 xml 文件都使用了同一个名字,比如 A.xml 文件是一个人员文件,里面用 name 这个名字表示人名,与此同时 B.xml 文件是一个公司文件,里面用 name 这个名字表示公司名。当两个文件同时执行时,如果不指定 xml 文件的命名空间,那么就会造成混乱。
通常情况下,如果要指定命名空间,应该是
xmlns:a
、xmlns:b
之类的写法,表示 a 的命名空间、b 的命名空间是什么,这里什么都不写,是表示默认的命名空间。xmlns="http://maven.apache.org/POM/4.0.0"
这一行的右边是一个字符串(更准确的讲,是一个 url 地址字符串),这个字符串表示该命名空间的唯一标识符。通常情况下,这个字符串会是一个 url 地址,点开之后能够看到详细的对于该命名空间的说明。但是在这里,如果你尝试的话,这个 url 并打不开,这个后面再说。xmlns:xsi
根据上面对于 xmlns 的解释,这里应该就是声明 xsi 命名空间了。
xsi 的全称是 xml schema instance,这个命名空间并不是偶然出现的,它已经成为了一种业界规范,是表示 xml 文档结构( XSD,xml scheme definition)的命名空间。xml 文档结构,就是整个 xml 文件需要有哪些内容,这些内容的格式、默认值等等是什么,我们所写的 pom.xml 文件,都是基于它的 xml 文档结构来写的。由于文档结构是非常重要的东西,因此为它单独指定一个命名空间。
这次你点开后面的网址,就能看见对于 xsi 这个命名空间的简单说明了。
xsi:schemaLocation
这个的意思是:xsi 的 schemaLocation 是什么,翻译之后的意思是:xml 文档结构的具体内容应该会存放在某个文件里,那么这个文件在哪里?
你去观察这行代码,发现在
xsi:schemaLocation
之后有两个 url 地址,而且第一个 url 地址,居然就是 xmlns 默认命名空间的指定地址。实际上,这两个 url 地址是以 key-value 的形式出现的,key 是该 xml 默认命名空间的值(那当然也就是 xmlns 所指定的那个 url 地址),value 是该 xml 文档结构文档的位置。如果你打开后面那个 url 地址,你会真的看到一个文件,而且你单纯从格式上就能判断出来,这就是该 xml 文件的文档结构的具体内容。
示例代码中,中间一共有六个配置项,逐一介绍:
modelVersion
Maven 工程的模型版本,就目前而言只有一种参数可能:
4.0.0
。但是即使如此,这一项也必须显式地配置出来,因为未来 Maven 可能会有更多的版本,例如
4.0.1
、5.0.0
等等。到那时,如果遇到了没有配置模型版本的 xml 文件,将无法向前兼容。
groupId
公司或组织名称,是指本项目的归属人是谁。
artifactId
项目名称,例如一个公司有10个项目,各个项目相互区别就是通过这个 ID。
groupId 和 artifactId 两个 ID 一起构成了唯一索引,也就是说,一个 groupId 加上一个 artifactId,在这个世界上只能出现一次(除非是同一个项目的不同版本)。
version
项目当前版本,格式为:主版本.次版本.增量版本-限定版本号。
限定版本号有两种:SNAPSHOT(快照)和 RELEASE(发布)。前者表示不稳定版本,可能会经常发生变化,后者是相对稳定的版本,版本不会频繁变动。
与 groupId 和 artifactId 联合构成 GAV,这三个值加在一起,可以精准地指向【一个特定版本的项目】。当别的项目要引入依赖,把别的项目加载进来时,必须指定这三个值。
packaging
项目打包之后的类型,有很多种,例如 jar、war、pom 等等,默认使用 jar(也就是说如果你不写这一行也可以,默认是
<packaging>jar</packaging>
)。pom 类型是父 pom.xml 文件所使用的,比如有一个公共的 pom.xml 文件,其中配置了很多共性的配置,有四个子模块的 pom.xml 文件都可以直接引用该 pom.xml 文件,少配置一些内容。这个公共的 pom.xml 文件,就是父 pom 文件,它的构建类型就是 pom(即
<packaging>pom</packaging>
)。name
项目的名称,Maven 生成文档时用的。
还有很多很多很多可以配置的地方,这里只介绍了一点点(但是足够日常使用了)。
项目依赖
以这段代码为例:
1 | <project |
这段示例 xml 代码共引入了两个外部依赖,分别是 junit(测试单元)和 joda-time(一个很好用的第三方时间类)。
dependencies
在 dependencies 内可以存放多个 dependency,表示项目所引入的所有依赖。Maven 将自动地去仓库里寻找依赖包,从本地开始找,没有就去仓库找,再没有就去远程仓库找,每次在仓库里找到了都会下载到本地。如果依赖之间有版本上的冲突,先根据依赖深度判决,深度相同再根据先来后到的原则判决。
dependency
一个依赖包,其中包括 groupId、artifactId、version 等标签。
groupId、artifactId、version
GAV 三参数,依赖包所必需的标签,通过这三个标签才能找到一个特定版本的依赖包。
scope
依赖作用的范围,例如值为 test 时表示,只会在测试时引用该依赖,正式的版本不会依赖。
共有种,分别是:compile(全程,默认配置)、provided(类似于全程,但打包期可以被替代)、test(测试)、runtime(除了编译的全程阶段)、system(类似于全程,但依赖从本地文件抓取)。
其他
在父 pom 文件中,还可以配置 dependencyManagement,用法上和 dependencies 类似,可以作为集中化的依赖配置中心,可以配置依赖的版本号、作用域等等,子 pom 文件如果声明了一个 dependency,并且只配置了 groupId、artifactId 这两个参数,其他参数就会从父 pom 文件的 dependencyManagement 里找。(当然,如果自己填了其他参数,会以子配置为准)
继承、聚合
1 | <parent> |
继承和聚合通常是对应存在的,父 pom 文件会标明聚合(modules)内容表示它的作用模块有哪些,子 pom 文件会标明继承(parent)内容,表示它继承自哪个 pom 文件。
parent
parent 表示继承, 子 pom 可以使用
parent
指定父 pom。parent 内需要明确写明的标签,依然是 GAV 三标签。
modules
modules 表示聚合,Java 的文件结构中,一个项目可以有多个模块,modules 内即是模块。
聚合内只需要放模块的名字即可。
其他配置内容,例如 build、profile、plugin 等,用到再说吧。
接下来看 Maven 的生命周期。
看了好多篇教程和博文,它们对 Maven 的生命周期的解释与描述,总让我觉得是相互冲突的,甚至在同一篇文章中都是冲突的。之后渐渐发现,原来 Maven 的生命周期并不只有一个,而是有三个,分别是 clean、default(or build)、site。也就是说“Maven 的生命周期”这种说法是不正确的,Maven 本身并没有生命周期,而应该是 Maven 的 clean 生命周期
、default 生命周期
、site 生命周期
。再换句话说,“生命周期”这四个字,描述的不是 Maven,而是那三个词。
生命周期这个说法,在编程中是一个非常常见的概念。我觉得 Maven 在这里也使用生命周期这个词,是为了表达【阶段性】的概念。我以人为例,人的生命周期是 幼年 -> 青年 -> 中年 -> 老年,那么当人值中年时,必定已经经历了幼年和青年。Maven 的三个生命周期,各自都有一些阶段,你可以不执行完全部的生命周期,可以只执行到中间的某个阶段,但是当你执行到这个阶段时,你一定已经执行完,在这之前的所有阶段了。
以下的内容主要参考来源为:《Maven 构建生命周期》。
clean 生命周期
clean 生命周期包含三个阶段:
- pre-clean:执行一些需要在 clean 之前完成的工作
- clean: 移除所有上一次构建生成的文件
- post-clean:执行一些需要在 clean 之后立刻完成的工作
这三个阶段,如果使用命令行执行的话,那么是这样子的:
1 | mvn pre-clean |
再提一遍,下文不再提了。当执行 mvn clean
语句时,表示执行 pre-clean 阶段和 clean 阶段。当执行 mvn post-clean
语句时,表示执行 pre-clean 阶段、clean 阶段和 post-clean 阶段。总之,之前的阶段,全部执行。
具体的执行过程、原理,不叙。
default 生命周期
你光看 default 这个词就能看出来,这是 Maven 最重要的那个生命周期。这是 Maven 的构建生命周期,项目的编译、测试、打包、部署等等,都属于这个生命周期的范围。
default 生命周期共有 23 个阶段,其中重要的阶段是:
- validate:验证项目是否正确且所有必须信息是可用的
- compile:编译代码
- test:运行测试(例如 JUnit 单元)
- package:打包,创建 jar 包、war 包等等
- verify:对测试结果进行检查
- install:把打包的内容安装到本地仓库中
- deploy:把打包的内容部署到远程仓库中
site 生命周期
site 生命周期用于创建报告文档、部署站点等等,包括以下四个阶段:
- pre-site:执行一些需要在生成站点文档之前完成的工作
- site:生成项目的站点文档
- post-site: 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备
- site-deploy:将生成的站点文档部署到特定的服务器上
本来还想写写 Maven 的插件,哎懒了,反正大概就是在执行生命周期的某个阶段,打开 IDE 一般都会默认集成一些 Maven 的插件,可以不用自己写指令了。
这篇就写到这里叭。