Apache Maven必知必会
前言
在没有使用Maven
之前,我们开发一个JavaWeb
项目,如果使用到非JDK
提供的类库,需要去网上下载对应的jar
包,然后将jar
包复制粘贴放到项目的lib
目录下才能够使用。这样的做法是很麻烦的,每开发一个JavaWeb
项目都需要去将jar
包文件放置在lib
目录下,且只能以文件的形式进行管理。
我们可以使用Maven
的依赖管理功能来减少复制jar
包这样重复的工作。
什么是Maven
?
Maven
是一个项目管理工具,可以对项目(不仅限于Java
语言)进行构建和依赖管理。简单来说:使用了Maven
之后我们就不需要再去下载复制粘贴jar
包了。
Maven
安装
Maven
的约定配置
Maven
提供约定优于配置的原则,对于一个使用Maven
构建和依赖管理的项目,应该遵循以下的目录结构:
目录 | 目录存放内容 |
---|---|
${basedir} | Maven 项目根路径,存放pom.xml 和所有的子目录 |
${basedir}/src/main/java | 项目的java 类文件 |
${basedir}/src/main/resources | 项目的资源文件,例如properties 配置文件,前端静态资源文件等 |
${basedir}/src/test/java | 项目的单元测试类文件,一般为Junit 的单元测试类 |
${basedir}/src/test/resources | 提供给测试类使用的资源文件 |
${basedir}/src/main/webapp/WEB-INF | JavaWeb 项目的web 应用文件目录,存放web.xml 、.jsp 和前端静态资源等文件。 |
${basedir}/target | 打包输出目录 |
${basedir}/target/classes | 编译输出目录,java 类编译后的字节码文件目录 |
${basedir}/target/test-classes | 测试类编译输出目录 |
XXXTest.java | Maven 只会自动运行类名以Test 结尾的测试类 |
~/.m2/repository | Maven 的默认本地仓库路径 |
pom.xml
常用标签
下面是一个典型Spring Boot
项目的pom.xml
文件内容:
1 |
|
以上是将Spring Boot
作为parent
的pom.xml
,这样的好处是parent
可以聚合依赖的版本等一些信息,在后面添加依赖时只需要填写groupId
和artifactId
坐标,版本号默认继承parent
中定义的版本号。但很多时候我们的项目是Maven
多模块项目,或者由于一些其它原因导致我们无法使用Spring Boot
作为parent
,这个时候就需要用到dependencyManagement
标签来进行依赖预定义了,Spring Boot
提供了BOM
用来进行预定义依赖的引入,使用方式如下:
1 |
|
Maven
的生命周期
Maven
有三个标准的生命周期:
clean
:项目清理的处理。default(build)
:项目部署的处理。site
:项目站点文档创建的处理。
Clean
生命周期
包含以下阶段:
pre-clean
:执行一些需要在clean
之前完成的工作。clean
:移除所有上一次构建生成的文件。post-clean
:执行一些需要在clean
之后立即完成的工作。
我们所用的mvn clean
命令就是上面的clean
阶段。在一个生命周期中,运行某个阶段的时候,在它之前的所有阶段都会被运行。也就是说,如果执行maven clean
命令将运行pre-clean
和clean
这两个生命周期阶段。
Default(build)
生命周期
包含以下23
个阶段:
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-class
处理测试类文件:处理测试源码编译生成的文件。test
测试:使用合适的单元测试框架运行测试(Junit
是其中之一)。prepare-package
准备打包:在实际打包之前,执行任何的必要的操作为打包做准备。package
打包:将编译后的代码打包成可分发格式的文件,比如JAR
、WAR
或者EAR
文件。pre-integration-test
集成测试前:在执行集成测试前进行必要的动作。比如说,搭建需要的环境。integration-test
集成测试:处理和部署项目到可以运行集成测试环境中。post-integration-test
集成测试后:在执行集成测试完成后进行必要的动作。比如说,清理集成测试环境。verify
验证:运行任意的检查来验证项目包有效且达到质量标准。install
安装:安装项目包到本地仓库,这样项目包可以用作其他本地项目的依赖。deploy
部署:将最终的项目包发布到远程仓库中与其他开发者和项目共享。
我们常用的mvn install
命令,在执行install
之前,按顺序执行了之前的21
个阶段。如果用于多模块项目,每一个子项目都会执行mvn install
命令。
还可以一次指定两个阶段,例如mvn clean deploy
,这可以用来纯净的构建和部署项目到远程仓库中。Maven
会先执行clean
命令,再执行deploy
命令,对多模块项目也适用。
Site
生命周期
Maven Site
插件一般用来创建新的报告文档和部署站点等。这个在我们的开发工作中一般不会用到,了解即可。
包含以下4
个阶段:
pre-site
:执行一些需要在生成站点文档之前完成的工作。site
:生成项目的站点文档。post-site
:执行一些需要在生成站点文档之后完成的工作,并且为部署做准备。site-deploy
:将生成的站点文档部署到特定的服务器上。
Maven
仓库
Maven
仓库简单的理解就是存储JAR
包的地方。有以下三种类型:
- 本地仓库(
local
) - 中央仓库(
central
) - 远程仓库(
remote
)
本地仓库(local
)
运行Maven
项目时,任何依赖的构建或第三方JAR
包都是直接从本地仓库获取的。如果本地仓库没有,它会首先尝试从远程仓库下载构建至本地仓库,然后再使用本地仓库的构建。
默认的本地仓库路径为~/.m2/repository
,要修改该默认位置,可在Maven
的setting.xml
文件中指定本地仓库路径:
1 |
|
中央仓库(central
)
中央仓库是由Maven
社区提供的仓库,需要通过网络进行访问。国内常用的是阿里云中央仓库。可在Maven
的setting.xml
文件中配置阿里云的中央仓库源:
1 |
|
Maven
社区提供了一个网站:https://search.maven.org,可搜索到所有可以获取的构建库和JAR
包。
远程仓库(remote
)
如果Maven
在中央仓库也找不到依赖的构建,它会停止构建过程并输出错误信息到控制台。为了避免这种情况,Maven
提供了远程仓库的概念,它是开发人员自己定制的仓库,包含了所需要的代码库或者其它工程中用到的JAR
文件。
Maven
依赖搜索顺序
首先从本地仓库(local
)中搜索,如果找不到,则去中央仓库(central
)中搜索;
如果在中央仓库(central
)中找不到,则查看是否设置了远程仓库(remote
),如果没有设置,则停止搜索并抛出错误(无法找到依赖)。
如果在远程仓库(remote
)中找不到,则停止搜索并抛出错误(无法找到依赖);如果在远程仓库(remote
)中找到了,则下载至本地仓库并引用。
Maven
插件
Maven
的三个标准生命周期中都包含一系列的阶段,每一个阶段的具体实现都是由Maven
的插件完成。
例如我们使用的mvn clean
命令,clean
对应着Clean
生命周期中的clean
阶段,clean
的具体操作是由maven-clean-plugin
完成的。
插件类型
Maven
提供了以下两种类型的插件:
Build Plugins
:在构建时执行,并在pom.xml
的元素中配置。Reporting Plugins
:在网站生成过程中执行,并在pom.xml
的元素中配置。
常用插件
下面是一些常用的插件:
clean
:构建之后清理目标文件。删除目标目录。compiler
:编译Java
源文件。surefile
:运行Junit
单元测试。创建测试报告。jar
:从当前工程中构建jar
文件。war
:从当前工程中构建war
文件。javadoc
:为工程生成javadoc
。antrun
:从构建过程的任意一个阶段中运行一个ant
任务的集合。
引入外部依赖
有些时候我们需要去对接一些第三方的SDK
包,一般第三方会提供SDK
的下载地址,但是很可能未发布至中央仓库,就导致无法通过Maven
坐标直接引入。这个时候我们需要手动将SDK
包下载并复制粘贴到项目中,可在${basedir}/src/main/resources
目录下新建一个lib
目录,专门用来存放未发布至中央仓库的JAR
包。引入方式如下:
1 | <dependencies> |
快照(SNAPSHOT
)
在Apache Dubbo
微服务项目开发中,服务消费方需要去引入服务提供方对外暴露的API
接口jar
包。服务提供方很有可能在短期内多次修改对外暴露的接口,如果按照正常的版本号:producer-service.jar:1.0.0
,那每修改一次接口都需要升级一个小版本,服务消费方不得不去更新pom.xml
来引用最新的jar
包。如果修改接口而不升级版本号就发布至仓库中,服务消费方在引用时不会去仓库下载相同版本号的最新jar
包。也就是说,服务消费方只会下载一次指定版本号的jar
包。为了避免这种繁琐的更新,Maven
提供了快照版本。
快照版本是一种特殊的版本,指定了当前的开发进度的副本。不同于常规的版本,Maven
每次构建都会在远程仓库中检查新的快照,服务提供方只需发布包含最新代码的快照版本(producer-service.jar:1.0.0-SNAPSHOT
)至仓库中,服务消费方每次都会自动去获取包含最新代码的快照(producer-service.jar:1.0.0-SNAPSHOT
)。
依赖管理
Maven
解析jar
包的方式是依赖传递。例如我们引入spring-boot-starter-web
,当Maven
解析该依赖时,不仅仅会引入其内部依赖的spring-web
、spring-webmvc
和spring-boot-starter-tomcat
等,还会引入这些内部依赖所依赖的jar
包,例如spring-web
依赖的spring-bean
等,依赖关系不断向下传递,直至没有依赖,最终形成了一颗依赖树。
依赖冲突的问题
举个栗子:假设有依赖A
,它内部的依赖传递关系为:A
->B
->C
->D
->E1
;有另一个依赖F
,它内部的依赖传递关系为:F
->G
->E2
。E1
和E2
为E
的不同版本。
如果pom.xml
同时引入A
和F
依赖,按照Maven
依赖传递原则,实际引入的依赖将包括:A
、B
、C
、D
、E1
、F
、G
和E2
,因此E1
和E2
将会产生包冲突。
解决依赖冲突
Maven
解析pom.xml
时,同一个groupId
和artifactId
的依赖只会保留一个,这样可以有效避免因引入不同版本的依赖所带来的问题。
Maven
默认处理策略:
- 最短路径优先原则:因为从
F
到E2
的路径比从A
到E1
的路径短,所以Maven
在面对E1
和E2
时会选择E2
。 - 最先声明优先原则:举个栗子:
A
->B
->C1
;D
->E
->C2
。这两个依赖传递的路径长度是一样的,所以谁在pom.xml
先被声明就引入谁。
排除依赖:默认处理策略已经能解决包的依赖问题,但还是会显示依赖冲突,现象就是IDEA
中会报红;同时默认处理引入的依赖版本号可能不是我们所需要的,如果我们想引入指定版本号的依赖,可以使用<exclusions />
和<exclusion>
标签先排除冲突的依赖,再另外单独在pom.xml
中引入指定版本号的依赖。
检测包冲突的Maven
命令如下:
mvn dependency:help
mvn dependency:analyze
mvn dependency:tree
mvn dependency:tree -Dverbose
最佳实践
当开发多模块项目时,在最外层的父pom.xml
中使用dependencyManagement
标签进行依赖及版本号的预定义,便于统一管理项目依赖及版本号。在子模块引入依赖时,只需指定groupId
和artifactId
即可引入对应依赖。
参考
- 菜鸟教程 -
Maven
教程