这部分内容主要根据 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 的本质是一个软件项目管理和理解工具。基于项目对象模型 (Project Object Model,POM) 的概念,Maven 可以从一条中心信息管理项目的构建、报告和文档。
什么是 POM? 每一个 Maven 工程都有一个 pom.xml
文件,位于根目录中,包含项目构建生命周期的详细信息。通过 pom.xml
文件,我们可以定义项目的坐标、项目依赖、项目信息、插件信息等等配置。
对于开发者来说,Maven 的主要作用主要有 3 个:
关于 Maven 的基本使用这里就不介绍了,建议看看官网的 5 分钟上手 Maven 的教程:Maven in 5 Minutes 。
项目中依赖的第三方库以及插件可统称为构件。每一个构件都可以使用 Maven 坐标唯一标识,坐标元素包括:
只要你提供正确的坐标,就能从 Maven 仓库中找到相应的构件供我们使用。
举个例子(引入阿里巴巴开源的 EasyExcel):
com.alibaba easyexcel 3.1.1
你可以在 https://mvnrepository.com/ 这个网站上找到几乎所有可用的构件,如果你的项目使用的是 Maven 作为构建工具,那这个网站你一定会经常接触。
如果使用 Maven 构建产生的构件(例如 Jar 文件)被其他的项目引用,那么该构件就是其他项目的依赖。
配置信息示例:
... ... ... ... ...
配置说明:
classpath 用于指定 .class
文件存放的位置,类加载器会从该路径中加载所需的 .class
文件到内存中。
Maven 在编译、执行测试、实际运行有着三套不同的 classpath:
Maven 的依赖范围如下:
servlet-api.jar
在 Tomcat 中已经提供了,我们只需要的是编译期提供而已。1、对于 Maven 而言,同一个 groupId 同一个 artifactId 下,只能使用一个 version。
in.hocg.boot mybatis-plus-spring-boot-starter 1.0.48 in.hocg.boot mybatis-plus-spring-boot-starter 1.0.49
若相同类型但版本不同的依赖存在于同一个 pom 文件,只会引入后一个声明的依赖。
2、项目的两个依赖同时引入了某个依赖。
举个例子,项目存在下面这样的依赖关系:
依赖链路一:A -> B -> C -> X(1.0) 依赖链路二:A -> D -> X(2.0)
这两条依赖路径上有两个版本的 X,为了避免依赖重复,Maven 只会选择其中的一个进行解析。
哪个版本的 X 会被 Maven 解析使用呢?
Maven 在遇到这种问题的时候,会遵循 路径最短优先 和 声明顺序优先 两大原则。解决这个问题的过程也被称为 Maven 依赖调解 。
路径最短优先
依赖链路一:A -> B -> C -> X(1.0) // dist = 3 依赖链路二:A -> D -> X(2.0) // dist = 2
依赖链路二的路径最短,因此,X(2.0)会被解析使用。
不过,你也可以发现。路径最短优先原则并不是通用的,像下面这种路径长度相等的情况就不能单单通过其解决了:
依赖链路一:A -> B -> X(1.0) // dist = 3 依赖链路二:A -> D -> X(2.0) // dist = 2
因此,Maven 又定义了声明顺序优先原则。
依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A->B->Y(1.0)、A-> C->Y(2.0),Y(1.0)和 Y(2.0)的依赖路径长度是一样的,都为 2。Maven 定义了依赖调解的第二原则:
声明顺序优先
在依赖路径长度相等的前提下,在 pom.xml
中依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。该例中,如果 B 的依赖声明在 D 之前,那么 X (1.0)就会被解析使用。
... dependency B ... dependency D
单纯依赖 Maven 来进行依赖调解,在很多情况下是不适用的,需要我们手动排除依赖。
举个例子,当前项目存在下面这样的依赖关系:
依赖链路一:A -> B -> C -> X(1.5) // dist = 3 依赖链路二:A -> D -> X(1.0) // dist = 2
根据路径最短优先原则,X(1.0) 会被解析使用,也就是说实际用的是 1.0 版本的 X。
但是!!!这会一些问题:如果 D 依赖用到了 1.5 版本的 X 中才有的一个类,运行项目就会报NoClassDefFoundError
错误。如果 D 依赖用到了 1.5 版本的 X 中才有的一个方法,运行项目就会报NoSuchMethodError
错误。
现在知道为什么你的 Maven 项目总是会报NoClassDefFoundError
和NoSuchMethodError
错误了吧?
如何解决呢? 我们可以通过exclusive
标签手动将 X(1.0) 给排除。
...... x org.apache.x
一般我们在解决依赖冲突的时候,都会优先保留版本较高的。这是因为大部分 jar 在升级的时候都会做到向下兼容。
如果高版本修改了低版本的一些类或者方法的话,这个时候就能直接保留高版本了,而是应该考虑优化上层依赖,比如升级上层依赖的版本。
还是上面的例子:
依赖链路一:A -> B -> C -> X(1.5) // dist = 3 依赖链路二:A -> D -> X(1.0) // dist = 2
我们保留了 1.5 版本的 X,但是这个版本的 X 删除了 1.0 版本中的某些类。这个时候,我们可以考虑升级 D 的版本到一个 X 兼容的版本。
在 Maven 世界中,任何一个依赖、插件或者项目构建的输出,都可以称为 构件 。
坐标和依赖是构件在 Maven 世界中的逻辑表示方式,构件的物理表示方式是文件,Maven 通过仓库来统一管理这些文件。 任何一个构件都有一组坐标唯一标识。有了仓库之后,无需手动引入构件,我们直接给定构件的坐标即可在 Maven 仓库中找到该构件。
Maven 仓库分为:
settings.xml
文件中可以看到 Maven 的本地仓库路径配置,默认本地仓库路径是在 ${user.home}/.m2/repository
。Maven 远程仓库可以分为:
Maven 依赖包寻找顺序:
Maven 的生命周期就是为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。
Maven 定义了 3 个生命周期META-INF/plexus/components.xml
:
default
生命周期clean
生命周期site
生命周期这些生命周期是相互独立的,每个生命周期包含多个阶段(phase)。并且,这些阶段是有序的,也就是说,后面的阶段依赖于前面的阶段。当执行某个阶段的时候,会先执行它前面的阶段。
执行 Maven 生命周期的命令格式如下:
mvn 阶段 [阶段2] ...[阶段n]
default
生命周期是在没有任何关联插件的情况下定义的,是 Maven 的主要生命周期,用于构建应用程序,共包含 23 个阶段。
validate initialize generate-sources process-sources generate-resources process-resources compile process-classes generate-test-sources process-test-sources generate-test-resources process-test-resources test-compile process-test-classes test prepare-package package pre-integration-test integration-test post-integration-test verify install deploy
根据前面提到的阶段间依赖关系理论,当我们执行 mvn test
命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。
clean 生命周期的目的是清理项目,共包含 3 个阶段:
pre-clean clean post-clean org.apache.maven.plugins:maven-clean-plugin:2.5:clean
根据前面提到的阶段间依赖关系理论,当我们执行 mvn clean
的时候,会执行 clean 生命周期中的 pre-clean 和 clean 阶段。
site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段:
pre-site site post-site site-deploy org.apache.maven.plugins:maven-site-plugin:3.3:site org.apache.maven.plugins:maven-site-plugin:3.3:deploy
Maven 能够基于 pom.xml
所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。
Maven 本质上是一个插件执行框架,所有的执行过程,都是由一个一个插件独立完成的。像咱们日常使用到的 install、clean、deploy 等命令,其实底层都是一个一个的 Maven 插件。关于 Maven 的核心插件可以参考官方的这篇文档:https://maven.apache.org/plugins/index.html 。
本地默认插件路径: ${user.home}/.m2/repository/org/apache/maven/plugins
除了 Maven 自带的插件之外,还有一些三方提供的插件比如单测覆盖率插件 jacoco-maven-plugin、帮助开发检测代码中不合规范的地方的插件 maven-checkstyle-plugin、分析代码质量的 sonar-maven-plugin。并且,我们还可以自定义插件来满足自己的需求。
jacoco-maven-plugin 使用示例:
org.jacoco jacoco-maven-plugin 0.8.8 prepare-agent generate-code-coverage-report test report
你可以将 Maven 插件理解为一组任务的集合,用户可以通过命令行直接运行指定插件的任务,也可以将插件任务挂载到构建生命周期,随着生命周期运行。
Maven 插件被分为下面两种类型:
多模块管理简单地来说就是将一个项目分为多个模块,每个模块只负责单一的功能实现。直观的表现就是一个 Maven 项目中不止有一个 pom.xml
文件,会在不同的目录中有多个 pom.xml
文件,进而实现多模块管理。
多模块管理除了可以更加便于项目开发和管理,还有如下好处:
多模块管理下,会有一个父模块,其他的都是子模块。父模块通常只有一个 pom.xml
,没有其他内容。父模块的 pom.xml
一般只定义了各个依赖的版本号、包含哪些子模块以及插件有哪些。不过,要注意的是,如果依赖只在某个子项目中使用,则可以在子项目的 pom.xml 中直接引入,防止父 pom 的过于臃肿。
如下图所示,Dubbo 项目就被分成了多个子模块比如 dubbo-common(公共逻辑模块)、dubbo-remoting(远程通讯模块)、dubbo-rpc(远程调用模块)。