maven执行原理及插件解析

  • 1. 博文说明
  • 2. 命令的解析及插件路由映射
    • 2.1 谈谈mvn.cmd
    • 2.2 maven插件路由映射

1. 博文说明

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

本文着眼点在命令的解析及插件路由映射插件包如何被识别并如何执行常见的maven命令常见的插件及说明等四个方面分析。

2. 命令的解析及插件路由映射

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

图示已经做了不同平台下的mvn命令脚本,mvn被识别的入库,如何验证?图示的test.cmd说明了一切:

一级命令取决于脚本文件的名称,毫无疑问,脚本文件的逻辑含义是maven核心之一,接下来深入看看这些脚本干了啥,以mvn.cmd为例(与mvn相比文件除了语法差异,其他并无差别)进行解析。

2.1 谈谈mvn.cmd

如果在windows的环境变量中配置了MAVEN_HOME,并在path中添加了maven目录下的bin/,那么在任意命令窗口里输入mvn就能触发该脚本文件执行,前段进行了一些通用的参数校验及参数设置,如JAVA_HOME,MAVEN_HOME,JAVACMD,执行命令入参MAVEN_CMD_LINE_ARGS设置等核心部分见下图:

这里干了四件事:

  1. 关联配置m2.conf;
  2. 关联入口plexus-classworlds-2.6.0.jar(去版本关联)及入口方法;
  3. 写入配置参数;
  4. 截取命令输入作为参数执行2中方法;

m2.conf配置如下:

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逐行解析m2.conf配置项,主要干了三件事:

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

2.2 maven插件路由映射

经过2.1的阐述,到了org.apache.maven.cli.MavenCli解析二级命令(即mvn之后的去前后空格字符串)并对应到相应的maven插件并指定该插件执行的goal,执行特定构建行为。接下来会说明其执行过程,可能会伴随一些代码的展示。
下面代码是执行mvn的核心方法,不难看出有一系列的子方法完善CliRequest类,它包含了执行插件所需要的所有参数,一条命令可能包含多条maven command line关键字,随着doMain方法的执行,关键字被依次解析,最终所有关键字都得以匹配,达到用户的意图:

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命令解析,设置toolchainstoolchains( 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.创建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 GuicesessionScope.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 exceptionsif ( 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执行扩展插件的实现,至此,构建脱离公共流程,
进入插件构建运行阶段

由上面的执行流程可以看出,插件包只要自己实现Mojo接口再加上适当的maven配置就可以被扩展,逻辑上来说如此,实际上肯定略显复杂,复杂的东西留给幕后,spring-boot-maven-plugin,这是常用且典型的构建插件。

//org.apache.maven.plugins.annotations.Mojo
//这个注解是作用于Mojo上面用于标注执行熟悉,当然,这个类必须继承自
//org.apache.maven.plugin.AbstractMojo这个类,用于被maven识别到
//具体解析过程是在maven启动时加载进入插件管理器的
@Mojo(//定义goal的名称,这个就是springboot重打包goalname = "repackage",//生命周期的枚举,具体有哪些可参考下面的枚举org.apache.maven.plugins.annotations.LifecyclePhasedefaultPhase = LifecyclePhase.PACKAGE,requiresProject = true, threadSafe = true,requiresDependencyResolution =    ResolutionScope.COMPILE_PLUS_RUNTIME,requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractDependencyFilterMojo {//省略中间实现
}

上述就是spring-boot-plugin执行重新打包操作的Mojo,自定义的插件在被安装之后,maven启动时就会把响应的插件Mojo实现加载到插件管理器里,执行的时候,通过命令前缀和goal两个维度定位到具体的执行Mojo,再把其从插件管理器里取出来,施展相应的扩展能力。

原创博文,理解有误之处欢迎留言,本着共同学习,不断迭代的学习态度,博文会持续更新。

maven执行原理及插件解析相关推荐

  1. wireshark协议解析器原理与插件编写

    工作原理 每个解析器解码自己的协议部分, 然后把封装协议的解码传递给后续协议. 因此它可能总是从一个Frame解析器开始, Frame解析器解析捕获文件自己的数据包细节(如:时间戳), 将数据交给一个 ...

  2. Maven 核心原理

    Maven 核心原理 标签 : Java基础 Maven 是每一位Java工程师每天都会接触的工具, 但据我所知其实很多人对Maven理解的并不深, 只把它当做一个依赖管理工具(下载依赖.打包), M ...

  3. Skywalking光会用可不行,必须的源码分析分析 - Skywalking Agent 插件解析

    3 Skywalking源码导入 接上文,已经学习了Skywalking的应用,接下来我们将剖析Skywalking源码,深度学习Skywalking Agent. 3.1 源码环境搭建 当前最新版本 ...

  4. Dubbo原理和源码解析之服务引用

    github新增仓库 "dubbo-read"(点此查看),集合所有<Dubbo原理和源码解析>系列文章,后续将继续补充该系列,同时将针对Dubbo所做的功能扩展也进行 ...

  5. Java MyBatis的介绍及其执行原理

    写在前面 ??MyBatis学习 ??今天我们进行MyBatis框架的学习,认识MyBatis及其执行原理,感谢你的阅读,内容若有不当之处,希望大家多多指正,一起进步!!! 如果觉得博主文章还不错,可 ...

  6. 第六节:Maven生命周期和插件

    生命周期详解: Maven的生命周期就是对项目构建过程进行的抽象和统一,就是项目构建的流程.但是构建过程中每一步(例如编译源代码)的实际行为都由插件来完成的. Maven的生命周期不是一个整体,它拥有 ...

  7. maven运行原理分析,源码分析

    maven启动脚本mvn.bat,借助于Plexus容器启动,Plexus提供完整的软件栈,用于创建和执行软件项目,是IoC框架,和spring类似 有兴趣想了解Plexus的,可以在github上下 ...

  8. Maven 生命周期和插件详解

    Maven 生命周期及其阶段 Maven 基于生命周期的核心概念.有三个内置的生命周期: clean:负责清理项目: default:负责构建项目: site:负责建立项目站点. 每个生命周期都包含一 ...

  9. 视觉SLAM开源算法ORB-SLAM3 原理与代码解析

    来源:深蓝学院,文稿整理者:何常鑫,审核&修改:刘国庆 本文总结于上交感知与导航研究所科研助理--刘国庆关于[视觉SLAM开源算法ORB-SLAM3 原理与代码解析]的公开课. ORB-SLA ...

最新文章

  1. 【日常分享1】三步,有效去除网页广告,完美过滤视频广告
  2. leetcode算法题--Binary Tree Paths
  3. android第三次作业
  4. html怎么给code标签添加语言,html code标签怎么用?html code标签的作用解释
  5. 品牌的mysql数据库监控_zabbix实现mysql数据库的监控(四)
  6. java中static关键字的理解(转载)
  7. 【0x40 数据结构进阶 例题】银河英雄传说【带权并查集】
  8. 什么是DVD?DVD有些格式?
  9. java源码社团管理系统_基于jsp的社团管理系统-JavaEE实现社团管理系统 - java项目源码...
  10. 邮件服务器的功能以及相关工作原理
  11. 《Steam平台上的VR虚拟现实》(Yanlz+Unity+XR+VR+AR+MR+Steam+SteamVR+Vive+Oculus+Valve+立钻哥哥+==)
  12. 正则表达式JS-1212
  13. django中request对象的属性和方法
  14. 计算机网络第六章 链路层和局域网
  15. 程序猿口中的hook是什么意思?
  16. 手机kakao聊天能自动翻译 WhatsApp翻译 实时翻译
  17. 于飞SEO:2020最新最有效的18种推广方式
  18. 嵌入式系统概念以及嵌入式基础知识
  19. Mac苹果电脑调整磁盘区域大小
  20. ESP32-总体理解

热门文章

  1. 【算法】零钱换整钱*
  2. 【java面试题】录入一篇文章,统计文章的标点符号或者某个词语出现的次数
  3. 段子PK秀:古代人打仗也这么自恋?
  4. 微信小程序使用WeUi框架教程,真·简单
  5. 一文读懂ELN的核心价值以及与LIMS的区别
  6. org.apache.commons.lang3.StringUtils.isNotBlank和isEmpty方法
  7. Python爬取网页所有小说
  8. mysql max()函数和min函数
  9. SSM中使用redis做中间缓存,详细注释,代码可以运行
  10. 请领导过目文件怎么说_越级汇报后,直属领导知道了,到处说我不懂规矩批评我,怎么办?...