Maven是一个项目管理和整合工具。Maven为开发者提供了一套完整的构建生命周期框架。开发团队几乎不用花多少时间就能够自动完成工程的基础构建配置,因为Maven使用了一个标准的目录结构和一个默认的构建生命周期。

Maven的主要目的是为开发者提供:​

  • ​一个可复用、可维护、更易理解的工程综合模型,与这个模型交互的插件或工具​
  • ​Maven工程结构和内容定义在一个xml文件中(一般是pom.xml)

Maven常用命令

常用参数

  • mvn -e 显示详细错误
  • mvn -U 强制更新snapshot类型的插件或依赖库(否则maven一天只会更新一次snapshot依赖)
  • mvn -o 运行offline模式,不联网更新依赖
  • mvn -N 仅在当前项目模块执行命令,关闭reactor
  • mvn -pl module_name在指定模块上执行命令
  • mvn -ff 在递归执行命令过程中,一旦发生错误就直接退出
  • mvn -Dxxx=yyy 指定java全局属性
  • mvn -Pxxx 引用profile xxx
  • mvn -s setting.xml -gs setting.xml -Dmaven.repo.local=$(pwd)/repos package -Dmaven.test.skip=true

mvn package

完成项目编译、单元测试、打包功能,打包到本项目,一般在项目target目录下。

打包文件未部署到本地Maven仓库和远程Maven仓库。

mvn install

完成项目编译、单元测试、打包功能,同时把打包文件部署到本地Maven仓库,但未部署到远程Maven仓库。

mvn deploy

完成项目编译、单元测试、打包功能,同时把打包文件部署到本地Maven仓库和远程Maven仓库。

1
2
3
4
5
6
7
8
9
10
mvn deploy:deploy-file 
-Dmaven.test.skip=true
-DgroupId=com.example
-DartifactId=demo
-Dversion=1.0.0
-Dpackaging=jar
-Dfile=demo-1.0.0.jar
-DrepositoryId=x-repo
-Durl=http://localhost:8081/nexus/content/repositories/thirdparty/
-DpomFile=pom.xml
  • -DgeneratePom
    生成pom文件,默认为true,只会生成一个最简单的pom,缺少依赖,会导致依赖该组件的服务无法获取组件的依赖。
  • -DpomFile
    指定本地pom文件作为组件的pom。

调用过程

当执行 mvn deploy 命令时,Maven 会通过maven-deploy-plugin将构件上传到 Nexus 的 Maven 仓库对应的 REST API。

Maven执行原理

https://blog.csdn.net/u014534566/article/details/106269680?spm=1001.2014.3001.5502
https://blog.csdn.net/yin18827152962/article/details/121482340

Maven本质上是定义并实现了一套管理和执行插件扩展能力的框架,从mvn式命令(这里的命令可以是手动输入,也可以是某些工具集成从而以程序语言执行)输入开始,mvn命令通过maven框架解析并映射到对应的maven插件库找到对应的插件包,相应的插件包接收到命令及入参执行相应行为,这是最直接的解释

mvn命令的解析

maven是由本质是执行命令,因为maven构建框架是固定的,变化的是插件,因此一级命令mvn(我们通常看到的mvn -v查看maven版本,就是这个mvn)是一致的。

一级命令取决于脚本的名称。毫无疑问,脚本文件的逻辑含义是maven核心之一。
接下来深入看看这些脚本干了啥。

mvn

1
2
3
4
5
6
7
8
9
10
11
12
CLASSWORLDS_JAR=`echo "${MAVEN_HOME}"/boot/plexus-classworlds-*.jar`
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher

exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "${CLASSWORLDS_JAR}" \
"-Dclassworlds.conf=${MAVEN_HOME}/bin/m2.conf" \
"-Dmaven.home=${MAVEN_HOME}" \
"-Dlibrary.jansi.path=${MAVEN_HOME}/lib/jansi-native" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${CLASSWORLDS_LAUNCHER} "$@"

mvn.cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for %%i in ("%MAVEN_HOME%"\boot\plexus-classworlds-*) do set CLASSWORLDS_JAR="%%i"
set CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher

"%JAVACMD%" ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %CLASSWORLDS_JAR% ^
"-Dclassworlds.conf=%MAVEN_HOME%\bin\m2.conf" ^
"-Dmaven.home=%MAVEN_HOME%" ^
"-Dlibrary.jansi.path=%MAVEN_HOME%\lib\jansi-native" ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%CLASSWORLDS_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
if ERRORLEVEL 1 goto error
goto end

这里干了四件事:

  1. 关联配置m2.conf
  2. 关联入口plexus-classworlds-*.jar及入口方法;
  3. 写入配置参数;
  4. 截取命令输入作为参数执行2中的方法。

m2.conf

1
2
3
4
5
6
7
8
main is org.apache.maven.cli.MavenCli from plexus.core

set maven.conf default ${maven.home}/conf

[plexus.core]
load ${maven.conf}/logging
optionally ${maven.home}/lib/ext/*.jar
load ${maven.home}/lib/*.jar

plexus-classworlds-*.jar

plexus-classworlds-*.jar逐行解析m2.conf配置项,主要干了三件事:

  1. 根据环境变量获取maven路径下lib的jar包,加载到当前的运行态中(URLClassLoader加载到URLClassPath中),包括maven-embedder-*.jar这个包,即maven执行命令MavenCli的入口;
  2. 据配置得到入口函数(MavenCli)及一些配置项,很难相信main is和from这种字符串能作为代码解析配置参数的标识;
  3. 结合输入命令截取的字符串作为入参,获取到的入口类通过反射调用其唯一默认的main方法,至此,maven成功进入命令解析并作出特异性插件行为阶段;

Maven插件路由映射

到了org.apache.maven.cli.MavenCli解析二级命令(即mvn之后的去前后空格字符串,如mvn deploy:deploy-file)并对应到相应的maven插件并指定该插件执行的goal,执行特定构建行为。

下面代码是执行mvn的核心方法,不难看出有一系列的子方法完善CliRequest类,它包含了执行插件所需要的所有参数,一条命令可能包含多条maven command line关键字,随着doMain方法的执行,关键字被依次解析,最终所有关键字都得以匹配,达到用户的意图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public int doMain( CliRequest cliRequest )
{
PlexusContainer localContainer = null;
try {
//完善工作路径workingDirectory,若为空则默认为当前用户路径,多模块项目路径multiModuleProjectDirectory
//若为空,则取maven.multiModuleProjectDirectory环境变量
initialize( cliRequest );
//完善commandLine,解析命令,匹配程序内置的命令-v, -h
//判断commandLine是否为-v或者-h,是就在控制台打印出相应的信息并退出
cli( cliRequest );
//将系统参数和用户参数写入到请求当中systemProperties以及userProperties属性的设置
//1.设置系统参数System.getenv(),一些系统级别的参数变量
//2.设置运行相关参数System.getProperties(),主要是java相关的属性
//3.设置maven构建的版本信息,主要从/org/apache/maven/messages/build.properties
// 里获取构建信息:在maven-core的messages包下获取,就是一些版本及发布者信息等
properties( cliRequest );
//完善执行请求中的日志属性配置,-x,-q,-e 设置MavenExecutionRequest日志级别,实际上是执行插件的日志级别
//设置打印日志的消息字体颜色是否启用,其底层是用org.fusesource.jansi.Ansi来控制多色输出;
//若命令中包含-b和-l部分,则控制台输出字体颜色为默认颜色
//对于-l file命令,重定向日志输出到file里,通过PrintStream将System日志写入,包含setOut及setErr;
logging( cliRequest );
//debug模式下解析解析到-V后打印版本信息
version( cliRequest );
//构建Maven.class,负责执行maven指定的构建行为,组装了一些参数
localContainer = container( cliRequest );
//打印一些设置信息是否开启,诸如错误日志开启,CHECKSUM_POLICY_FAIL是否开启
commands( cliRequest );
//EventSpyDispatcher初始化及设置,-s,-gs命令解析,设置用户配置文件及全局配置文件
configure( cliRequest );
//-t,-gt命令解析,设置toolchains
toolchains( cliRequest );
//1.校验将会废弃的maven命令,{ "up", "npu", "cpu", "npr" },并打印警告信息
//2. 解析-b(批处理),-nsu(进展快照更新),-N(不递归到子项目中),-ff(在构建响应堆中首次失败时停止构建)
//-fae(仅仅是当前失败的构建才会置为失败,其余不受影响的构建会继续),-fn(所有的构建失败都会被忽略,无论个中构建任务失败与否)[-ff,-fae,-fn是顺序检测取第一个]
//-o(设置线下操作标识),-U(标识是否更新快照)
//-C与-c(互斥出现),当checksum不匹配的时候,使用什么处理策略,默认是-c(警告)-C会使得checksum不匹配后使构建失败退出
//-P xxx,xxxx(用以引入配置文件list,以逗号间隔,这里不涉及配置文件的格式及解析过程)
//-ntp(在上传或者下载时不显示进度信息,这个在进度显示优先级最高),然后如果有-l xxxx.log等日志文件,
//就使用Slf4jMavenTransferListener监听并写入到日志文件,如果没有且日志级别为debug的,使用ConsoleMavenTransferListener监听进度
//-f xxxx/pom.xml,加载pom.xml文件到执行参数里,如果没有设置,默认为基础路径下的pom文件,如果有父文件则进行加载和引用
//-rf xxxxx(当构建失败后,重新执行从某个特定模块重新执行,选项后可跟随[groupId]:artifactId)
//-pl xxxxxx(project list,以逗号间隔多个模块的相对路径,或者模块以[groupId]:artifactId的形式表示),后面跟的每个参数项,会以+,-,!,或其他符号开始,+或其他符号为头标会被exclude,其余则被include
//-am xxx,-amd xxx,xxx表示模块,设置编译行为或者编译作用域,前者表示同时编译选定模块所依赖的模块(上游make),后者表示编译依赖选定模块的部分(下游make),根据实际需求选定,若没有指定,则make时会默认把上游和下游模块都加载进来
//依次在用户配置和系统配置(用户配置没有取到)里加载maven.repo.local,加载本地仓库路径到运行参数里
//-T xxx(设置构建并发的并行线程数,可以在数字之后跟C,表示这个线程数需要跟当前运行时jvm可用的processor数量相乘,即针对每个内核都派发那么多线程,通过Runtime.getRuntime().availableProcessors()获取)
//-b xxxxxx(指定构建器的id,默认是multithreaded构建器(builder))
populateRequest( cliRequest );
//-emp xxx(单独的工具,对master密码进行加密并打印),不参与构建过程
//-ep xxx(单独的工具,对服务器密码进行加密并打印),不参与构建过程
encryption( cliRequest );
//通过两种方式判断是否使用maven2老版本的本地库而不使用远程仓库:
//-llr(即legacy-local-repository,设置使用老版本参数)
//系统配置maven.legacyLocalRepo为true,则使用老版本maven库
repository( cliRequest );
//执行命令入参
return execute( cliRequest );
}
//省略catch语法
finally {
if ( localContainer != null ) {
localContainer.dispose();
}
}
}

用户的执行目标及需求配置确认并封装完成之后,就开始执行构建操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//1.设置构建的初始化属性
//2.校验本地依赖库的可访问性
//3.创建RepositorySystemSession
//4.创建MavenSession
//5.执行AbstractLifecycleParticipant.afterSessionStart(session)
//6.获取校验pom对象error的对象
//7.创建ProjectDependencyGraph用以调整--projects和reactor模式(确保所有传递到ReactorReader的项目仅仅是指定的项目)
//8.创建ReactorReader用以获取对象映射(getProjectMap( projects )),在获取的时候会对对象做唯一性校验,这些对象是从第6步中获取的对象集合
//9.执行AbstractLifecycleParticipant.afterProjectsRead(session)后置处理
//10.创建ProjectDependencyGraph,不用再调整,第7步已经做了这个工作,这里要完成对AbstractLifecycleParticipants的拓扑排序,可能会改变依赖进而影响构建顺序
//11.开始执行LifecycleStarter.start()生命周期开始
//=============================================================================
//上述为构建执行的准备步骤,接下来是构建的详细步骤,会涉及到获取maven插件(plugin)配置的获取以及插件goal的执行,直到每个插件执行自己的构建逻辑:
//1.每个插件的goal执行是基于某个生命周期运行的,这里是执行插件某个gaol的入口:org.apache.maven.lifecycle.internal.LifecycleStarter#execute(MavenSession)
//2.执行DefaultLifecycleTaskSegmentCalculator#calculateTaskSegments,获取配置插件对应的goal集合,如果找不到就会默认用MavenSession.getTopLevelProject().getDefaultGoal(),走默认maven构建(获得的构建任务放在TaskSegment里)
//3.根据2中得出的构建任务,计算得出构建对象集合,封装在ProjectBuildList里,实际上是做了一个映射,当前MavenSession的所有projects都必须执行TaskSegment集合里的任务
//4.获取执行参数里的builderId,分为两种,单例构建或多线程构建,默认是多线程,可以通过-T命令设置线程数量,然后通过指定构建器着手进行构建工作
//5.构建逻辑,单例构建遍历任务集合(TaskSegment)与构建对象集合ProjectBuildList逐一进行构建,多线程按照指定的线程数量作为上限进行并发构建
//6.一个项目(project)构建策略,计算得出当前项目的构建计划MavenExecutionPlan,统一Project中的PluginManagement与BuildPlugin(构建模块的版本),获得MojoExecution,

public void buildProject( MavenSession session, MavenSession rootSession, ReactorContext reactorContext,
MavenProject currentProject, TaskSegment taskSegment )
{
session.setCurrentProject( currentProject );
long buildStartTime = System.currentTimeMillis();

// session may be different from rootSession seeded in DefaultMaven
// explicitly seed the right session here to make sure it is used by Guice
sessionScope.enter( reactorContext.getSessionScopeMemento() );
sessionScope.seed( MavenSession.class, session );
try {
if ( reactorContext.getReactorBuildStatus().isHaltedOrBlacklisted( currentProject ) ) {
eventCatapult.fire( ExecutionEvent.Type.ProjectSkipped, session, null );
return;
}

BuilderCommon.attachToThread( currentProject );
projectExecutionListener.beforeProjectExecution( new ProjectExecutionEvent( session, currentProject ) );
eventCatapult.fire( ExecutionEvent.Type.ProjectStarted, session, null );
//获取构建执行计划
MavenExecutionPlan executionPlan =
builderCommon.resolveBuildPlan( session, currentProject, taskSegment, new HashSet<Artifact>() );
//通过执行计划得到执行器
List<MojoExecution> mojoExecutions = executionPlan.getMojoExecutions();
projectExecutionListener.beforeProjectLifecycleExecution( new ProjectExecutionEvent( session,
currentProject,
mojoExecutions ) );
//执行插件构建任务,其内部是循环执行mojoExecutions对应的执行器,
//具体逻辑是通过MavenPluginManager拿到插件对应的Mojo接口实例
//然后执行Mojo实例,由此执行扩展接口逻辑,得到插件提供的强大扩展能力
mojoExecutor.execute( session, mojoExecutions, reactorContext.getProjectIndex() );
long buildEndTime = System.currentTimeMillis();
projectExecutionListener.afterProjectExecutionSuccess( new ProjectExecutionEvent( session, currentProject,
mojoExecutions ) );
reactorContext.getResult().addBuildSummary( new BuildSuccess( currentProject,
buildEndTime - buildStartTime ) );
eventCatapult.fire( ExecutionEvent.Type.ProjectSucceeded, session, null );
}
catch ( Throwable t ) {
builderCommon.handleBuildError( reactorContext, rootSession, session, currentProject, t, buildStartTime );
projectExecutionListener.afterProjectExecutionFailure( new ProjectExecutionEvent( session, currentProject, t ) );
// rethrow original errors and runtime exceptions
if ( t instanceof RuntimeException ) {
throw (RuntimeException) t;
}
if ( t instanceof Error ) {
throw (Error) t;
}
}
finally {
sessionScope.exit();
session.setCurrentProject( null );
Thread.currentThread().setContextClassLoader( reactorContext.getOriginalContextClassLoader() );
}
}

//=================执行Mojo简要说明=========================
执行方法org.apache.maven.plugin.DefaultBuildPluginManager#executeMojo,调用Mojo(一个goal对应一个Mojo)能力,
执行org.apache.maven.plugin.MavenPluginManager#getConfiguredMojo获得Mojo实例
执行org.apache.maven.plugin.Mojo#execute执行扩展插件的实现,至此,构建脱离公共流程,
进入插件构建运行阶段

Maven插件分析

mvn deploy:deploy-file - mven-deploy-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Mojo( name = "deploy-file", requiresProject = false, threadSafe = true )
public class DeployFileMojo extends AbstractDeployMojo {
void initProperties()
throws MojoExecutionException
{
if ( pomFile != null ) {
processModel( readModel( pomFile ) );
}
else {
boolean foundPom = false;
JarFile jarFile = null;
try
{
Pattern pomEntry = Pattern.compile( "META-INF/maven/.*/pom\\.xml" );
jarFile = new JarFile( file );
Enumeration<JarEntry> jarEntries = jarFile.entries();

// ...

if ( !foundPom ) {
getLog().info( "pom.xml not found in " + file.getName() );
}
}
// ...
}
// ...
}

// 核心逻辑执行方法
public void execute()
throws MojoExecutionException, MojoFailureException {
// 省略...

// 对pom文件的处理
initProperties();

// deploymentRepository
ArtifactRepository deploymentRepository = createDeploymentArtifactRepository( repositoryId, url );
// deployableArtifacts
List<Artifact> deployableArtifacts = new ArrayList<Artifact>();

// 省略...

try {
// deploy
artifactDeployer.deploy( getSession().getProjectBuildingRequest(), deploymentRepository, deployableArtifacts );
} catch ( ArtifactDeployerException e ) {
throw new MojoExecutionException( e.getMessage(), e );
}
}
}