Maven 简介
Maven 想必大家都很熟悉,下面看一下Maven官方的相关介绍。
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.
官网的介绍为:Apache Maven 是一个软件项目管理和理解工具。基于项目对象模型(POM)的概念,Maven 可以从一个“中心信息”管理项目的构建、报告和文档。
这里提到一个关键词 “项目对象模型”(Project Object Model,POM),那什么是 POM 呢?
It is an XML representation of a Maven project held in a file named
pom.xml
. When in the presence of Maven folks, speaking of a project is speaking in the philosophical sense, beyond a mere collection of files containing code.A project contains configuration files, as well as the developers involved and the roles they play, the defect tracking system, the organization and licenses, the URL of where the project lives, the project's dependencies, and all of the other little pieces that come into play to give code life.
简单翻译一下,它其实是一个 pom.xml
文件,是 Maven 项目的 XML 表示形式,一个 Maven 项目 并不是指简单的代码文件的集合,而是包含配置文件(configuration files)、开发人员及其角色(developers)、缺陷跟踪系统(defect tracking system)、组织和许可证(organization and licenses)、项目所在的 URL、项目的依赖项(dependencies),以及其他所有为代码赋予生命的元素。
其实工作中,对 POM 文件的使用是最多的就是配置项目依赖项,POM 作为一种模型它其实和 DOM(Document Object Model,文档对象模型)差不多,都是模型化思想,一个是将项目作为对象,一个是将文档作为对象。DOM 将文档看作对象,就可以方便地修改文档的结构、内容和样式,POM 将项目看作对象,就可以更好地进行项目的配置与建立项目之间的依赖关系。
总而言之,Maven 最主要的功能有两个:构建项目,管理项目的依赖关系。
下载安装
下载、安装、配置环境变量,没啥好说的,这里我用的JDK 是 22 版本,Maven 是 3.9.8 版本。
C:\Users\CangoKing>java --version
java 22.0.2 2024-07-16
Java(TM) SE Runtime Environment (build 22.0.2+9-70)
Java HotSpot(TM) 64-Bit Server VM (build 22.0.2+9-70, mixed mode, sharing)
C:\Users\CangoKing>mvn --version
Apache Maven 3.9.8 (36645f6c9b5079805ea5009217e36f2cffd34256)
Maven home: D:\ProgrammingSoft\Maven\apache-maven-3.9.8
Java version: 22.0.2, vendor: Oracle Corporation, runtime: D:\ProgrammingSoft\Java\jdk22
Default locale: zh_CN, platform encoding: UTF-8
不知不觉,JDK竟然已经更新这么多版本了,但工作还在用 Java8, o(╥﹏╥)o ,有机会写几篇介绍Java新特性的文章,敬请期待。
目录结果如下:
apache-maven-3.9.8
├─bin // mvn运行的脚本
├─boot // 启动类加载器框架,用于加载 Maven 自己的类库
├─conf // 配置目录
├─lib // 该目录包含了所有 Maven 运行时需要的Java类库
├─LICENSE
├─NOTICE
└─README.txt
很常规。
配置文件
Maven 有两个配置文件,一个是全局级别的,一个是用户级别的:
▪ 用户级别:路径为 ${user.home}/.m2/settings.xml
▪ 全局级别:路径为 ${maven.home}/conf/settings.xml
用户级别的配置文件更高,我一般将其删除,只保留软件安装目录下的配置文件,方便管理。
配置文件中的内容大都被注释掉了,为了更好的使用Maven,这里配置两个项:
⨳ 本地仓库
<localRepository>D:\ProgrammingSoft\Maven\repository</localRepository>
⨳ 远程仓库
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
这里使用的是阿里云镜像,默认的的Maven远程仓库下载会非常慢...
当开发者构建项目时,Maven会从远程仓库下载所需的依赖,并将其缓存到本地仓库中。这样,在后续构建过程中,Maven就可以直接从本地仓库中获取依赖,而无需再次从远程仓库下载,加快了构建速度并减少了对网络的依赖。
IDEA 创建 Maven 项目
⨳ 新建Maven项目
⨳ 填写项目坐标
通过 groupId
、artifactId
和 version
这三个元素的组合,可以唯一确定一个项目的坐标,确保项目在 Maven 仓库中的唯一性:
▪ groupId
(组ID):定义了项目属于的组织或机构,通常使用反转的域名方式来命名。
▪ artifactId
(项目ID):定义了项目的名称,是项目在组织内的唯一标识。通常,artifactId
是项目的名称,用来唯一标识一个具体的项目。
▪ version
(版本):指定了项目的版本号,用来区分不同版本的项目。版本号遵循一定的命名规则,通常包含主版本号、次版本号和修订版本号。
无论 Jar 包在本地仓库还是远程仓库都,这三个元素组成的坐标都是必须要有的。
新建的 Maven 项目结构如下:
上图左侧是一个 Maven 工程有约定的目录结构:
maven_demo // 工程名作为跟目录
├─src // 源码目录
│ ├─main // 主程序目录
│ │ ├─java // 主程序的Java源文件目录
│ │ └─resources // 主程序的资源文件目录
│ └─test // 测试程序目录
│ └─java
├─target // 编译结果存放目录
│ └─classes
└─ pom.xml // 项目对象模型
上图中间是 pom.xml
文件的内容,可以看到刚刚指定的项目坐标;上图右侧是 Maven 的一些快捷命令按钮,这两部分是了解 Maven 的关键。
⨳ 编写程序
在 src/main/java
下编写一下演示程序:
package com.cango;
public class Main {
public static void main(String[] args) {
System.out.println("Hello Maven");
}
}
POM 文件
上文稍微讲了一个POM和 pom.xml
,下面就看看 pom.xml
的组成和应该怎么配置。
坐标 groupId、artifactId、version
坐标就是项目的唯一标识,从而确保了项目在 Maven 仓库中的唯一性。
上文创建 Maven 项目时填写的 groupId
、artifactId
和 version
就是可以唯一确定一个项目的坐标:
<groupId>com.cango</groupId>
<artifactId>maven_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
除此之外,还有两个与坐标相关的配置项:packaging
和 classifie
:
▪ packaging
(打包类型):指定了 Maven 项目构建输出的类型,通常是 jar(Java项目)、war(Web应用)、ear(Java企业级应用)、pom(纯粹的POM项目) 等,默认为 jar。
▪ classifier
(分类器):是一个可选项,它用于对构件的进一步分类,通常在同一 artifactId
和 version
下存在多个附属构建产物时使用。
项目打完包后的名字是和坐标相对应的, 一般的规则为artifactId-version[-classifier].packaging
。
比如说大名鼎鼎的 SpringBoot 的坐标如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>3.3.2</version>
</dependency>
很自然的会想到 SpringBoot 的 jar包 名字就是 spring-boot-3.3.2.jar
,而 groupId
是该 jar 包在 Maven远程仓库中的目录,如下:
这里再多提一嘴 Spring 的版本号,如上全是数字的版本(3.3.2)是稳定版本,Spring官网上会在该版本后面显示 GA(General Availability)的标识,推荐使用;如果版本号后缀为 SNAPSHOT,表示快照,可以稳定使用,但仍有继续改进版本;如版本号后缀为 PRE,表示预览版,仅给开发人员和测试人员测试和找BUG。
再多提一嘴,如果知道某个 jar 包的名字,但具体坐标不知道,当然可以到官网上找它的坐标,也可以到 Maven 依赖库搜索引擎中搜索对应的 jar 包。
依赖 dependencies
依赖 是指在 Maven 项目中声明和管理的外部库或组件 。
这些依赖是项目运行或构建所需要的,可以是第三方开发的 Java 库、框架或其他工具。通过使用 Maven,开发者可以方便地引入、更新和管理这些依赖。
Maven 最主要的就是使用它的依赖管理功能,格式如下:
<!-- 当前工程所依赖的jar包 -->
<dependencies>
<!-- 使用dependency配置一个具体的依赖 -->
<dependency>
<!-- 在dependency标签内使用具体的坐标依赖我们需要的一个jar包 -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<!-- scope标签配置依赖的范围 -->
<scope>test</scope>
</dependency>
</dependencies>
****
根标签 project
下的 dependencies
可以包含一个或者多个 dependency
标签,以声明一个或者多个项目依赖。每个依赖可以包含的配置项有:
▪ groupId
、artifactId
和 version
:依赖的基本坐标;
▪ type
:依赖的类型,对应于项目坐标定义的 packaging
,默认值为 jar
;
▪ scope
:依赖的范围;
▪ optional
:标记依赖是否可选,即项目是否可以选择不包含此依赖。
▪ exclusions
:用来排除传递性依赖;
因为“依赖”既是动词又是名词,下文提到的名词依赖有时会用 jar 包替代。
依赖的范围 scope
要了解依赖的范围,得先了解 classpath
,因为 Java 是编译型语言,开发者开发的是源码文件 .java
,可以被 JVM 执行的是编译后的 .class
文件,因此,编译器需要知道到哪去找 .java
,JVM 需要知道到哪去找 .class
,classpath
就是用来指定加载路径的。
总来来说有两个阶段需要 classpath
:
▪ 编译 classpath 用于指定需要编译的 java 文件路径,如 src/main/java
;
▪ 运行 classpath 用于指定需要加载进 JVM 的 class 文件路径,如 target/classes
。
算上测试程序(src/test/java
),就是四个阶段需要 classpath:源程序编译 classpath、源程序运行 classpath、测试程序编译 classpath 和 测试程序运行 classpath。
知道了这些,就容易理解依赖的范围了。
依赖范围(scope)用于指定依赖在不同阶段的可用性。通过设置依赖的范围,Maven 可以控制哪些依赖在编译、测试和运行时是可用的。
scope
的可选项如下:
▪ compile
:依赖 jar 包的路径会存在于这四个阶段的 classpath 中,默认依赖的范围。
▪ test
:用于测试阶段,依赖 jar 包的路径仅存在于 测试程序编译和运行classpath 中。如 Junit。
▪ provided
:该依赖 jar 包的路径会仅存在于编译时的 classpath
中。这个范围适用于一些 Java EE 容器和 servlet 容器提供的 API,如 servlet-api
Tomcat 就有这个 jar 包,项目就不需要在运行中再将其加载到 JVM 了。
▪ runtime
:该依赖 jar 包的路径仅存在于运行时的 classpath
中 ,不参与项目的编译过程,例如 jdbc 的驱动包。
难道 jdbc 驱动包不需要编译就可以运行吗?
是因为 jar 包就是编译好的多个 class 文件组成的压缩包,不需要再次编译,而且 jdbc 驱动是以动态反射的方式加载到项目中的(Class.forName
),所以不需要参与项目的编译。
▪ system
:作用范围和 provided
一样,仅存在于编译时的 classpath 中,但system
范围表示依赖不会从 Maven 仓库中解析,而是从本地系统文件中取得。这种范围的依赖通常不建议使用,因为会导致项目在不同环境中出现不一致的情况。
在 Maven 项目中,
compile
和runtime
依赖会被打包进 JAR 文件中,而test
和provided
依赖则不会被打包进 JAR 文件中。
通过正确配置依赖和范围,开发者可以有效地控制 classpath,以确保项目的依赖关系正确、环境隔离清晰。
依赖的传递 exclusions
依赖的传递指的是当一个项目依赖于另一个项目时,它也会自动获取另一个项目所依赖的项目。换句话说,如果项目 A 依赖于项目 B,而项目 B 又依赖于项目 C,则项目 A 就会间接依赖于项目 C。
为啥需要传递依赖的,不传递行不行,前边讲过依赖的本意就是将其加入到 classpath 中,供项目源码编译、加载使用,如果 A 项目不间接依赖 C 项目,那 C 项目就不会加入到 classpath 中,编译 或 运行 A项目时就会报错,那就只能根据报错信息手动在 A项目的 pom 文件中建立 C项目的依赖了。
也就是说,有了间接依赖,可以看作将所有依赖,所以依赖的依赖,所以依赖的依赖的依赖,...,都放到同一份 classpath 里。
那这样的话又会出现几个问题:
1)A 依赖于 B,B 依赖与 C ,但 A配置B 的 scope 和 B配置C 的 scope 不同,那 A 项目间接依赖的 C项目最终对于 A 项目来说,scope 还会保持原状吗?
<!-- 项目A 依赖 项目B-->
<dependency>
<groupId>com.cango</groupId>
<artifactId>B</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
<!-- 项目B 依赖 项目C-->
<dependency>
<groupId>com.cango</groupId>
<artifactId>C</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
第一想法肯定是 C项目在 B项目中怎么配置就怎么解析呗,但仔细想想并不是这样,如果 B项目对于 A项目的 scope
的 test
,那 A项目想当然的会认为 B 项目及其它运行所需要的依赖都要是 test
,这样才不会影响 A 项目的 src
源码。
具体 scope 传递规则如下,最左边一列表示 B 在 A项目配置的 scope
,最上面一行表示 C 在 B项目中配置的 scope
,中间的交叉单元格则表示 A 项目看到的 C 项目的作用范围:
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | \ | \ | runtime |
test | test | \ | \ | test |
provided | provided | \ | provided | provided |
runtime | runtime | \ | \ | runtime |
看起来挺复杂的,但实际上仔细想想也能解释得通:
▪ 当 A 依赖于 B 时,B 也依赖于很多包,但 A 想要的肯定是 src/main/java
源码编译后的 class,那 B 项目中测试程序(src/test/java
)包括测试程序编译运行需要的 jar包,A 项目又不需要,所以 B 项目依赖的 test 范围的 jar 包,不需要转递,如上图 test 列。
▪ 当 A 依赖于 B 时,B 也依赖于很多包,但因为 A 依赖 B 时,B已经是编译完成的 jar 包形式,A 项目也就不需要 B项目依赖的 provided
jar 包再参与编译了,如上图的 provided 列。
也就是说,A 依赖于 B 的真实含义是 A 只想要 B项目 src/main/java
源码编译后的 class,和 B 项目运行需要的 jar 包(compile
和 runtime
)。
▪ 当 A 想让 B 参与编译运行时,其实 A 想要的只有 B项目 src/main/java
源码编译后的 class,包括 B 项目 compile
和 runtime
范围依赖的 jar 包,至于 B 项目 test
和 provided
范围的 jar 包就不需要了,如上图的 compile 行。
▪ 当 A 只想让 B 参与测试(test 行)、编译(provided 行)或运行(runtime 行)时,那 B 项目src/main/java
源码编译成的 class,包括依赖的 jar 包都要降级成 test
、provided
或 runtime
,当然还是还排除掉 test
与 provided
。
至于为啥 A 只想让 B 参与编译(provided
)时,B项目 provided
的依赖也要引入 A 项目,这我也不知道 ...
2) 多个相同的 jar 包问题,如 A 依赖于 B1,B1 依赖于 C;A 依赖于 B2,B2 依赖于 C,那究竟哪个C会被加载呢?
相同的包,加载哪个其实对项目没啥影响,如果是不同版本号相同的 jar 包,那项目 A 最终会加载哪个版本的 jar 包呢?
<!-- 项目A 依赖 项目B1-->
<dependency>
<groupId>com.cango</groupId>
<artifactId>B1</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.cango</groupId>
<artifactId>B2</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 项目B1 依赖 1.0.0 版本的项目C-->
<dependency>
<groupId>com.cango</groupId>
<artifactId>C</artifactId>
<version>1.0.0</version>
</dependency>
<!-- 项目B2 依赖 2.0.0 版本的项目C-->
<dependency>
<groupId>com.cango</groupId>
<artifactId>C</artifactId>
<version>2.0.0</version>
</dependency>
其实 Maven 有两个原则自动处理这种情况:
▪ 路径最近者优先原则:如 项目X 有两条传递路径到 项目A,A->B->C->X(1.0)
和 A->D->X(2.0)
,因 X(2.0)
距离项目A 最短,所以加载到项目A 的 X 是 2.0 版本。
▪ 第一声明者优先原则:如 项目X 传递到 项目A 的路径长度相同,在 POM
中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。
这两个原则可以解决大部分包冲突问题,但如果 A->B->C->X(1.0)
和 A->D->X(2.0)
,X(2.0)
的路径最短,但 A项目 就想用 X(1.0)
,怎么办?
可以在项目 A 的 POM 文件中使用 <exclusion>
标签 将 X(2.0)
给排除掉,如下:
<dependency>
<groupId>com.cango</groupId>
<artifactId>D</artifactId>
<version>1.1</version>
<exclusions>
<exclusion>
<groupId>com.cango</groupId>
<artifactId>X</artifactId>
</exclusion>
</exclusions>
</dependency>
需要注意的是,声明 exclusion 的时候只需要 groupId
和 artifactId
,而不需要 version
,这是因为只需要 groupId
和 artifactId
就能唯一定位依赖中的某个依赖。
<exclusion>
标签配置在上层项目,从而阻止某个依赖向上传递;也可以在下层项目配置 <optional>
标签,同样可以避免赖向上传递,如 D项目 依赖的 X(2.0) 不想传递给上层项目 A,可以在 D 项目的 POM 文件中,这样配置 X:
<dependency>
<groupId>com.cango</groupId>
<artifactId>X</artifactId>
<version>2.0</version>
<optional>true</optional>
</dependency>
<optional>
是“可选的”意思,适用于多个互斥的 jar 包,比如 MySQL 和 Oracle 驱动包,假设 D 项目是与数据库打交道的项目,它可以同时依赖 MySQL 和 Oracle 驱动包,如果 A 项目想要依赖 D 项目与数据库交互,但肯定只能选择一种驱动,如果 D 项目对这两个驱动配置了 <optional>true</optional>
,就阻止了向上传递,A 项目按需依赖想要的驱动即可。
依赖相关命令 dependency
⨳ mvn dependency:list
:列出当前项目的所有依赖项及其版本信息。
执行这个命令时,Maven 会扫描项目的 pom.xml
文件并输出所有直接和间接依赖项的清单。
这个命令的输出通常包括每个依赖项的坐标 (groupId, artifactId, version),以及它们的类型和范围 (例如 compile, test, runtime 等)。这对于了解项目所依赖的库以及它们的版本非常有用,特别是在进行依赖项管理和版本控制时。
如下,是一个 SpringBoot 项目的依赖:
D:\Agit\demo_osframework\springboot_demo>mvn dependency:list
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- dependency:3.6.1:list (default-cli) @ demo ---
[INFO]
[INFO] The following files have been resolved:
[INFO] org.springframework.boot:spring-boot-starter-web:jar:3.3.0:compile -- module spring.boot.starter.web [auto]
[INFO] org.springframework.boot:spring-boot-starter:jar:3.3.0:compile -- module spring.boot.starter [auto]
[INFO] org.springframework.boot:spring-boot:jar:3.3.0:compile -- module spring.boot [auto]
[INFO] org.springframework.boot:spring-boot-autoconfigure:jar:3.3.0:compile -- module spring.boot.autoconfigure [auto]
[INFO] org.springframework.boot:spring-boot-starter-logging:jar:3.3.0:compile -- module spring.boot.starter.logging [auto]
[INFO] ch.qos.logback:logback-classic:jar:1.5.6:compile -- module ch.qos.logback.classic
[INFO] ch.qos.logback:logback-core:jar:1.5.6:compile -- module ch.qos.logback.core
[INFO] org.apache.logging.log4j:log4j-to-slf4j:jar:2.23.1:compile -- module org.apache.logging.log4j.to.slf4j
...
⨳ mvn dependency:tree
:用于生成项目的依赖树。
这个命令会读取项目的 pom.xml
文件,并按照依赖关系递归地展示出项目及其所有依赖项的依赖树。
使用这个命令通常是为了查看项目中的依赖关系,以及确保没有循环依赖或者不必要的依赖。
如下,是一个 SpringBoot 项目的依赖:
D:\Agit\demo_osframework\springboot_demo>mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- dependency:3.6.1:tree (default-cli) @ demo ---
[INFO] com.example:demo:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.3.0:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.3.0:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:3.3.0:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:3.3.0:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:3.3.0:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.5.6:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.5.6:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.23.1:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.23.1:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:2.0.13:compile
[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:2.1.1:compile
[INFO] | | \- org.yaml:snakeyaml:jar:2.2:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:3.3.0:compile
⨳ mvn dependency:analyze
:用于分析项目 pom.xml
文件中声明的依赖项。
执行这个命令时,Maven 会进行以下分析以检测潜在的问题或依赖项不一致性:
▪ 未使用的依赖项: Maven 会识别出在项目代码中声明但实际未被使用的依赖项。
▪ 依赖项汇聚: Maven 检查不同依赖项是否引入了同一库的冲突版本。这有助于识别潜在的版本冲突问题。
▪ 依赖项管理: Maven 提供关于在项目的依赖管理部分(<dependencyManagement>
)中未显式管理的依赖项的反馈。这有助于确保不同模块或项目中版本的一致性。
▪ 插件和扩展依赖: Maven 还检查在构建过程中插件或扩展所需的依赖项,确保它们被正确声明和解析。
这个命令的输出将根据所执行的分析提供警告和建议。它是一个有用的工具,可以在开发过程早期维护依赖项的清洁性并解决潜在的冲突。
聚合与继承 modules、parent
聚合(Aggregation)指的是将多个 Maven 项目合并成一个父项目。
父项目的作用是对子项目的构建和依赖进行统一管理,旨在消除子项目 POM 中的重复配置,所以父项目不包含有任何实际代码,而且父项目的打包类型(packaging)必须是 pom
。
<project>
<groupId>com.cango</groupId>
<artifactId>parent-project</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>child-project-A</module>
<module>child-project-B</module>
<!-- Add more modules as needed -->
</modules>
</project>
因为子项目在父项目 pom 文件中,是配置在 <modules>
模块标签下,所以子项目也可以看作是父项目的一个个模块。
通常将聚合模块放在项目目录的最顶层,其他模块则作为聚合模块的子目录存在,这样当用户得到源码的时候,第一眼发现的就是聚合模块的POM,从而了解整个项目。
parent-project
|─ child-project-A
| |— pom.xml // 子项目的 pom 文件
|— child-project-B
| |— pom.xml // 子项目的 pom 文件
|_ pom.xml // 父项目的 pom 文件
<module>
配置的是子模块的路径,也就是子项目的目录名,一般目录名和子项目的<artifactId>
一致。
继承(Inheritance)则是指一个 Maven 项目(子项目)可以继承自一个父项目的配置信息。
在子项目的 pom.xml
文件中,继承关系需要使用 <parent>
标签声明它们是属于父项目的一部分,如下所示:
<parent>
<groupId>com.cango</groupId>
<artifactId>parent-project</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>child-project-A</artifactId>
<!-- .... -->
多提一嘴,我们开发者自己创建的 SpringBoot 项目,就是将 SpringBoot 作为父项目。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
依赖管理 dependencyManagement
搞项目聚合一个主要目的是统一管理子项目的依赖,怎么统一管理呢?先看子项目能从父项目的 POM 文件中继承什么,节选如下:
▪ 项目的坐标:如 <groupId>
、<version>
、<description>
...
▪ 自定义的Maven属性:<properties>
。
▪ 项目的依赖项:<dependencies>
、<dependencyManagement>
。
▪ 项目的构建项:<build>
▪ ...
总的来说,一个完整的 pom 文件也就有这么几部分,也就是说子项目可以继承父项目大部分的配置项,当然也包括 依赖项 <dependencies>
。
那这就有一个问题了,如果父项目引入的依赖,子项目A想要,子项目B不想要,因为继承的关系,子项目B也会继承这个不想要的 jar 包,怎么办?
这个依赖的传递问题差不多,一个是子传递给父,一个是父遗留给子。
Maven 提供的 <dependencyManagement>
标签可以解决这个问题,当项目的依赖配置用 <dependencyManagement>
标签圈起来的时候,子项目就不会继承父项目的任何依赖了。
如下的 junit 包,如果子项目不主动显示声明在自己的 pom 文件夹,子项目就不会有 junit 包:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
你可能会说,这不就破坏了聚合与继承的意义了嘛,毕竟继承就是为了复用相同的配置,其实对于依赖的继承来说,最重要的是继承依赖的版本。
可以在 dependencyManagement
部分统一定义依赖的版本号,然后在项目中的各个模块中引用这些依赖时,只需要指定依赖的 groupId
和 artifactId
,而不需要重新指定版本号。
如子项目 A 想用 junit 包,仅需要指定 groupId
和 artifactId
即可:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
在大型项目中,多个模块可能会依赖不同版本的相同库。通过集中管理版本,可以避免这些版本冲突问题。
常用命令
要想了解 Maven 命令,必先了解插件与生命周期,先来看一下生命周期。
生命周期 lifecycle
Maven 生命周期定义了项目构建的阶段,如对项目进行清理、编译、测试、部署...
Maven 拥有三套相互独立的生命周期,它们分别为 clean
、default
和 site
。每个生命周期都有不同的构建阶段:
⨳ Clean 生命周期 用于清理项目
▪ pre-clean :在实际项目清理之前执行所需的处理过程。
▪ clean :删除前一次构建生成的所有文件。
▪ post-clean :执行完成项目清理所需的处理过程。
⨳ Default 生命周期 用于构建项目
▪ validate :验证项目是否正确,并且所有必要的信息都可用。
▪ initialize :初始化构建状态,例如设置属性或创建目录。
▪ generate-sources :生成任何需要编译的源代码。
▪ process-sources :处理项目主资源文件。一般来说,是对src/main/resources
目录的内容进行变量替换等工作后,复制到项目输出的 classpath
目录中。
▪ generate-resources :生成需要打包的资源。
▪ process-resources :将资源复制并处理到目标目录,准备打包。
▪ compile :编译项目的主源码。一般来说,是编译 src/main/java
目录下的Java文件至项目输出的 classpath
目录中。
▪ process-classes :对编译生成的文件进行后处理,例如对 Java 类进行字节码增强。
▪ generate-test-sources :生成需要编译的测试源代码。
▪ process-test-sources :处理项目测试资源文件。一般来说,是对src/test/resources
目录的内容进行变量替换等工作后,复制到项目输出的测试classpath
目录中。
▪ generate-test-resources :创建用于测试的资源。
▪ process-test-resources :将资源复制并处理到测试目标目录。
▪ test-compile :编译项目的测试代码。一般来说,是编译src/test/java
目录下的Java文件至项目输出的测试classpath
目录中。
▪ process-test-classes :对测试编译生成的文件进行后处理,例如对 Java 类进行字节码增强。
▪ test :使用适当的单元测试框架运行测试。这些测试不应要求代码被打包或部署。
▪ prepare-package :在实际打包之前执行任何必要的操作以准备打包。这通常会导致一个解压、处理过的包的版本。
▪ package :将编译的代码打包成可分发的格式,如 JAR。
▪ pre-integration-test :在执行集成测试之前执行所需的操作。这可能涉及设置所需的环境等操作。
▪ integration-test :处理并部署包(如果需要)到可以运行集成测试的环境中。
▪ post-integration-test 在执行集成测试后执行所需的操作。这可能包括清理环境等操作。
▪ verify :运行任何检查以验证包是否有效并符合质量标准。
▪ install :将包安装到本地仓库中,以供其他项目在本地使用。
▪ deploy :在集成或发布环境中执行,将最终的包复制到远程存储库,以与其他开发人员和项目共享。
⨳ Site 生命周期 用于建立项目站点
▪ pre-site :在实际项目站点生成之前执行所需的处理过程
▪ site :生成项目的站点文档
▪ post-site :执行完成站点生成所需的处理过程,并准备站点部署
▪ site-deploy :将生成的站点文档部署到指定的 Web 服务器
了解这些生命周期有啥用呢?
-
阶段是有顺序的,并且后面的阶段依赖于前面的阶段、比如 Clean生命周期,用户调用
post-clean
的时候,pre-clean
、clean
和post-clean
会以顺序执行,并不是只执行调用那个阶段。 -
用户和 Maven 最直接的交互方式就是调用这些生命周期阶段。如:
# 调用Clean生命周期的clean阶段
# 实际执行的阶段为Clean生命周期的pre-clean和clean阶段。
mvn clean
# 调用Default生命周期的test阶段。
# 实际执行的阶段为Default生命周期的validate、initialize等,直到test的所有阶段。
mvn test
# 该命令调用Clean生命周期的clean阶段和Default生命周期的install阶段。
# 实际执行的阶段为Clean生命周期的pre-clean、clean阶段,以及Default生命周期的从validate至install的所有阶段。
mvn clean install
# 该命令调用Clean生命周期的clean阶段、Default生命周期的deploy阶段,以及Site生命周期的site-deploy阶段。
# 实际执行的阶段为clean生命周期的pre-clean、clean阶段,default生命周期的所有阶段,以及site生命周期的所有阶段。
mvn clean deploy site-deploy
这三个生命周期的每个阶段都是独一无二的,这样就是可以直接指定阶段,而不是生命周期+阶段的方式进行调用。
常用的 Maven 命令实际都是基于这些阶段简单组合而成的,因此只要对 Maven生命周期有一个基本的理解,就可以正确而熟练地使用 Maven 命令。
插件 plugin
Maven 的生命周期只是个逻辑上的定义,真正干活的实际上是插件,插件能够完成的工作叫目标(goals),一个生命周期所涉及的阶段都是插件完成的。而目插件完成的目标是与生命周期的阶段绑定的。
所以了解插件与插件的目标,能更好的理解生命周期:
⨳ maven-clean-plugin:在项目目录中删除构建时(build-time)生成的文件
▪ clean:clean 查找并删除在 project.build.directory
、project.build.outputDirectory
、project.build.testOutputDirectory
和 project.reporting.outputDirectory
中配置的目录中的文件。对应 Clean生命周期 clean 阶段.
⨳ maven-compiler-plugin:编译项目的源代码
▪ compiler:compile 编译应用程序源代码。默认情况下使用执行Maven的JDK的javac编译器。* 对应 Default生命周期的 compile 阶段。*
▪ compiler:testCompile 编译应用程序测试源代码。默认情况下使用执行Maven的JDK的javac编译器。对应 Default 生命周期的 test-compile 阶段。
⨳ maven-surefire-plugin:用于构建生命周期中的测试阶段
▪ surefire:test 执行测试用例。对应 Default生命周期的 test 阶段。
⨳ maven-jar-plugin:创建项目 jar 包
▪ jar:jar 将项目的主要构件以及生命周期中其他插件附加的任何其他构件安装到本地仓库。对应 Default 生命周期的 install 阶段。
⨳ maven-install-plugin:将项目输出构件安装到本地仓库
▪ install:install 将项目的主要构件以及生命周期中其他插件附加的任何其他构件安装到本地仓库。对应 Default 生命周期的 test 阶段。
⨳ maven-deploy-plugin:将项目输出构件部署到远程仓库
▪ deploy:deploy 将构件部署到远程仓库。 对应 Default 生命周期的 deploy 阶段。
⨳ 更多插件可以的到 Maven 插件网站查看。。。
了解插件,就可以就可以按需要将某一个插件绑定到某个生命周期的某一个阶段上,如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
此配置片段设置了 maven-source-plugin
在 Maven 构建的 verify
阶段执行,很容易理解吧。
可以看到插件也是用坐标进行定位的,有坐标就说明这个插件不是 org.apache.maven
独有的东西,其他厂商也可以自己创建不同的插件。
如果配置的插件是 Maven 的官方插件,就可以省略
groupId
配置。
插件的命令执行和生命周期的执行差不太多,指定哪个插件的哪个目标即可,如上文提到的 dependency:list
、dependency:tree
和 dependency:analyze
其实就是执行的 maven-dependency-plugin 插件的 list
目标、tree
目标 和 analyze
目标。
至于为什么命令不用写插件的精确坐标,甚至连插件 artifactId
都不需要写全,这是与 Maven 自动解析与前缀有关,这里就不详细说明了。
关于插件需要注意一点的是,插件的命令执行是指定哪个目标就执行哪个目标,生命周期是指定哪个阶段,执行该阶段及其前边的阶段。
Maven命令格式 mvn
使用 -h
选项可以查看 Maven 命令的用法:
$ mvn -h
usage: mvn [options] [<goal(s)>] [<phase(s)>]
options
是选项,基本每个明里都有这玩意,这里就不一一说明了,goal(s)
是插件:目标,很容易理解吧,phase(s)
是生命周期某个阶段,也很容易理解吧。
下面看几个常用的命令,作为这篇文章的收尾:
▪ clean
清理 输出目录 target/
D:\Agit\demo_osframework\maven_demo>mvn clean
[INFO] Scanning for projects...
[INFO] --- clean:3.2.0:clean (default-clean) @ maven_demo ---
[INFO] ...
[INFO] Deleting D:\Agit\demo_osframework\maven_demo\target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.797 s
[INFO] Finished at: 2024-05-31T16:36:57+08:00
[INFO] ------------------------------------------------------------------------
▪ compile
编译项目代码(包括 resources 文件和 java 代码)到输出目录 target/
D:\Agit\demo_osframework\maven_demo>mvn compile
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< com.cango:maven_demo >------------------------
[INFO] Building maven_demo 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] ...
[INFO] --- resources:3.3.1:resources (default-resources) @ maven_demo ---
[INFO] ...
[INFO] Copying 0 resource from src\main\resources to target\classes
[INFO]
[INFO] --- compiler:3.13.0:compile (default-compile) @ maven_demo ---
[INFO] ...
[INFO] Compiling 1 source file with javac [debug target 1.8] to target\classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.093 s
[INFO] Finished at: 2024-05-31T16:53:16+08:00
[INFO] ------------------------------------------------------------------------
▪ package
将项目打成jar包
D:\Agit\demo_osframework\maven_demo>mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< com.cango:maven_demo >------------------------
[INFO] Building maven_demo 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] Building jar: D:\Agit\demo_osframework\maven_demo\target\maven_demo-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18.735 s
[INFO] Finished at: 2024-05-31T16:57:53+08:00
[INFO] ------------------------------------------------------------------------
▪ install
将项目输出的jar安装到了Maven本地仓库,供其他Maven项目引用
D:\Agit\demo_osframework\maven_demo>mvn install
[INFO] Scanning for projects...
[INFO] Installing D:\Agit\demo_osframework\maven_demo\pom.xml to D:\ProgrammingSoft\Maven\apache-maven-3.9.7\repository\com\cango\maven_demo\1.0-SNAPSHOT\maven_demo-1.0-SNAPSHOT.pom
[INFO] Installing D:\Agit\demo_osframework\maven_demo\target\maven_demo-1.0-SNAPSHOT.jar to D:\ProgrammingSoft\Maven\apache-maven-3.9.7\repository\com\cango\maven_demo\1.0-SNAPSHOT\maven_demo-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.537 s
[INFO] Finished at: 2024-05-31T17:02:08+08:00
[INFO] ------------------------------------------------------------------------