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:listmvn dependency:treemvn dependency:analyzemvn 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=cleanmvn help:describe -Dplugin=cleanmvn help:describe -Dplugin=org.apache.maven.plugins:maven-clean-plugin:2.5mvn help:describe -Dplugin=org.apache.maven.plugins:maven-clean-plugin:2.5 -Dgoal=cleanmvn help:describe -Dplugin=help -Ddetailmvn help:describe -Dplugin=versionsmvn help:describe -Dplugin=archetypemvn 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