Maven 笔记

平平无奇的 Java 的包管理工具。

这是《Maven实战》的读书笔记。

电子书:https://4lib.org/s/B009WMAZX4

书本实例代码:https://github.com/juven/mvn_in_action_code

概念

构件:在 Maven 世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。

坐标:每一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径。

依赖:一个 Maven 项目使用另一个 Maven 项目的构件,作为依赖。使用坐标来定位。

仓库:在某个位置储存所有 Maven 项目共享的组件。当需要的时候,Maven 会根据坐标找到仓库中的构件,使用它们。

命令

1
2
3
mvn archetype:generate
mvn dependency:list
mvn dependency:tree

坐标和依赖

依赖范围

1
2
3
4
5
6
7
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</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

依赖调解

依赖调解的原则:

  1. 路径最近者优先。依赖的路径比较短的包会被解析使用。
  2. 第一声明者优先。如果路径长度一样,在POM文件中,顺序靠前的那个依赖优胜。

仓库

graph TD
    Maven仓库 --- 本地仓库
    Maven仓库 --- 远程仓库
    远程仓库 --- 中央仓库
    远程仓库 --- 私服
    远程仓库 --- 其他公共库

仓库解析的机制

  1. 依赖范围是 system 时,直接从本地解析
  2. 计算仓库路径后,如果本地仓库存在则解析
  3. 本地不存在,依赖版本是发布版本构件,遍历远程仓库,下载到本地并解析
  4. 如果依赖版本是 RELEASE 或 LATEST,获取远程仓库元数据,计算出真实版本值,重复 2 和 3。
  5. 如果依赖版本是 SNAPSHOT,获取远程仓库元数据,得到最新版本,检查,根据需要下载。
  6. 如果版本是时间戳格式的快照,会恢复成非时间戳格式,再去找。
  7. 版本不明晰的时候,如 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 available
  • compile - compile the source code of the project
  • test - test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed
  • package - 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 met
  • install - install the package into the local repository, for use as a dependency in other projects locally
  • deploy - 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-aggregator</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Aggregator</name>
<modules>
<module>account-email</module>
<module>account-persist</module>
<module>account-parent</module>
</modules>
</project>

模块不一定要是树形嵌套的结构的,也可以平行放置,这样 module 标签要改成相对路径。

这样运行 mvn clean install 命令就会同时构建两个工程。

继承

如果多个模块都依赖了一堆相同的东西,那么可以建立一种父子结构,让子模块继承父模块。

作为父模块,打包类型需要为 pom

1
2
3
4
5
6
7
8
9
<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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
</project>

子模块需要继承它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<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
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath>
</parent>
<artifactId>account-email</artifactId>
<name>Account Email</name>
<dependencies>
……
</dependencies>
<build>
<plugins>
……
</plugins>
</build>
</project>

relativePath 似乎可以不需要。

子模块没有配置 groupId 和 version 的话,会继承父类的配置。也可以显式声明。

最后要把父模块也添加到聚合模块里面。

所有可以被继承的POM元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
groupId:项目组ID,项目坐标的核心元素。
version:项目版本,项目坐标的核心元素。
description:项目的描述信息。
organization:项目的组织信息。
inceptionYear:项目的创始年份。
url:项目的URL地址。
developers:项目的开发者信息。
contributors:项目的贡献者信息。
distributionManagement:项目的部署配置。
issueManagement:项目的缺陷跟踪系统信息。
ciManagement:项目的持续集成系统信息。
scm:项目的版本控制系统信息。
mailingLists:项目的邮件列表信息。
properties:自定义的Maven属性。
dependencies:项目的依赖配置。
dependencyManagement:项目的依赖管理配置。
repositories:项目的仓库配置。
build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等。
reporting:包括项目的报告输出目录配置、报告插件配置等。

依赖管理

使用 dependencyManagement 元素的依赖,不会给子模块引入依赖。子模块需要引入依赖的时候,需要提供 groupId 和 artifactId,该依赖的还是要写,不过不需要提供版本号。

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
</dependencies>

scope import

maven中import scope依赖方式解决单继承问题的理解 - 花花牛 - 博客园

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</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 就会报错。

用户可以裁剪反应堆,也就是可以选择只构建反应堆中的一些个模块,加速构建。