
本文详解如何在 Maven 多模块构建中,通过自定义插件将各子模块生成的文件汇总后,作为单个附属构件(attached artifact)发布到根项目的最终坐标下,并确保其正确出现在本地仓库(.m2)及远程仓库中。
本文详解如何在 maven 多模块构建中,通过自定义插件将各子模块生成的文件汇总后,作为单个附属构件(attached artifact)发布到根项目的最终坐标下,并确保其正确出现在本地仓库(`.m2`)及远程仓库中。
在 Maven 多模块项目中,直接在非根模块(如最后一个子模块)中调用 projectHelper.attachArtifact(mavenSession.getTopLevelProject(), ...) 试图向已构建完成的根项目附加构件,是无效的——因为 Maven 的构件生命周期严格绑定于当前正在构建的 MavenProject 实例。一旦根项目完成 install 阶段,其 project.getAttachedArtifacts() 已冻结,后续无法动态追加;attachArtifact() 方法仅影响当前 project 对象的内存状态,不会反向更新已安装到本地仓库的 .pom 或主构件元数据。
✅ 正确解法:采用「占位-替换」策略,配合 install 阶段执行:
-
统一在
install阶段执行插件(而非compile),确保所有模块编译、打包、安装均已完成; - 在根项目构建时(第一个 reactor 模块)创建并附加一个空占位文件,使其随根项目一同安装至本地仓库;
-
在最后一个子模块构建时(最后 reactor 模块),定位该占位文件在本地仓库中的物理路径(如
~/.m2/repository/group/id/version/id-version-doc-classifier.html),然后用聚合生成的真实文件覆盖它。
以下是关键实现逻辑示例(需在 Mojo 中注入 MavenProject, MavenSession, ProjectBuilder, RepositorySystem 等组件):
private void attachPlaceholderToRoot() throws MojoExecutionException {
MavenProject root = mavenSession.getTopLevelProject();
File placeholder = new File(root.getBuild().getDirectory(), "placeholder.html");
try {
Files.createDirectories(placeholder.getParentFile().toPath());
Files.write(placeholder.toPath(), "<!-- placeholder -->".getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new MojoExecutionException("Failed to create placeholder", e);
}
projectHelper.attachArtifact(root, "html", "doc-classifier", placeholder);
}
private void replacePlaceholderWithAggregatedFile() throws MojoExecutionException {
MavenProject root = mavenSession.getTopLevelProject();
String groupId = root.getGroupId().replace('.', '/');
String artifactId = root.getArtifactId();
String version = root.getVersion();
String fileName = String.format("%s-%s-%s-doc-classifier.html", artifactId, version, version);
// 构建本地仓库中目标文件路径:${localRepo}/${groupId}/${artifactId}/${version}/${fileName}
String repoPath = String.join("/",
mavenSession.getLocalRepository().getBasedir(),
groupId,
artifactId,
version,
fileName
);
File targetFile = new File(repoPath);
// 确保目录存在并写入真实聚合内容(例如从各 module/target/xxx.html 合并)
File aggregatedFile = generateAggregatedHtml(); // 自定义方法:读取所有子模块 output 并合并
try {
Files.createDirectories(targetFile.getParentFile().toPath());
Files.copy(aggregatedFile.toPath(), targetFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
getLog().info("✅ Successfully replaced placeholder with aggregated artifact: " + targetFile);
} catch (IOException e) {
throw new MojoExecutionException("Failed to replace placeholder artifact", e);
}
}⚠️ 注意事项:
-
切勿在
compile或package阶段尝试附加根项目构件——此时根项目早已构建完毕,attachArtifact()无实际效果; -
禁止删除已安装的构件再重装(如
mvn clean install中的 clean 会清空.m2,但运行时手动删.jar/.pom会导致仓库不一致,破坏 Artifactory/Nexus 的校验与部署流程); - 占位文件必须与最终产物扩展名、classifier、GAV 坐标完全一致,否则替换后 Maven 元数据(POM)与实际文件不匹配,引发依赖解析失败;
- 若需支持远程仓库(如 Nexus),应额外集成
Wagon或使用maven-deploy-plugin的 API 进行安全上传(本方案默认适用于本地聚合+人工同步场景)。
通过该模式,你只需维护一个插件、一套配置,即可在任意多模块项目中可靠地生成并发布面向根项目的聚合文档/报告/清单等附属产物,真正实现“一次声明、全局生效”的工程化实践。


















