一文从 14 个方向分析 Maven 的技术点,在创建 Java 项目或者使用开源的项目代码的过程中不再错误理解 Maven 的概念,不再对于 pom.xml 感到无从下手,正确理解 Maven 在项目的生命周期中扮演的角色。另外,笔者增加整理的图示,希望在本 Chat 中读者能够快速有效的理解 Maven,并且让它不再成为创建项目和使用项目的障碍。
适合人群:对使用 Maven 构建项目有兴趣的技术人员。
在正文中,我们会向读者展示不同程度的问题,因为通过问题驱动学习,一直是一个不错的学习方法, 只有在问题出现的时候,带着问题去寻找答案,得到的知识点才更有切合实际的应用场景。
问题一
在打开 IDEA 或者 Eclipse 的时候会看到下图:

看到其中的 lifecycle,以及 lifecycle 下的 clean、validate、compile、test、package、verify、install、site、deploy,可能看着比较熟悉,但又似懂非懂。看起来熟悉是因为从字面意思上的生命周期,亦或是清理、验证、编译、测试等环节是程序开发过程中,不论哪一种语言都或多或少要经历的环节,不太明确的原因是,不能够知道这几个环节在 Java 的项目开发过程中,或者在 Eclipse 或者 IDEA 的项目构建过程中究竟是如何使用,起到什么作用?
问题二:Maven 安装后在系统用户的路径下的 repository 没有 settings.xml 文件怎么办?
问题三:Maven 的 repository 仓库下载的包全部下载到 C 盘,占用空间,如何进行修改?
问题四:在默认的配置下,使用 Maven 下载插件的速度特别慢如何解决?
问题五:本地的 jar 包应该如何使用 mvn 命令安装到本地仓库?
问题六:如何配置环境变量?
问题七:classpath 是什么?
下面带着这些问题去寻找答案。
同样像之前的文章一样,既然追根溯源,那就找到它的真身,而不是去查找经过好几手的信息。笔者会在不同的文章中一直提到这个方法。

按照上图所示,我们对于概念的理解,一般从四个方面切入,官网、维基百科、百度百科、以及其他可靠的平台寻找贴近本质的概念的描述,从宏观正确把握不太了解的技术点。
在 Apache Maven Project 的官方网站中解释如下(摘自:https://maven.apache.org/):
Apache Maven is a software project management and comprehension tool. Based on the concept of a project object model (POM), Maven can manage a project’s build, reporting and documentation from a central piece of information.
在中文维基百科中的解释如下(摘自:https://wiki.hk.wjbk.site/zh-cn/Apache_Maven)
Apache Maven,是一个软件(特别是 Java 软件)项目管理及自动构建工具,由 Apache 软件基金会所提供。基于项目对象模型(缩写:POM)概念,Maven 利用一个中央信息片段能管理一个项目的构建、报告和文档等步骤。
在百度百科中的解释如下(摘自:https://baike.baidu.com/item/Maven/6094909?fr=aladdin)
Maven 项目对象模型(POM),可以通过一小段描述信息来管理项目的构建,报告和文档的项目管理工具软件。
在 bunoob.com 中的解释如下(摘自:https://www.runoob.com/maven/maven-tutorial.html):
Maven 翻译为“专家”、“内行”,是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven 利用一个中央信息片段能管理一个项目的构建、报告和文档等步骤。 Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。 Maven 也可被用于构建和管理各种项目,例如 C#、Ruby、Scala 和其他语言编写的项目。Maven 曾是 Jakarta 项目的子项目,现为由 Apache 软件基金会主持的独立 Apache 项目。
从上面四方面的解释,想必你一定能够对 Maven 有了一个清晰的宏观认识。能获取到如下几点:
对于上述的几种表示,可能有的地方对于 a central piece of information 的解释为“中央信息片”,或者“一小段信息描述”令人感到费解,究竟什么是“中央信息片”,什么叫“一小段信息描述”。没有明确的细化表示,可以理解为 Maven 中一些信息的重要核心的部分,比如,生命周期中的一些环节,pom 中的一些配置,或者命令行中的一些 mvn 的操作,等等。
简单来说,应该就是一些用来管理编译、清理、报告等操作的信息或者是配置。

Maven 解决的问题就是在项目构建的过程中,消除重复,不再一圈一圈,一轮一轮的重复配置、重复编译、重复测试等等。
那么它是如何解决的呢?将过去依赖于人工进行的繁琐的 XML 配置,抽象成了生命周期,在项目构建的过程中重复的工作,交给约束的规范,交给相应的插件来完成自动化的构建过程。
有经验的程序员都知道,在项目开发的过程中, 会因为一个配置节而导致需要几分钟甚至是几天的检查,会因为一次又一次产品部门提出的修改任务,不停地编译,不停地测试,不停地部署,甚至有些情况下跳过测试导致生产环境出现重大错误的情况也层出不穷,导致程序员无法从复杂的流程中抽身,浪费大量的时间在维护流程上。
因此,可以说,Maven 的出现给程序员尤其是 Java 程序员带来了一道曙光。通过借助 Maven 管理分散的项目信息,自动生成站点,利用插件获得项目文档、测试报告等等各种各样有价值的信息。
使用 Maven 最高效的方式永远是命令行,IDE 在自动化构建方面有天生的缺陷。这一点可能在项目进行过程中会有体会,比如清理不及时,版本有冲突,或者波浪线莫名其妙消失不了等情况。
由于 Maven 仓库是通过简单文件系统透明地展示给 Maven 用户的,有些时候可以绕过 Maven 直接查看和修改仓库文件,在遇到疑难问题时,这往往十分有用。
尽量不要直接修改 mvn.bat 或者 mvn 这两个 Maven 执行脚本文件。如果修改了脚本文件,升级 Maven 就不得不再次修改,一来麻烦,二来容易忘记。同理,应该尽可能不修改任何 Maven 安装目录下的文件。

安装目录,如下图所示:

不同文件夹的含义:

另外的几个 NOTICE、LICENSE、README 是说明性质的文件,不再赘述。
注意:默认情况下 ~/.m2 目录下除了 repository 仓库之外就没有其他目录和文件了,不过大多数 Maven 用户需要复制 M2_HOME/conf/settings.xml 文件到 ~/.m2/settings.xml。这是一条最佳实现。(这个问题在开始部署 Maven 的过程中时常会遇到)
安装好 Maven,配置完对应的环境变量后,可以很快找到对应的 Maven 下的 conf 文件夹下的 settings.xml。如下:
#查看mvn是否安装成功,以及对应版本
mvn -v
#查看mvn对应的路径
where mvn

1. 代理的配置,settings.xml 中:
<settings>
<proxies>
<!--多个proxy的时候,默认第一个被激活的生效-->
<proxy>
<id></id>
<!--激活该代理-->
<active>true</active>
<!--使用的代理协议-->
<protocol>http</protocol>
<!--指定代理的主机和端口,认证的用户名和密码-->
<host>211.***.***.***</host>
<port>3128</port>
<username>****</username>
<password>****</password>
<!--不需要代理的主机名-->
<nonProxyHosts>*.google.com</nonProxyHosts>
</proxy>
</proxies>
</settings>
2. 默认本地仓库的路径:

<!--注释放开,将localRepository节点中对应的text内容改为自己想要存放的本地路径,通过这里的设置避免本地仓库随着构件的下载,占用C盘空间越来越多影响系统运行-->
<localRepository>D:/repo</localRepository>
1. 判断是否已经安装了,window->preferences:


2. 安装
方法一:
help-->install->m2e - https://www.eclipse.org/m2e/



方法二:
help->eclipse marketplace

IDE 往往会集成比较新版本的 Maven,比较新版本的 Maven 存在不稳定因素,因此,应该在 IDE 中配置 Maven 插件时使用与命令行一致的 Maven。

使用 where mvn 查看 mvn 的对应路径
window->preferences->maven->installations

这两通过设置,Eclipse 的 IDE 就与命令行用的 Maven 是同一个 Maven 了。构建过程中就不会因为版本或者配置的问题导致不一致的问题。

(图片来自:http://maven.apache.org/)
Maven 的核心是 pom.xml(Project Object Model,项目对象模型),它定义了项目的基本信息,用于描述项目如何构建、声明项目依赖,等等。
<!--引用Maven实战-->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dev.mvnpractice</groupId>
<artifactId>hellworld</artifactId>
<version>0.0.1-SNAPSHOT</version>
</project>
每个节点的含义如下:
明确以后就不会在每次对于 groupId 和 artifactId 的选择再发愁了,把 groupId 视为项目外部,对项目进行标识的 ID、artifactId 被视为在项目内部,对于不同业务的项目名称命名,例如:

当然这只是一种理解方式,为了便于命名,也有建议如下这种用法的,假设是百度百科下的一个历史方向的产品:
No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK?

使用上面的方法修改 Eclipse 里的配置后对于命令行里的执行没有变化,最后在命令行里输入 mvn -v 查看如下内容发现:

对应的运行时依然是 D:\software\jre8。


重新打开 cmd 发现对应的运行时改成了 JDK,注意这里要重新打开 cmd,否则无法使用新配置的环境变量:

再次执行 mvn clean compiler:

默认情况下,Maven 构建的所有输出都在 target / 目录中。
Maven 项目中默认的主代码目录是 src/main/java,对应地,Maven 项目中默认的测试代码目录是 src/test/java。
pom.xml 添加 JUnit 的依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>


测试用例编译成功,得出结果。
POM 没有指定打包类型,使用默认打包类型 jar。
执行命令:
mvn clean package

Maven 会在打包之前执行编译、测试等操作。
jar 任务负责打包,实际上就是 jar 插件的 jar 目标将项目主代码打包成一个名为 hello-world-0.0.1-SNAPSHOT.jar 的文件。
该文件也位于 target/ 输出目录中,它是根据 artifact-version.jar 规则进行命名的,如有需要,还可以使用 finalName 来自定义该文件的名称,这里暂且不展开。
打包完成,得到了项目的输出,如果有需要的话,就可以复制这个 jar 文件到其他项目的 classpath 中从而使用 HelloWorld 类。但是如何才能让其他的 Maven 项目直接引用这个 jar 呢?还需要一个安装的步骤:
mvn clean install

安装 jar 包到 Maven 的 repository 对应的路径中。安装到本地仓库后,其他 Maven 项目才能使用它。
上面大概描述了几个主要的命令:
mvn clean compile
mvn clean test
mvn clean package
mvn clean install
在项目的根目录中放置 POM.xml,在 src/main/java 目录中放置项目的主代码,在 src/test/java 中放置项目的测试代码,这些基本的目录结构和 pom.xml 文件内容称为项目的骨架。
重复的构建这个骨架一样会导致效率的低下,只要重复的工作都会有提醒人优化的那一天,为了节约时间创造更多的价值。
Maven 提供了 archetype 以帮助我们快速勾勒出项目骨架。
Maven 3
mvn archetype:generate
Maven 2
mvn.org.apache.maven.plugins:maven-archetype-plugin:2.0-alpha-5:generate
很多资料会让你直接使用更为简单的 mvn archetype:generate 命令,但在 Maven 2 中这是不安全的,因为该命令没有指定 archetype 插件的版本,于是 Maven 会自动下载最新的版本,进而可能得到不稳定的 snapshot 版本,导致运行失败。
然而在 Maven 3 中,即使用户没有指定版本,Maven 也只会解析最新的稳定版本,因此这是安全的。
实际上是在运行插件 maven-archetype-plugin,注意冒号的分割,其格式为 groupId:artifactId:version:goal。
输入命令后,下载依赖文件,并提供 archetype 的选择:

选择其中一个骨架回车后会提示你输入必要的信息:

基本概念了解后,对于使用 Eclipse 或者 IDEA 之类的 IDE 去创建 Maven 项目或者是导入 Maven 项目的思路都会更加清晰和明确。
在 maven.apache.org 中笔者没有发现与 coordinate 相关的术语,但是发现了 Jason van Zyl(Apache Maven 项目的创始人)对于 Maven 历史的介绍。

(图片来自:http://maven.apache.org/)

(图片来自 http://maven.apache.org/)
然后坐标,也就是“coordinate”,这个词出现在了 Jaon van Zyl 作为作者之一的 Maven The Definitive Guide 这本书中。
具体描述如下(摘自:Maven The Definitive Guide):
The Archetype plugin created a project with a file named pom.xml. This is the Project Object Model (POM), a declarative description of a project. When Maven executes a goal, each goal has access to the information defined in a project’s POM. When the jar:jar goal needs to create a JAR file, it looks to the POM to find out what the JAR file’s name is. When the compiler:compile task compiles Java source code into bytecode, it looks to the POM to see if there are any parameters for the compile goal.Goals execute in the context of a POM. Goals are actions we wish to take upon a project, and a project is defined by a POM. The POM names the project, provides a set of unique identifiers (coordinates) for a project, and defines the relationships between this project and others through dependencies, parents, and prerequisites. A POM can also customize plugin behavior and supply information about the community and developers involved in a project. Maven coordinates define a set of identifiers which can be used to uniquely identify a project, a dependency, or a plugin in a Maven POM. Take a look at the following POM.
从这段描述能看出来,Maven 坐标这一概念实际上是指 Maven 对于项目,依赖,插件身份定义的个唯一标识的规则。
Maven 坐标的元素包括 groupId、artifactId、version、packaging、classifier。
现在只要我们提供正确的坐标元素,Maven 就会从仓库中寻找相应的构建供我们使用。
也许你会奇怪:
Maven 是从哪里下载构建的呢?
答案其实很简单,Maven 内置了一个中央仓库的地址,该中央仓库包含了世界上大部分流行的开源项目构件,Maven 会在需要的时候去那里下载。
Maven 坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组 Maven 坐标是通过一些元素定义的,它们是 groupId、artifactId、version、packaging、classifier 等。
Maven 项目中所有可能的子元素列表的参考说明:

(图片来自 http://maven.apache.org/)
基础常见的几个如下:
关于依赖的元素:

其中,packaging 是可选的,classifier 是不能直接定义的???
前面对于基础的内容,包括 Eclipse 与 Maven 的集成,POM 中的不同基础节点的介绍,以及 mvn 常见命令的说明。接下来该到了 Maven 使用构件的时候了。

上图中的箭头所指就能看到 Maven 中自带的中央仓库的配置。
Maven 可以在某个位置统一存储所有 Maven 项目共享的构件,这个统一的位置就是仓库。

从图中可以看出来,仓库只分为两大类:本地仓库和远程仓库。在 Maven 根据坐标去仓库中寻找构件的时候,它会查看本地仓库,如果本地仓库存在此构件,直接使用;如果本地仓库不存在此构件,或者需要查看是否有更新的构件版本,就会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。
另外,为了实现重用,项目构建完毕后生成的构件也可以安装或者部署到仓库中,供其他项目使用。比如我们在工作中会遇到的生成的基于公司特有业务的封装的构件,但是又需要代码共享后其他的同事也能使用,就需要本地安装构件到 Maven 的仓库中。公司内部可能会因为带宽、成本、开发效率等等的原因选择搭建局域网内部的私服。
私服大致的流程如下图所示:

它是假设在局域网内的仓库服务,相对于远程仓库来说,私服承担的是代理的角色,衔接远程仓库和本地用户。当 Maven 用户下载构件的时候,它从私服请求,如果私服上不存在该构件,则从外部的远程仓库下载,缓存在私服上之后,再为 Maven 的下载请求提供服务。
对于一些无法从外部仓库下载到的构件也能够本地上传到私服上供大家使用。
在很多情况下,默认的中央仓库无法满足项目的需求,可能项目需要的构件存在于另外一个远程仓库中。这时可以在 pom 中配置该仓库。
<repositories>
<repository>
<releases>
<enabled/>
<updatePolicy/>
<checksumPolicy/>
</releases>
<snapshots>
<enabled/>
<updatePolicy/>
<checksumPolicy/>
</snapshots>
<id/>
<name/>
<url/>
<layout/>
</repository>
</repositories>
release 和 snapshots,用来控制 Maven 对于发布版本构件和快照版本构件的下载:
对于 releases 和 snapshots 来说,除了 enabled,它们还包含另外两个子元素 updatePolicy 和 checksumPolicy。
id:id 必须是唯一的,Maven 自带的中央仓库使用的 id 是 central,如果其他的仓库声明也使用该 id,就会覆盖中央仓库的配置。
name:友好的可读的仓库名称。
url:仓库的 url,格式为 protocol://hostname/path。
layout:查找或者存储构件仓库的布局类型,可以是 legacy 或者 default,默认值是 default。而 default 表示仓库的布局是 Maven 2 及 Maven 3 的默认布局,而不是 Maven1 的布局。
根据上述,Maven 会从仓库下载对应版本的构件。
另外有时候由于默认仓库下载较慢,需要在 settings.xml 中配置其他镜像,如下:

<!-- 阿里云镜像 -->
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<!-- https://maven.aliyun.com/repository/public/ -->
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
对于 settings.xml 的每一个节点的说明,同样可以去 maven.apache.org 上查找详细的解释。就拿上述的 mirrorOf 来说:

(图片来自 http://maven.apache.org/)
Maven 除了能对项目进行编译、测试、打包之外,还能将项目生成的构建部署到仓库中。首先,需要编辑项目的 pom.xml 文件。配置 distributionManagement 元素见代码:
<project>
<distributionManagement>
<repository>
<uniqueVersion/>
<releases>
<enabled/>
<updatePolicy/>
<checksumPolicy/>
</releases>
<snapshots>
<enabled/>
<updatePolicy/>
<checksumPolicy/>
</snapshots>
<id/>
<name/>
<url/>
<layout/>
</repository>
<snapshotRepository>
<uniqueVersion/>
<releases>
<enabled/>
<updatePolicy/>
<checksumPolicy/>
</releases>
<snapshots>
<enabled/>
<updatePolicy/>
<checksumPolicy/>
</snapshots>
<id/>
<name/>
<url/>
<layout/>
</snapshotRepository>
<site child.inherit.append.path=.. >
<id/>
<name/>
<url/>
</site>
<downloadUrl/>
<relocation>
<groupId/>
<artifactId/>
<version/>
<message/>
</relocation>
<status/>
</distributionManagement>
</project>
配置正确后,在命令行运行 mvn clean deploy,Maven 就会将项目构建输出的构件部署到配置对应的远程仓库,如果项目当前的版本是快照版本,则部署到快照版本仓库地址,否则就部署到发布版本仓库地址。
这里明确了 mvn clean deploy 对应的含义。也就是这里的 deploy 是对于构件的部署,而不是对于所开发的应用的部署,可能很多人会对于 IDE 中的 deploy 有误解。
另外就是需要对快照版本注意,它只应该在组织内部的项目或模块间依赖使用,由于快照版本的不稳定性这样的依赖会造成潜在的危险。即使项目构建今天是成功的,由于外部的快照版本依赖实际对应的构件随时可能变化,项目的构建就可能由于这些外部的不受控制的因素而失败。
1. https://repository.sonatype.org/

(图片来自 https://repository.sonatype.org)
2. https://mvnrepository.com/

(图片来自 https://mvnrepository.com/)

(图片来自 http://maven.apache.org)
Maven 的生命周期是抽象的,其实际行为都由插件来完成。
这一句话包含两层含义:
也就是说抽象的生命周期本身不做任何实际的工作,在编码过程中接口对于继承实现的规范,接口单独存在的时候它只是起到规范的作用,没有任何的实现方法。这同样是生命周期抽象的含义。
既然有抽象,那就需要有实现,只不过这里的实现交给插件完成的。
Maven 的生命周期不是一个整体,它有三种标准的生命周期,分别是:

(图片来自 http://maven.apache.org)
从命令行执行 Maven 任务的最主要的方式就是调用 Maven 的生命周期阶段。各个生命周期是相互独立的,而一个生命周期的阶段是有前后依赖关系的。

这里只是做了一个类比,实际上笔者引出的是 pom.xml 中的 Parent 的使用。
也就是,在 Maven 项目中是如何定义子模块的?如何在子模块中声明 Parent 的?如何通过这种方式实现模块的聚合,以及构件的集成的?
我们在项目开发的过程中,有经验的同学都知道要对模块进行分割,避免团队开发时因为过度耦合导致的业务问题和效率问题。最简单的比如,三层模式。
为了解决上述问题,就 Maven 而言需要了解如下的基本点。
父模块应用:

(图片来自:《Maven 权威指南中文版》)
用户可以通过一个打包方式为 pom 的 Maven 项目中声明任意数量的 module 元素来实现模块的聚合,每个 module 的值都是一个当前 pom 的相对目录。
子模块:

(图片来自:《Maven 权威指南中文版》)
上面的例子中,父模块聚合了两个 module,在这里父模块对于两个模块实现了聚合。也可以父模块中不包含 module,只是在依赖上或者插件上实现继承。
可继承的 pom 元素:
Maven 提供的 dependencyManagement 元素能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在 dependencyManagement 元素下的依赖声明不会引入实际的依赖,不过它能够约束 dependencies 下的依赖使用。
对于插件的管理也会受到父级模块的影响。如下图:

(图片来自:http://maven.apache.org)
当公司的项目发展到一定程度的情况下,持续集成的重要性会逐渐突出。原因是,项目越来越多,模块越来越多,维护的成本越来越高,测试越来越繁琐,部署人工干预越来越多,导致提供的生产环境的服务越来越不稳定。
相信很多团队都希望能够做到:
编译、测试、审查、打包、部署一整套流程完美实现。并且在此之后对于测试,发布部署的结果能够有任务调度,监控产品,并得到报告,根据需求反馈给承担不同角色的用户。
增加整个环节的自动触发,减少人工干预。
那么,这个工作能实现吗?答案当然是肯定的,那么我们需要自己从头写一套工作流吗?答案是否定的。把这些交给持续集成产品(Jenkins)就好。
Jenkins 为 Maven 提供了很好的支持,并且精通 Maven 的项目结构以及依赖关系。具体的使用方法可以参考 Jenkins 的相关资料。

一般来说只需要选择 internal,然后再选择一个 archetype,最后单击 next 按钮:



构件 Web 应用过程中应该注意的测试问题,手动的 Web 页面测试是必不可少的,但这种方法不是万能的。在工作过程中常常不论修改了什么地方的代码(尤其是 Web 应用的程序员),都会习惯性地打开浏览器测试整个应用,这往往是没有必要的。单元测试可以覆盖的代码就不一应该依赖于 Web 页面测试。
传统的 Web 测试方法要求我们编译、测试、打包及部署,这往往会消耗数 10 秒至数分钟的时间。
常用项目报告插件:
Maven 插件可以在学习过程中尝试编写,可以尝试创建自己的项目骨架 archetype。