一、基础概念
1.1 Maven是什么
Maven是一个项目构建,依赖管理和项目管理工具。它提供了一套标准化的项目结构,一套标准化的构建流程(编译,测试,打包,发布…),一套依赖管理机制(对jar统一管理,自动去中央仓库下载相关依赖,并解决依赖的依赖问题)。
1.2 为什么使用Maven?
由于 Java 的生态非常丰富,无论你想实现什么功能,都能找到对应的工具类,这些工具类都是以 jar 包的形式出现的,例如 Spring,SpringMVC、MyBatis、数据库驱动,等等,都是以 jar 包的形式出现的,jar 包之间会有关联,在使用一个依赖之前,还需要确定这个依赖所依赖的其他依赖,所以,当项目比较大的时候,依赖管理会变得非常麻烦臃肿,这是 Maven 解决的第一个问题。
Maven 还可以处理多模块项目。简单的项目,单模块分包处理即可,如果项目比较复杂,要做成多模块项目,例如一个电商项目有订单模块、会员模块、商品模块、支付模块...,一般来说,多模块项目,每一个模块无法独立运行,要多个模块合在一起,项目才可以运行,这个时候,借助 Maven 工具,可以实现项目的一键打包。
二、Maven项目结构
2.1 项目结构
Maven的标准目录结构默认如下:
a-maven-project 项目名
├── pom.xml 项目描述文件
├── src
│ ├── main
│ │ ├── java 项目的Java源码
│ │ └── resources 项目的资源,例如property文件,spring.xml
│ └── test
│ ├── java 项目的测试源码
│ └── resources 测试用的资源
└── target
└──classes 编译输出目录
2.2 项目描述文件pom.xml
POM( Project Object Model,项目对象模型 ) 是 Maven 工程的基本工作单元,是一个XML文件,包含了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。执行任务或目标时,Maven 会在当前目录中查找 POM。它读取 POM,获取所需的配置信息,然后执行目标。
POM 中可以指定以下配置:项目依赖,插件,执行目标,项目构建 profile,项目版本等。
<?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/xsd/maven-4.0.0.xsd"> <!-- 模型版本 --> <modelVersion>4.0.0</modelVersion> <!-- 公司或者组织的唯一标志,并且配置时生成的路径也是由此生成, 如com.companyname.project-group,maven会将该项目打成的jar包放本地路径:/com/companyname/project-group --> <groupId>com.companyname.project-group</groupId> <!-- 项目的唯一ID,一个groupId下面可能多个项目,就是靠artifactId来区分的 --> <artifactId>project</artifactId> <!-- 版本号 --> <version>1.0</version> <!--项目产生的构件类型,例如jar、war、ear、pom。插件可以创建他们自己的构件类型,所以前面列的不是全部构件类型 --> <packaging>jar</packaging> <name>用户更为友好的项目名称</name> ...... </project>
第一个标签是xml头,其指定了该xml文档的版本和编码方式。
第二个标签project,是工程的根标签。它声明了一些POM相关的命名空间及xsd元素。
<modelVersion>指定了当前的POM模型的版本,Maven3的模型版本只能是4.0.0。
<groupId> 是项目组的标识,定义了项目属于哪个组,它在一个组织或者项目中是唯一的。例如,谷歌公司的myapp项目组,就取名为 com.google.myapp。
<artifactId>是项目的标识,它通常是项目的名称。groupId 和 artifactId 一起定义了 Artifact 在仓库中的位置。
<version>是版本号。例如,0.0.1-SNAPSHOT,SNAPSHOT意为快照,说明该项目还处于开发中,是不稳定的。
<groupId> 、<artifactId>和<version>定义了一个项目基本的坐标,任何的jar、pom都是以基于这三个基本的坐标进行区分的。
<name>声明六一个对用户友好的项目名称,但不是必须的。
三、Maven依赖管理
依赖管理是Maven的核心功能。Maven提供了多模块项目的模块间的复杂依赖关系的管理问题,以及我们的项目所依赖的第三方jar包的下载问题。
例如,我们的项目依赖abc这个jar包,而abc又依赖xyz这个jar包。当我们声明了自己的项目需要abc,Maven会自动导入abc的jar包,再判断出abc需要xyz,又会自动导入xyz的jar包,这样,最终我们的项目会依赖abc和xyz两个jar包。当我们声明一个spring-boot-starter-web依赖时,Maven会自动解析并判断最终需要大概二三十个其他依赖。如果我们自己去手动管理这些依赖是非常费时费力的,而且出错的概率很大。
3.1 依赖的配置
<?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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.companyname.project-group</groupId> <artifactId>project</artifactId> <version>1.0</version> <packaging>jar</packaging> <!--该元素描述了项目相关的所有依赖。 这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。 --> <dependencies> <!--参见dependencies/dependency元素 --> <dependency> <groupId>项目组</groupId> <artifactId>项目</artifactId> <version>版本</version> <type>依赖类型</type> <scope>依赖范围</scope> <optional>依赖是否可选</optional> <!—主要用于排除传递性依赖--> <exclusions> <exclusion> <groupId>…</groupId> <artifactId>…</artifactId> </exclusion> </exclusions> </dependency> ...... </dependencies> </project>
根元素project下的dependencies可以包含一个或者多个dependency元素,以声明一个或者多个项目依赖。每个依赖可以包含的元素有:
-
grounpId、artifactId和version:依赖的基本坐标,Maven根据坐标才能找到需要的依赖。
-
type:依赖的类型,对应项目坐标定义的packaging。大部分情况下,该元素不必声明,其默认值为jar
-
scope:依赖的范围
-
optional:标记依赖是否可选
-
exclusions:用来排除传递性依赖
3.2 依赖范围
Maven有如下几种依赖范围:
-
compile(默认): 编译时需要用到当前依赖,该依赖参与项目的编译、运行、测试、打包。
-
test: 依赖只在编译测试代码和运行测试代码的时候需要,在编译主代码或者运行项目的使用时将无法使用此依赖,打包的时候也不会包含。例如Jnuit,它只有在编译测试代码及运行测试的时候才需要。
-
provided:依赖在编译时需要用到,但运行时无效。参与项目的编译、运行、测试,但是不参与打包。例如servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器以及提供,就不需要Maven重复地引入一遍。
-
runtime: 编译时不需要用到当前依赖,但运行时需要用到。该依赖不参与项目编译,参与项目的运行、测试、打包。例如JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
-
system:依赖范围和provided完全一致,但是,使用system范围的依赖时必须通过systemPath元素显示地指定依赖文件的路径。由于此类依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能构成构建的不可移植,因此应该谨慎使用。systemPath元素可以引用环境变量,如:
-
<dependency> <groupId>javax.sql</groupId> <artifactId>jdbc-stdext</artifactId> <Version>2.0</Version> <scope>system</scope> <systemPath>${java.home}/lib/rt.jar</systemPath> </dependency>
-
3.3传递性依赖
假设项目A依赖于项目B,项目B依赖于项目C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖和第二直接依赖的范围决定了传递性依赖的范围,如下表所示,最左边一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递依赖范围。
compile (第二直接依赖范围) |
test |
provided |
runtime | |
---|---|---|---|---|
compile (第一直接依赖范围) |
compile |
- |
- |
runtime |
test |
test |
- |
- |
test |
provided |
provided |
- |
provided |
provided |
runtime |
runtime |
- |
- |
runtime |
有了传递性依赖机制,A在使用B的时候就不用去考虑它依赖了什么,也不用担心引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。
3.4 依赖调节
依赖调节的作用是当多个手动创建的版本同时出现时,决定哪个依赖版本将会被使用。
依赖调节的两大原则:
-
路径最近原则:例如项目有A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,所以根据路径最近原则,A->D->X(2.0)路径短,所以X(2.0)会被解析使用。
-
若在A中直接引入对X(1.0)的依赖,则其路径更短,X(1.0)被解析使用。
-
-
第一声明者优先原则:例如项目有A有这样的依赖关系:A->B->Y(1.0)、A->C->Y(2.0),项目A到Y(1.0)和Y(2.0)的路径一样长,路径最近原则不适用,根据第一声明者优先原则,先声明的被解析。
3.5 可选依赖
项目中A依赖B,B依赖于X和Y,如果所有这三个的范围都是compile的话,那么X和Y就是A的compile范围的传递性依赖。但是,如果想让X、Y不作为A的传递性依赖,就需要使用配置可选依赖。用<optional>true</optional>标识可选依赖,这样A如果想用X、Y就要直接显示的添加依赖了。
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.companyname.project-group</groupId> <artifactId>project-b</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>com.companyname.project-group</groupId> <artifactId>project-x</artifactId> <version>1.1.2</version> <optional>true</optional> </dependency> <dependency> <groupId>com.companyname.project-group</groupId> <artifactId>project-y</artifactId> <version>1.1.1</version> <optional>true</optional> </dependency> </dependencies> </project>
3.6 排除依赖
有时候你引入的依赖中包含你不想要的依赖,这时候就要用到排除依赖了,例如spring-boot-starter-web自带了logback这个日志包,我想引入log4j2的,所以我先排除掉logback的依赖包,再引入想要的包就行了。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
排除依赖代码结构:
<exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions>
注意:声明exclustion的时候只需要groupId和artifactId,而不需要version元素,这是只需要groupId和artifactId就能唯一定位依赖图中的某个依赖。
四、仓库
4.1 仓库的概念
在Maven世界中,任何一个依赖、插件或者项目构建的输出,都可以称为构件。得益于坐标机制,任何Maven项目使用任何一个构件的方式都是完全相同的。在此基础上,Maven可以在某个位置统一存储所有Maven项目共享的构件,这个统一的位置就是仓库。
实际的Maven项目将不再各自存储其依赖文件,它们只需要声明这些依赖的坐标,在需要的时候(例如,编译项目的时候需要将依赖加入到classpath中),Maven会自动根据坐标找到仓库中的构件,并使用它们。
为了实现重用,项目构建完毕后可生成的构件也可以安装或者部署到仓库中,供其他项目使用。
4.2 仓库的分类
Maven仓库分为本地仓库和远程仓库,远程仓库又分为中央仓库,私服仓库等。
本地仓库
本地仓库是指把本地开发的构件“发布”在本地,这样其他项目可以通过本地仓库引用它。但是我们不推荐把自己的模块安装到Maven的本地仓库,因为每次修改某个模块的源码,都需要重新安装,非常容易出现版本不一致的情况。更好的方法是使用模块化编译,在编译的时候,告诉Maven几个模块之间存在依赖关系,需要一块编译,Maven就会自动按依赖顺序编译这些模块。
运行 Maven 的时候,Maven 所需要的任何构件都是直接从本地仓库获取的。如果本地仓库没有,它会首先尝试从远程仓库下载构件至本地仓库,然后再使用本地仓库的构件。
中央仓库
Maven 中央仓库是由 Maven 社区提供的仓库,由Apache 团队Apache 团队来维护。其中包含了绝大多数流行的开源Java构件,它们由第三方模块的开发者自己把编译好构建发布到Maven的中央仓库之中。
私有仓库
私有仓库是指公司内部如果不希望把源码和jar包放到公网上,那么可以搭建私有仓库。私有仓库总是在公司内部使用,它只需要在本地的~/.m2/settings.xml中配置好,使用方式和中央仓位没有任何区别。
4.3 Maven 依赖搜索顺序
当我们执行 Maven 构建命令时,Maven 开始按照以下顺序查找依赖的库:
-
在本地仓库中搜索,如果找不到,执行步骤 2,如果找到了则执行其他操作。
-
在中央仓库中搜索,如果找不到,并且有一个或多个远程仓库已经设置,则执行步骤 4,如果找到了则下载到本地仓库中以备将来引用。
-
如果远程仓库没有被设置,Maven 将简单的停滞处理并抛出错误(无法找到依赖的文件)。
-
在一个或多个远程仓库中搜索依赖的文件,如果找到则下载到本地仓库以备将来引用,否则 Maven 将停止处理并抛出错误(无法找到依赖的文件)。
五、Maven命令
5.1 Maven生命周期
5.1.1 default生命周期
Maven的生命周期由一系列阶段(phase)构成,以内置的生命周期default为例,它包含以下phase:
生命周期阶段phase |
描述 |
---|---|
validate(校验) |
校验项目是否正确并且所有必要的信息可以完成项目的构建过程。 |
initialize(初始化) |
初始化构建状态,比如设置属性值。 |
generate-sources(生成源代码) |
生成包含在编译阶段中的任何源代码。 |
process-sources(处理源代码) |
处理源代码,比如说,过滤任意值。 |
generate-resources(生成资源文件) |
生成将会包含在项目包中的资源文件。 |
process-resources (处理资源文件) |
复制和处理资源到目标目录,为打包阶段最好准备。 |
compile(编译) |
编译项目的源代码。 |
process-classes(处理类文件) |
处理编译生成的文件,比如说对Java class文件做字节码改善优化。 |
generate-test-sources(生成测试源代码) |
生成包含在编译阶段中的任何测试源代码。 |
process-test-sources(处理测试源代码) |
处理测试源代码,比如说,过滤任意值。 |
generate-test-resources(生成测试资源文件) |
为测试创建资源文件。 |
process-test-resources(处理测试资源文件) |
复制和处理测试资源到目标目录。 |
test-compile(编译测试源码) |
编译测试源代码到测试目标目录. |
process-test-classes(处理测试类文件) |
处理测试源码编译生成的文件。 |
test(测试) |
使用合适的单元测试框架运行测试(Juint是其中之一)。 |
prepare-package(准备打包) |
在实际打包之前,执行任何的必要的操作为打包做准备。 |
package(打包) |
将编译后的代码打包成可分发格式的文件,比如JAR、WAR或者EAR文件。 |
pre-integration-test(集成测试前) |
在执行集成测试前进行必要的动作。比如说,搭建需要的环境。 |
integration-test(集成测试) |
处理和部署项目到可以运行集成测试环境中。 |
post-integration-test(集成测试后) |
在执行集成测试完成后进行必要的动作。比如说,清理集成测试环境。 |
verify (验证) |
运行任意的检查来验证项目包有效且达到质量标准。 |
install(安装) |
安装项目包到本地仓库,这样项目包可以用作其他本地项目的依赖。 |
deploy(部署) |
将最终的项目包复制到远程仓库中与其他开发者和项目共享。 |
如果我们运行mvn package,Maven就会执行default生命周期,它会从头开始一直运行到package这个phase为止。即validate,...,package。
如果我们运行mvn compile,Maven也会执行default生命周期,但这次它只会运行到compile,即以下几个phase。即validate,...,compile。
5.1.2 clean生命周期
Maven另一个常用的生命周期是clean,它会执行3个phase:
-
pre-clean:执行一些需要在clean之前完成的工作
-
clean:移除所有上一次构建生成的文件(注意这个clean不是lifecycle而是phase)
-
post-clean:执行一些需要在clean之后立刻完成的工作
我们使用mvn这个命令时,后面的参数是phase,Maven自动根据生命周期运行到指定的phase。
更复杂的例子是指定多个phase,例如,运行mvn clean package,Maven先执行clean生命周期并运行到clean这个phase,然后执行default生命周期并运行到package这个phase。即pre-clean,clean (注意这个clean是phase),validate,...,package。
5.1.3 site生命周期
Maven Site 插件一般用来创建新的报告文档、部署站点等。
-
pre-site:执行一些需要在生成站点文档之前完成的工作
-
site:生成项目的站点文档
-
post-site: 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备
-
site-deploy:将生成的站点文档部署到特定的服务器上
5.2 Maven命令
在实际开发过程中,经常使用的命令有:
-
mvn clean:清理所有生成的class和jar;
-
mvn clean compile:先清理,再执行到compile;
-
mvn clean test:先清理,再执行到test,因为执行test前必须执行compile,所以这里不必指定compile;
-
mvn clean package:先清理,再执行到package。
-
...
六、模块管理
在软件开发中,把一个大项目分拆为多个模块是降低软件复杂度的有效方法。对于Maven工程来说,原来是一个大项目:
single-project
├── pom.xml
└── src
现在可以分拆成3个模块:
mutiple-project
├── module-a
│ ├── pom.xml
│ └── src
├── module-b
│ ├── pom.xml
│ └── src
└── module-c
├── pom.xml
└── src
6.1 继承
Maven可以有效地管理多个模块,我们只需要把每个模块当作一个独立的Maven项目,它们有各自独立的pom.xml。我们提取模块的共同部分作为parent。
multiple-project
├── pom.xml
├── parent
│ └── pom.xml
├── module-a
│ ├── pom.xml
│ └── src
├── module-b
│ ├── pom.xml
│ └── src
└── module-c
├── pom.xml
└── src
注意parent的<packaging>是pom而不是jar,因为parent本身不含任何Java代码。编写parent的pom.xml只是为了在各个模块中减少重复的配置。
module-a 继承了 parent 。
<?xml version="1.0" encoding="UTF-8"?> <project> <parent> <groupId>org.example</groupId> <artifactId>parent</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <packaging>jar</packaging> <artifactId>module-a</artifactId> </project>
6.2 聚合
在编译的时候,需要在根目录创建一个pom.xml统一编译。multiple-project 聚合了三个工程。
<?xml version="1.0" encoding="UTF-8"?> <project > <groupId>org.example</groupId> <artifactId>maven-demo</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>parent</module> <module>maven-a</module> <module>maven-b</module> <module>maven-c</module> </modules> </project>
这样,在根目录执行mvn clean package时,Maven根据根目录的pom.xml找到包括parent在内的共4个<module>,一次性全部编译。
小结:
-
继承用于消除冗余配置,比如一些配置打包配置,配置的变量,项目依赖和插件依赖版本管理。
-
聚合用于快速构建项目。聚合之前打包 a,b,c 需要分别运行 mvn package。聚合之后,我咱们只需要在 multiple-project 下运行 mvn package。
参考资料
https://www.runoob.com/maven/maven-tutorial.html
https://www.liaoxuefeng.com/wiki/1252599548343744/1255945359327200
http://tengj.top/2018/01/01/maven/