Maven源码阅读
Maven源码阅读
- 源码版本 3.8.3
- 下载地址
- jdk1.8
Main函数在哪里?
在apache-maven-3.8.3\apache-maven\src\bin\mvn的109行
108 CLASSWORLDS_JAR=`echo "${MAVEN_HOME}"/boot/plexus-classworlds-*.jar`
109 CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
org.codehaus.plexus.classworlds.launcher.Launcher(这个是我们梦开始的地方:main函数就在这里)
org.codehaus.plexus.classworlds.launcher.Launcher#main
public static void main( String[] args )
{
try
{
int exitCode = mainWithExitCode( args ); // TODO 核心方法
System.exit( exitCode );
}
catch ( Exception e )
{
e.printStackTrace();
System.exit( 100 );
}
}
如何启动?
说明: idea开发工具
JVM参数
说明: 具体参数可以参考idea启动参数
-Dclassworlds.conf=E:\#learning\apache-maven-3.8.3\apache-maven\src\bin\m2.conf -Dmaven.home=D:\apache-maven-3.8.3 -Dmaven.conf=D:\apache-maven-3.8.3\conf -Dclassworlds.conf=D:\apache-maven-3.5.4\bin\m2.conf -Dmaven.multiModuleProjectDirectory=E:\#adt\audit-trail-ms -Duser.dir=E:\#adt\audit-trail-ms
自己的疑惑:
maven.multiModuleProjectDirectory和user.dir参数有什么区别, 默认user.dir是我们源码工程路径maven.multiModuleProjectDirectory没有默认值,必须填写一个,我们mvn命令执行的路径是user.dir配置的, 为什么必须配置两个,这个地方还需要深入理解,有知道的伙伴评论区给出你的理解,谢谢!
程序参数示例
clean package
脚本启动的原理
maven本质是执行命令,因为maven构建框架是固定的,变化的是插件,因此一级命令(我们通常看到的mvn -v查看maven版本,就是这个mvn)是一致的:
一级命令取决于脚本文件的名称,毫无疑问,脚本文件的逻辑含义是maven核心之一,接下来深入看看这些脚本干了啥,以mvn.cmd为例(与mvn相比文件除了语法差异,其他并无差别)进行解析。
一次你完全可以修改mvn名称,改变命令: 示例 mvn -> mvn -副本
D:\apache-maven-3.8.3\bin>mvn - 副本 -v Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739) Maven home: D:\apache-maven-3.8.3 Java version: 1.8.0_151, vendor: Oracle Corporation, runtime: D:\Java\jdk1.8.0_151\jre Default locale: zh_CN, platform encoding: GBK OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"
命令行启动cmd命令
for %%i in ("%MAVEN_HOME%"\boot\plexus-classworlds-*) do set CLASSWORLDS_JAR="%%i" @REM 指定执行命令的jar set CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher "%JAVACMD%" ^ %JVM_CONFIG_MAVEN_PROPS% ^ %MAVEN_OPTS% ^ %MAVEN_DEBUG_OPTS% ^ -classpath %CLASSWORLDS_JAR% ^ @REM classworlds 配置文件地址 "-Dclassworlds.conf=%MAVEN_HOME%\bin\m2.conf" ^ @rem maven home "-Dmaven.home=%MAVEN_HOME%" ^ @rem jansi 的位置 "-Dlibrary.jansi.path=%MAVEN_HOME%\lib\jansi-native" ^ @rem 命令执行的位置, 默认是maven工程目录 "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ @rem 自动调用classworlds.launcher.Launcher的main方法, 并吧命令行参数传给main的args %CLASSWORLDS_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
脚本主要干了四件事
- 关联配置m2.conf;
- 关联入口plexus-classworlds-2.6.0.jar(去版本关联)及入口方法;
- 写入配置参数;
- 截取命令输入作为参数执行2中方法;
这里我们通过源码直接启动java代码方式调试代码
org.codehaus.plexus.classworlds.launcher.Launcher#main
启动的第一行代码, 梦开始的地方
mainWithExitCode
launcher.configure( is ); 加载配置
launcher.launch( args ); 梦想起航
public void launch( String[] args )
throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, NoSuchMethodException,
NoSuchRealmException
{
try
{
launchEnhanced( args ); // 增强启动, 主要看这个
return;
}
catch ( NoSuchMethodException e )
{
// ignore
}
launchStandard( args ); // 异常后使用标准启动
}
launchEnhanced: 增强启动
Object ret = mainMethod.invoke( mainClass, new Object[]{args, getWorld()} ); 反射调用
MavenCli#main(String[], ClassWorld): Maven 命令行
doMain: 核心流程
public int doMain(CliRequest cliRequest) {
PlexusContainer localContainer = null; // 丛容器
try {
// 以下注释来自: https://www.codenong.com/cs106269680/, 如有侵权联系删除
// 两个核心路径,都有默认值
// 完善工作路径workingDirectory,若为空则默认为当前用户路径,多模块项目路径 multiModuleProjectDirectory
// 若为空,则取maven.multiModuleProjectDirectory环境变量
// 这两个参数只能通过命令参数取设置,因为没有set方法和其他赋值地方
// maven.home解析绝对路径
initialize(cliRequest); // 初始化目录: TODO 重点看下
// 完善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); // 设置日志
informativeCommands(cliRequest); // 异常提示
//debug模式下解析解析到-V后打印版本信息
version(cliRequest); // 打印版本
//构建Maven.class,负责执行maven指定的构建行为,组装了一些参数
// maven = container.lookup(Maven.class); DefaultMaven后续处理pom会用到
localContainer = container(cliRequest); // 构建容器
//打印一些设置信息是否开启,诸如错误日志开启,CHECKSUM_POLICY_FAIL是否开启
commands(cliRequest);
//EventSpyDispatcher初始化及设置,-s,-gs命令解析,设置用户配置文件及全局配置文件
// 设置参数, 解析settings.xml, TODO 核心逻辑
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 (ExitException e) {
return e.exitCode; // exit, 系统退出
} catch (UnrecognizedOptionException e) {
// pure user error, suppress stack trace : 纯用户错误,抑制堆栈跟踪
return 1;
} catch (BuildAbort e) {
CLIReportingUtils.showError(slf4jLogger, "ABORTED", e, cliRequest.showErrors);
return 2;
} catch (Exception e) {
CLIReportingUtils.showError(slf4jLogger, "Error executing Maven.", e, cliRequest.showErrors);
return 1;
} finally {
if (localContainer != null) {
localContainer.dispose();
}
}
}
MavenCli#execute: maven执行的核心入口
- MavenCli#execute: maven
private int execute(CliRequest cliRequest) throws MavenExecutionRequestPopulationException { // 获取本地仓库 DefaultMavenExecutionRequestPopulator DefaultMavenExecutionRequest 填充器
MavenExecutionRequest request = executionRequestPopulator.populateDefaults(cliRequest.request);
eventSpyDispatcher.onEvent(request); // 请求事件通知
// TODO 核心执行逻辑
MavenExecutionResult result = maven.execute(request); // maven命令执行, 核心中的核心
eventSpyDispatcher.onEvent(result); // 结果时间通知
eventSpyDispatcher.close(); // 监控事件调度器关闭
}
- org.apache.maven.DefaultMaven#execute
org.apache.maven.DefaultMaven#doExecute
MavenExecutionResult result = new DefaultMavenExecutionResult();
org.apache.maven.DefaultMaven#doExecute
private MavenExecutionResult doExecute(MavenExecutionRequest request, MavenSession session,
MavenExecutionResult result, DefaultRepositorySystemSession repoSession) {
eventCatapult.fire(ExecutionEvent.Type.ProjectDiscoveryStarted, session, null); // 开始处理工程了 TODO主要是通知事件
// 构建pom
Result<? extends ProjectDependencyGraph> graphResult = buildGraph(session, result); // 所有pom
try {
session.setProjectMap(getProjectMap(session.getProjects())); // 设置项目地图: 项目的pom位置
} catch (DuplicateProjectException e) {
return addExceptionToResult(result, e);
}
WorkspaceReader reactorWorkspace;
try { // 反应堆, 获取反应堆
reactorWorkspace = container.lookup(WorkspaceReader.class, ReactorReader.HINT);
} catch (ComponentLookupException e) {
return addExceptionToResult(result, e);
}
// 设置工作空间阅读器
repoSession.setWorkspaceReader(
ChainedWorkspaceReader.newInstance(reactorWorkspace, repoSession.getWorkspaceReader()));
repoSession.setReadOnly();
graphResult = buildGraph(session, result); // todo: 核心 工程pom信息
try {
result.setProject(session.getTopLevelProject());
validatePrerequisitesForNonMavenPluginProjects(session.getProjects());
lifecycleStarter.execute(session); // todo: 核心, 生命周期构建
validateActivatedProfiles(session.getProjects(), request.getActiveProfiles());
} finally {
}
return result;
}
-
org.apache.maven.lifecycle.internal.LifecycleStarter#execute
builder.build( session, reactorContext, projectBuilds, taskSegments, reactorBuildStatus );
-
org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder#build
还有一个多线程的,类似
public void build(MavenSession session, ReactorContext reactorContext, ProjectBuildList projectBuilds, List<TaskSegment> taskSegments, ReactorBuildStatus reactorBuildStatus) { for (TaskSegment taskSegment : taskSegments) { for (ProjectSegment projectBuild : projectBuilds.getByTaskSegment(taskSegment)) { try { // todo: 核心 构建工程 lifecycleModuleBuilder.buildProject(session, reactorContext, projectBuild.getProject(), taskSegment); // 生命周期构建 if (reactorBuildStatus.isHalted()) { break; } } catch (Exception e) { break; // Why are we just ignoring this exception? Are exceptions are being used for flow control } } } }
-
org.apache.maven.lifecycle.internal.LifecycleModuleBuilder#buildProject
核心中的核心
注释参考: https://www.codenong.com/cs106269680/ 如有侵权联系删除
- 设置构建的初始化属性
- 校验本地依赖库的可访问性
- 创建RepositorySystemSession
- 创建MavenSession
- 执行AbstractLifecycleParticipant.afterSessionStart(session)
- 获取校验pom对象error的对象
- 创建ProjectDependencyGraph用以调整–projects和reactor模式(确保所有传递到ReactorReader的项目仅仅是指定的项目)
- 创建ReactorReader用以获取对象映射(getProjectMap( projects )),在获取的时候会对对象做唯一性校验,这些对象是从第6步中获取的对象集合
- 执行AbstractLifecycleParticipant.afterProjectsRead(session)后置处理
- 创建ProjectDependencyGraph,不用再调整,第7步已经做了这个工作,这里要完成对AbstractLifecycleParticipants的拓扑排序,可能会改变依赖进而影响构建顺序
- 开始执行LifecycleStarter.start()生命周期开始
//=============================================================================
//上述为构建执行的准备步骤,接下来是构建的详细步骤,会涉及到获取maven插件(plugin)配置的获取以及插件goal的执行,直到每个插件执行自己的构建逻辑:- 每个插件的goal执行是基于某个生命周期运行的,这里是执行插件某个goal的入口:org.apache.maven.lifecycle.internal.LifecycleStarter#execute(MavenSession)
- 执行DefaultLifecycleTaskSegmentCalculator#calculateTaskSegments,获取配置插件对应的goal集合,如果找不到就会默认用MavenSession.getTopLevelProject().getDefaultGoal(),走默认maven构建(获得的构建任务放在TaskSegment里)
- 根据2中得出的构建任务,计算得出构建对象集合,封装在ProjectBuildList里,实际上是做了一个映射,当前MavenSession的所有projects都必须执行TaskSegment集合里的任务
- 获取执行参数里的builderId,分为两种,单例构建或多线程构建,默认是多线程,可以通过-T命令设置线程数量,然后通过指定构建器着手进行构建工作
- 构建逻辑,单例构建遍历任务集合(TaskSegment)与构建对象集合ProjectBuildList逐一进行构建,多线程按照指定的线程数量作为上限进行并发构建
- 一个项目(project)构建策略,计算得出当前项目的构建计划MavenExecutionPlan,统一Project中的PluginManagement与BuildPlugin(构建模块的版本),获得MojoExecution,
public void buildProject(MavenSession session, MavenSession rootSession, ReactorContext reactorContext,
MavenProject currentProject, TaskSegment taskSegment) {
session.setCurrentProject(currentProject);
sessionScope.enter(reactorContext.getSessionScopeMemento());
sessionScope.seed(MavenSession.class, session);
try {
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()); // todo 核心:解析依赖
} catch (Throwable t) {
} finally {
}
}
-
org.apache.maven.lifecycle.internal.MojoExecutor#execute
- org.apache.maven.lifecycle.internal.MojoExecutor#execute
- org.apache.maven.lifecycle.internal.MojoExecutor#execute
mojo执行器