Maven 笔记
平平无奇的 Java 的包管理工具。
这是《Maven实战》的读书笔记。
电子书:https://4lib.org/s/B009WMAZX4
书本实例代码:https://github.com/juven/mvn_in_action_code
概念
构件:在 Maven 世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。
坐标:每一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径。
依赖:一个 Maven 项目使用另一个 Maven 项目的构件,作为依赖。使用坐标来定位。
仓库:在某个位置储存所有 Maven 项目共享的组件。当需要的时候,Maven 会根据坐标找到仓库中的构件,使用它们。
命令
1 | mvn archetype:generate |
坐标和依赖
依赖范围
1 | <dependency> |
Maven 在编译,测试,和运行的时候各使用一种 classpath。依赖范围就是在某个阶段是否需要这个包。
compile:编译依赖范围。默认依赖范围。对编译,测试,运行都会使用到。例如 spring-core。
test:测试依赖范围。只对于测试 classpath 有效。例如 JUnit。
provided:已提供依赖范围。对于编译和测试有效,在运行时无效。例如 servlet-api,编译和测试时需要用到该依赖,但是运行的时候容器已经提供,就不需要重复引入,再把依赖打进 jar 包。
runtime:运行时依赖范围。对测试和运行有效,但在编译时不需要。例如 JDBC 的驱动实现,在代码里面我们没有直接引用 MySQL 或者 Oracle 的 JDBC驱动,而是用 JDBC 接口去调用。所以编译的时候不需要,实际运行的时候才要。
system:系统依赖范围。范围和 provided 范围一致,但是需要使用 systemPath 指定依赖文件的路径,用来导入本级系统中的包,没有可移植性。
import:用于依赖管理,只在 dependencyManagement 标签中使用。作用是把另一个 POM 的 dependencyManagement 配置导入到当前 POM 的 dependencyManagement 里面。
Scope | 编译 | 测试 | 运行 |
---|---|---|---|
compile | ✔ | ✔ | ✔ |
test | ✔ | ||
provided | ✔ | ✔ | |
runtime | ✔ | ✔ | |
system | ✔ | ✔ |
依赖传递
左边一列是第一直接依赖(A),上面一行表示第二直接依赖(B),单元格表示传递性依赖范围(C)。
A 依赖 B,B 依赖 C,那么 A 对 C 是什么依赖范围。
可选依赖不会被传递。即添加了标签为
<optional>true</optional>
的。
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | - | - | runtime |
test | test | - | - | test |
provided | provided | - | provided | provided |
runtime | runtime | - | - | runtime |
依赖调解
依赖调解的原则:
- 路径最近者优先。依赖的路径比较短的包会被解析使用。
- 第一声明者优先。如果路径长度一样,在POM文件中,顺序靠前的那个依赖优胜。
仓库
1 | graph TD |
仓库解析的机制
- 依赖范围是 system 时,直接从本地解析
- 计算仓库路径后,如果本地仓库存在则解析
- 本地不存在,依赖版本是发布版本构件,遍历远程仓库,下载到本地并解析
- 如果依赖版本是 RELEASE 或 LATEST,获取远程仓库元数据,计算出真实版本值,重复 2 和 3。
- 如果依赖版本是 SNAPSHOT,获取远程仓库元数据,得到最新版本,检查,根据需要下载。
- 如果版本是时间戳格式的快照,会恢复成非时间戳格式,再去找。
- 版本不明晰的时候,如 RELEASE、LATEST 和 SNAPSHOT,会根据远程仓库的更新策略来检查更新,和仓库看配置有关。相关的配置标签:releases、snapshots 的 enabled,和 updatePolicy 等。
镜像
<mirrorOf>*</mirrorOf>
:匹配所有远程仓库。
<mirrorOf>external:*</mirrorOf>
:匹配所有远程仓库,使用
localhost 的除外,使用 file://
协议的除外。也就是说,匹配所有不在本机上的远程仓库。
<mirrorOf>repo1,repo2</mirrorOf>
:匹配仓库
repo1 和 repo2,使用逗号分隔多个远程仓库。
<mirrorOf>*,!repo1</mirrorOf>
:匹配所有远程仓库,repo1
除外,使用感叹号将仓库从匹配中排除。
需要注意的是,由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven 仍将无法访问被镜像仓库,因而将无法下载构件。
生命周期
Maven的生命周期就是为了对所有的构建过程进行抽象和统一。Maven从大量项目和构建工具中学习和反思,然后总结了一套高度完善的、易扩展的生命周期。这个生命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。也就是说,几乎所有项目的构建,都能映射到这样一个生命周期上。
Maven 的生命周期是抽象的,实现由插件来完成。有默认插件,也可以绑定其它插件。
Maven 拥有三套独立的生命周期:clean,default,site。
clean
- pre-clean
- clean
- post-clean
default
太多了,完整的在 Lifecycle Reference。
部分重要的
validate
- validate the project is correct and all necessary information is availablecompile
- compile the source code of the projecttest
- test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployedpackage
- take the compiled code and package it in its distributable format, such as a JAR.verify
- run any checks on results of integration tests to ensure quality criteria are metinstall
- install the package into the local repository, for use as a dependency in other projects locallydeploy
- done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.
site
- pre-site
- site
- post-site
- site-deploy
插件
Maven 生命周期和插件相互绑定。
可以自定义将某个插件目标绑定到生命周期的某个阶段上。
可以通过命令行参数 -D 对插件进行配置。例如跳过测试:
1 | mvn install -Dmaven.test.skip=true |
聚合和继承
聚合
如果想要一次构建两个项目,而不是到两个模块的目录下面分别执行 mvn 命令,我们可以再新建一个 pom 文件。
1 |
|
模块不一定要是树形嵌套的结构的,也可以平行放置,这样 module 标签要改成相对路径。
这样运行 mvn clean install
命令就会同时构建两个工程。
继承
如果多个模块都依赖了一堆相同的东西,那么可以建立一种父子结构,让子模块继承父模块。
作为父模块,打包类型需要为 pom
。
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
子模块需要继承它。
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
relativePath 似乎可以不需要。
子模块没有配置 groupId 和 version 的话,会继承父类的配置。也可以显式声明。
最后要把父模块也添加到聚合模块里面。
所有可以被继承的POM元素
1 | groupId:项目组ID,项目坐标的核心元素。 |
依赖管理
使用 dependencyManagement
元素的依赖,不会给子模块引入依赖。子模块需要引入依赖的时候,需要提供
groupId 和 artifactId,该依赖的还是要写,不过不需要提供版本号。
1 | <dependencies> |
scope import
1 | <dependencyManagement> |
使用 import 依赖,可以把另一个 POM 中的 dependencyManagement 配置合并到 当前 POM 的 dependencyManagement 元素中。想要重复使用一批 dependencyManagement 配置,除了可以复制配置或者继承之外,还可以通过这种 import 范围依赖将这一配置导入进来。
如果有多个项目,它们使用的依赖版本都是一致的,则就可以定义一个使用 dependencyManagement 专门管理依赖的POM,然后在各个项目中导入这些依赖管理配置。
这样,用聚合代替继承,就可以解决 POM 文件只能单继承的问题,也可以更好地按功能划分 POM 文件。
插件管理
pluginManagement
也是类似的原理。
聚合和继承的关系
对于聚合模块来说,它知道有哪些被聚合的模块,但那些被聚合的模块不知道这个聚合模块的存在。
对于继承关系的父 POM 来说,它不知道有哪些子模块继承于它,但那些子模块都必须知道自己的父 POM 是什么。
一个 POM 可以既是聚合 POM,又是父 POM,没有什么问题。
超级POM
任何一个 Maven 项目都隐式地继承自该 POM。这个超级 POM 约定了中央仓库、项目结构、插件版本等等配置。这些配置成了 Maven 所提倡的约定,可以在 Maven 的依赖包里面找到。
反应堆
在一个多模块的Maven项目中,反应堆(Reactor)是指所有模块组成的一个构建结构。对于单模块的项目,反应堆就是该模块本身,但对于多模块项目来说,反应堆就包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序。
Maven 会根据模块之间的依赖关系,决定构建模块的顺序。如果一个模块依赖于另一个模块,就先构建那个模块。这么递归下去,直到找到没有依赖的模块。
模块之间的依赖关系会构成一个有向无环图(DAG),所以说不能出现环,要是出现了循环依赖,Maven 就会报错。
用户可以裁剪反应堆,也就是可以选择只构建反应堆中的一些个模块,加速构建。