Maven 详细介绍
maven 是一个声明式的 Java 程序构建工具,最开始人们使用 make 命令搭配 makefile 脚本实现构建过程,tomcat 的作者认为 make 命令不跨平台且脚本编写复杂,因此发明了 Ant(Another Neat Tool)。Ant 解决了 make 命令不跨平台且脚本编写困难的问题,不过 Ant 依然是过程式的,每一个使用 Ant 的用户仍然需要编写自己所需要的一系列脚本。
maven 通过定义了一系列的标准,让用户基本不再需要自己编写脚本,只需要按照 maven 暴露出的简单标准接口实现构建操作。这样既可以降低用户使用的复杂度,也能够定义一套统一的标准,当用户接手一个全新的项目时,可以根据已知的标准快速上手。
安装
maven 的安装很简单,只需要下载压缩包解压到磁盘上,并将MAVEN根目录/bin添加到PATH中方便使用mvn命令。自 maven 3.5 之后已经不再需要设置JAVA_HOME和M2_HOME环境变量了,同时建议使用手动安装的 maven 替换 idea 中的 bundle maven。
1 | mvn -v |
pom.xml
和 Make 的 Makefile、Ant 的 build.xml 一样,maven 的核心是 pom.xml,POM(Project Object Model,项目对象模型)描述了项目的详细信息。
1 |
|
例如针对如上的一个 pom.xml 配置文件,第一行定义了文件的版本和编码,紧接着的就是 project 元素,它声明了一些描述信息以方便编辑工具检查 XML 文件的格式。project 元素包含了一些子元素,它们的含义如下
| 元素 | 含义 |
|---|---|
| modelVersion | POM 的版本,对于 Maven 2 和 Maven 3 这个值为 4.0.0 |
| groupId | 项目所属的组织 |
| artifactId | 项目在组织中的名称 |
| version | 项目的版本 |
| name | 项目的可读名称,非必须 |
| description | 项目的描述,非必须 |
maven 提供了一个叫做archetype的插件,这个插件是一个创建 maven 项目的脚手架工具。我们可以使用help插件的describe goal 来查看这个插件的详细信息
mvn help:describe -Dplugin=archetype
查看描述信息可以知道archetype插件有一个generate的 goal,可以使用这个 goal 创建 maven 项目
mvn archetype:generate
maven 坐标
maven 将依赖通过一些属性的定义管理起来,就如同地理上确定一个位置需要经纬度一样,maven 中确定一个依赖需要一下几个属性
1 | <groupId>org.example</groupId> |
前三个属性必须设置,packaging 是可选的,默认为 jar。当 maven 项目需要依赖其它项目的时候,一般可以有如下配置
1 | <project> |
groupId、artifactId 和 version 代表依赖的基本坐标,而 type 默认为 jar。
scope(依赖范围)
maven 在编译项目时会使用一个 classpath,在测试项目的时候会使用另一个 classpath,而最终打包的结果在运行业务的时候也会使用一个自己的 classpath。这对应了依赖的 scope 的三个选项
compile
如果不指定就使用 compile 选项,代表编译依赖范围。使用这个配置的依赖范围的 maven 依赖,在编译、测试和运行的时候这个依赖都有效。例如 spring-code
test
测试依赖范围,这些依赖只对测试 classpath 有效。例如 JUint
provided
已提供依赖范围,对编译和测试 classpath 有效,对运行时 classpath 无效。例如 servlet-api,运行时 tomcat 会提供
runtime
运行时依赖范围,对测试和运行 classpath 有效,对编译时无效。例如 JDBC 驱动,代码编译的时候只需要接口,实现只在真正运行时才需要。即 SPI(Service Provider Interface)机制下常用。
传递性依赖
maven 会对依赖的依赖进行依赖,即传递性依赖。例如 A 依赖 B,B 依赖 C,则 A 也会依赖 C。传递依赖存在优先级的概念:
- 如果有多个依赖引用了同一个依赖,则选择最短路径。例如
- A -> B -> C -> X(1)
- A -> D -> X(2)
- 因为 X(2) 路径更短,选择 X(2)
- 如果路径长度一样,谁先声明就选谁
可选依赖
可以通过<optional></optional>标签实现可选依赖,例如 B 项目有依赖
1 | <dependency> |
那么 B 项目会正常依赖 mysql 的包。但是如果此时有 A 项目依赖了 B 项目,那么 A 项目是不会自动依赖 mysql 包的。A 项目如果需要正常依赖 mysql,就需要手动引入 mysql 的依赖才行。
排除依赖
如果项目 A 依赖了 B 项目,但是却不想引用 B 项目里面的某个依赖,则可以使用排除依赖
1 | <dependency> |
版本变量
如果多个依赖使用了同样的版本,则可以在 pom.xml 中定义一个版本变量
1 | <project> |
maven 提供了插件 dependency 来查看依赖的一些信息
mvn dependency:list
mvn dependency:tree
mvn dependency:analyze
mvn dependency:sources -X
仓库
maven 的仓库分为远程仓库和本地仓库。在以前没有 maven 仓库的时候,都是从网上下载或者从别人那里拷贝的 jar 包,把包复制到 eclipse 的/lib 文件夹下,并手动将包添加到 classpath 中。
maven 的包一般按照 groupId、artifactId、version 的方式管理,例如
1 | <dependency> |
的依赖就位于com/fasterxml/jackson/core/jackson-core/2.12.3文件夹下。一般来说本地的仓库位于~/.m2/repository文件夹,当然也可以根据~/.m2/settings.xml中的配置修改本地仓库的位置
1 | <settings> |
当依赖在本地仓库不存在时,就会从远程仓库中下载(推荐 一个 maven 依赖搜索网站)。
maven 的默认远程仓库:https://repo1.maven.org/maven2/,也可以在pom.xml中使用其它的远程仓库
1 | <project> |
同样的,可以在pom.xml中配置发布依赖的远程仓库
1 | <project> |
使用命令mvn deploy就可以把 release 或 snapshot 版本的依赖推到私服上去了。
maven 仓库可以配置镜像,例如如下的镜像配置
1 | <mirrors> |
就代表了使用阿里云的镜像来替换对 central 中央仓库的请求。
生命周期、阶段、目标和插件
我们在日常软件开发和构建中总会有着一些固定的流程,maven 对这些流程做了思考和分析,总结出了三套默认的生命周期(lifecycle),clean、default 和 site。clean 生命周期用于清理项目,default 生命周期用于构建项目,site 生命周期用于构建项目站点,它们之前相互独立互不影响。
每个生命周期又由多个阶段(phase)组成,phase 的执行有先后顺序的概念。在执行某个 phase 时,会按顺序从这个 lifecycle 最开始的 phase 开始执行,当上一个 phase 执行完之后会开始继续执行下一个 phase,一直执行到当前指定的 phase 结束。maven 的生命周期的阶段如下表
clean:清理构建输出,包括生成的编译类、JAR 文件等
| 阶段 | 描述 |
|---|---|
| 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 | |
| generate-test-sources | |
| process-test-sources | 处理src/test/resources目录的资源 |
| generate-test-resources | |
| process-test-resources | |
| test-compile | 编译src/test/java并放到 classpath 中 |
| process-test-classes | |
| test | 执行单元测试 |
| prepare-package | |
| package | 将编译好的代码打包成可发布的格式,如 jar |
| pre-integration-test | |
| integration-test | |
| post-integration-test | |
| verify | |
| install | 将打包后的文件安装到本地仓库 |
| deploy | 将打包后的文件发布到远程仓库 |
site:为项目生成文档
| 阶段 | 描述 |
|---|---|
| pre-site | 执行生成站点之前需要完成的工作 |
| site | 生成项目站点文档 |
| post-site | 执行生成站点之后需要完成的工作 |
| site-deploy | 将生成的项目站点发布到服务器上 |
可以在命令行直接执行 maven 的 phase,例如执行mvn clean将会执行 clean 生命周期的 clean 阶段,在 clean 执行之前 pre-clean 阶段会先执行。同样的mvn package会执行 default 生命周期的 package 阶段,在 package 阶段执行之前会先执行 package 阶段之前的阶段。phase 也可以组合起来执行,mvn clean package就会先执行 clean 阶段,之后再执行 package 阶段。
插件和目标
maven 的阶段只是一个声明,它不执行任何实际的操作,maven 实际的操作都是由目标(goal)来完成的,而 goal 则是由 maven 的插件(plugin)实现的,目标也称为 MOJO(Maven Old Java Object,与 Plain Old Java Object 对应)。只需要将插件的某个 goal 绑定到一个 phase,在执行这个 phase 的时候就会执行这个 goal。一个 phase 可以绑定多个 goal,一个插件也可以实现多个不同功能的 goal。
如上图,plugin abc 分别实现了一些 goal,而这些 goal 可以随意的绑定到指定的阶段上。一个 phase 可以绑定多个 goal,一个 goal 也可以绑定多个 phase。当执行某个 phase 的时候,其实就是在执行这一系列绑定在 phase 上的 goal。
上面已经介绍了 maven 的 phase 例如 clean 和 package 执行方法,我们已经知道执行 phase 实际上就是在执行绑定在这个 phase 的 goal。事实上,我们也可以直接执行插件的 goal 而不执行 phase,语法如下
mvn groupId:artifactId:version:goal
例如
mvn org.apache.maven.plugins:maven-clean-plugin:2.5:clean
如果是 maven 官方插件
- 可以省略 groupId
- 还可以省略 artifactId 中的 maven-xxx-plugin,即命名的通用部分。maven 官方插件的命名为
maven-xxx-plugin,非官方推荐为xxx-maven-plugin。 - 如果不使用版本号,会自动使用最新的版本(maven 2.x 版本会拉取最新 snapshot 版本,存在问题,3.x 只会拉取最新的 release 版本)
- 因此上面执行 goal 的命令也可以简化为
mvn clean:clean
插件 maven-help-plugin 的 describe 目标可以查看 phase 和 plugin 的详细信息(-D,--define Define a system property)
mvn help:describe -Dcmd=clean
mvn help:describe -Dplugin=clean
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-clean-plugin:2.5
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-clean-plugin:2.5 -Dgoal=clean
mvn help:describe -Dplugin=help -Ddetail
mvn help:describe -Dplugin=versions
mvn help:describe -Dplugin=archetype
mvn help:describe -Dplugin=com.ymm:apide-maven-plugin:1.7.5
maven 默认的 phase 就已经绑定了一些 goal,因此我们可以直接使用 maven 的阶段而不需要手动声明插件依赖,maven 阶段默认绑定的 goal 如下
| 阶段 | 插件 | goal | 任务 |
|---|---|---|---|
| clean | maven-clean-plugin | clean | 清除已生成的构建文件 |
| process-resources | maven-resources-plugin | resources | 复制主资源至主输出目录 |
| compile | maven-compiler-plugin | compile | 编译主代码至主输出目录 |
| process-test-resources | maven-resources-plugin | testResources | 复制测试资源至测试输出目录 |
| test-compile | maven-compiler-plugin | testCompile | 编译测试代码至测试输出目录 |
| test | maven-surefire-plugin | test | 执行测试用例 |
| package | maven-jar-plugin | jar | 创建项目 jar 包 |
| install | maven-install-plugin | install | 将项目输出构建安装到本地仓库 |
| deploy | maven-deploy-plugin | deploy | 将项目输出构建安装到远程仓库 |
| site | maven-site-plugin | site | 创建项目站点 |
| site-deploy | maven-site-plugin | deploy | 发布项目站点 |
除了已经绑定好的 goal,我们在项目中也可以手动将插件的 goal 绑定到指定 phase 上
1 | <build> |
如上就是在这个项目中,将插件 lc-maven-plugin 的名称为all的 goal 绑定到 package 这个 phase 上,当项目执行到 package 阶段的时候,插件的目标 all 就会执行。此外,我们还定义了 maven 插件的配置,设置了 appName 等参数的值。
除了上面用到的全局配置外,maven 还可以将配置设置在指定的任务上
1 | <executions> |
当然,直接在命令行设置参数也是可以的
mvn package -DappName=lc-service
聚合与继承
聚合
在项目开发中,我们经常会需要有多个相互配合的模块,例如 RPC 接口一般就会包含一个需要暴露给客户端的 API 模块和一个需要部署在服务端的 API 具体实现模块。如果将这两个模块分开来,那么在开发的时候在每个模块都需要去执行 maven 相关的操作命令,这显然是很不方便的。
maven 因此提出了模块的聚合概念,我们可以给一些模块定义一个聚合模块,对于这些模块通用的操作,我们都可以在聚合模块中去完成。例如我们有 project-api 和 project-service 两个模块,它们的 pom.xml 分别如下:
project-api
1 |
|
project-service
1 |
|
如果我们想要同时对它们执行 maven 的相关操作,我们可以再在它们的同级目录创建一个 pom.xml 文件,此时的目录结构如下图
展开子模块可以看到 project-api 和 project-service 都是普通的 maven 模块
聚合模块的 pom.xml 配置如下
1 |
|
聚合模块的定义和普通模块存在很多一样的地方,例如 groupId、artifactId 等,但也存在区别。第一个特殊的地方就是packaging,它的值为pom,和之前普通模块的jar不一样。聚合模块的打包方式packaging的值必须为pom,否则就无法正常构建。
另一个特殊的地方就是元素modules,它包含了多个模块,每个模块都是一个当前聚合模块的子模块,module的值为子模块所在的目录与当前pom.xml文件的相对路径。一般会把子模块和聚合模块的 pom.xml 放在同一个目录下面,方便进行源码管理。当然,也可以不遵循这个规则,例如如下的一个目录结构,将聚合 pom.xml 放在一个文件夹里面
.
|-- project
| `-- pom.xml
|-- project-api
`-- project-service
那么聚合模块的 pom.xml 中的module的配置就应该如下
1 | <module>../project-api</module> |
根据上面创建的模块结构,我们可以直接在 project 文件夹下面执行 maven 指令,而不需要再去 project-api 和 project-service 目录中重复执行 maven 命令了。我们执行命令mvn compile可以得到如下输出
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] project-api
[INFO] project-service
[INFO] project
[INFO]
[INFO] Reactor Summary:
[INFO]
[INFO] project-api ........................................ SUCCESS [ 0.977 s]
[INFO] project-service .................................... SUCCESS [ 0.085 s]
[INFO] project ............................................ SUCCESS [ 0.001 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.138 s
[INFO] Finished at: 2023-06-13T14:21:59+08:00
[INFO] Final Memory: 14M/207M
[INFO] ------------------------------------------------------------------------
可以看到 maven 分别对 project-api 和 project-service 执行了操作,最后对 project 本身也执行了 maven 操作。这就是 maven 聚合功能的好处,如果没有聚合功能,我们也许会创建一个build.sh,在里面定义对多个模块的打包的命令
1 | cd project-api |
这种过程式的操作方法更像 Ant 的操作,而不符合 maven 的声明式操作理念。感谢 maven 的聚合功能,让我们不再需要去手动编写多模块处理脚本。
继承
上面提到的聚合是为了避免在每个子模块中重复执行 maven 操作,是通过聚合模块来操作子模块。而继承的目的,则是通过子模块获取父模块中的配置,防止在子模块中重复的对属性进行配置。
例如在上面的例子中,两个子模块的 groupId 和 version 与父模块中的值都是一样的,这显然是一种重复,因为 maven 可以在子模块中继承父模块来使用父模块的属性值。所以我们可以修改子模块的 pom.xml 如下
1 |
|
如上就是设置了 api 模块继承 project 模块,这样一来 api 模块就可以不需要设置自己的 groupId 和 version 了。当然,如果子模块需要有自己的 groupId 或 version,也可以显式的进行设置对父模块的值进行覆盖。常见的继承属性如下
| 属性 | 说明 |
|---|---|
| groupId | 项目组 ID |
| version | 项目版本 |
| description | 项目的描述信息 |
| organization | 项目的组织信息 |
| inception Year | 项目的创建年份 |
| url | 项目 URL 地址 |
| developers | 项目的开发者信息 |
| contributors | 项目的贡献者信息 |
| distributionManagement | 项目的部署配置 |
| issueManagement | 项目的缺陷跟踪系统信息 |
| ciManagement | 项目的持续集成信息 |
| scm | 项目的版本控制系统信息 |
| mailingLists | 项目的邮件列表信息 |
| properties | 项目的自定义的属性 |
| dependencies | 项目的依赖配置 |
| dependencyManagement | 项目的依赖管理配置 |
| repositories | 项目的仓库配置 |
| build | 项目的源码目录配置、输出目录配置、插件配置、插件管理配置等 |
| reporting | 项目的报告输出目录配置、报告插件配置等 |
以上的属性都可以让子模块从父模块继承到,需要着重介绍的是 dependencies 和 dependencyManagement 属性
dependencies
所有声明在 dependencies 里的依赖都会被自动引入,并且被所有的子项目继承
dependencyManagement
dependencyManagement 只是声明依赖,并不会真正的引入。
当父模块的某个依赖声明在了 dependencyManagement 中,如果子项目中没有显式声明此依赖,则子项目是不会引入该依赖的。只有当子项目在 dependencies 中声明了该依赖,此时该依赖才真正的被子项目依赖了。
如果子项目中的依赖没有指定版本号,就会继承父项目中 dependencyManagement 依赖的版本号,子项目在声明的时候也可以重写自己所需要的版本号。使用该配置的目的是为了在不把所有的依赖都继承给子模块的情况下,统一所有子模块中某个指定依赖的版本号。
一般使用方法就是父模块声明所有用到的依赖的版本号,然后子模块真正的进行依赖且不需要加版本号,这样不同的子模块中使用依赖的版本号都会继承自父模块,保证子模块中版本号的一致。
类似的,maven 也有插件的版本管理机制 pluginManagement,用法也类似,在父 pom 中设置了 pluginManagement 的相关配置之后,子模块只要在 plugin 元素下配置相关的 groupId 和 artifactId 即可。
提示:Maven 在依赖的时候,如果存在有父子关系的包,即使只依赖子 jar,也是需要把父的 pom 推到 nexus 上面去的。如果只推了子 jar 而没有推父 pom,在依赖的时候会报错。
就像 Java 的所有类都继承自java.lang.Object类一样,maven 所有的 pom 也是继承自一个 pom。它位于M2_HOME/lib/maven-model-builder-x.x.x.jar中,解压这个 jar,它位于org/apache/maven/model/pom-4.0.0.xml。maven 的继承主要是为了解决两个问题,即减少重复和统一标准,继承可以让子模块不再需要反复的去声明一些配置,也可以让所有子模块拥有和父模块一样统一的配置。
小结
从上面可以看到,maven 的聚合和继承是两个不同的东西。聚合是为了将多个模块的执行操作合并成一个,减少重复的命令操作;而继承则是为了让多个模块都使用父模块的配置信息,防止许多重复的配置,例如我们使用 spring-boot 的时候经常会需要 继承一个父模块,这里就只是使用了继承操作而没有使用聚合。
当然,很多时候聚合和继承也会结合起来一起使用的,更多关于继承和聚合的内容可以参考 maven 的 官方文档。
编写 maven 插件
从上面我们已经知道,maven 的操作都是由插件实际完成的,有的时候已有的插件不能完成我们所需要的功能,这时候就需要自己编写相关的插件。编写插件的主要步骤如下
- 创建一个 maven-plugin 项目,它也是一个 maven 项目,只不过它的 packaging 元素应该是 maven-plugin
- 为插件编写目标 goal,每个插件都应该有一个或者多个 goal
- 为目标提供配置参数,这些参数可以在使用插件的时候进行配置
- 编写代码实现 goal 的逻辑
- 错误处理以及测试
例如如果我们想要创建一个统计源码行数(line count,lc)的插件,可以使用如下命令创建一个 maven 插件项目
mvn archetype:generate \
-DgroupId=org.example \
-DartifactId=lc-maven-plugin \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-plugin
创建完项目后,设置 pom.xml 如下
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
maven-plugin-api 是开发 maven 插件必须依赖的核心包,而 maven-plugin-annotations 是为了能在在项目中使用 maven 插件开发的注解。其它诸如 groupId、artifactId 等元素都是很常见的,packaging 元素也如之前所说的是 maven-plugin。
接下来我们开始实现插件的 goal,一个插件可以有一个或多个 goal,每个 goal 都需要继承org.apache.maven.plugin.AbstractMojo类,并实现其execute()方法,goal 的名称由注解org.apache.maven.plugins.annotations.Mojo定义。
1 |
|
如上 goal 的名称就叫做 count,而 goal 的具体逻辑就在execute方法中实现,相关的参数可以使用注解@Parameter引入。参数注解的defaultValue属性可以定义参数的默认值,默认值可以包含与这个项目参数相关的表达式,例如${project.version}代表项目的版本。更多参数的表达式可以在 这里查到,如${repositorySystemSession}就代表了项目的本地仓库。就如同之前所说的,可以使用参数-D在命令行中设定系统变量来定义插件参数的值。
关于 maven 插件开发更加详细的信息可以 参考 maven 官方插件开发文档,完整的插件代码也可以在 GitHub 上面 找到。
想要使用如上插件,需要先在插件项目执行mvn clean install将项目安装到本地 maven 仓库,随后在其它的项目中依赖此插件。例如想要将插件绑定到某个 maven 项目的 clean 阶段,可以进行如下设置
1 | <plugin> |
之后在项目中执行mvn clean就可以看到插件的执行信息了。
参考
《Maven 实战》
终于把项目构建神器 Maven 捋清楚了~
Maven-构建生命周期、阶段、目标
maven 插件开发实战
Guide to Developing Java Plugins