第三章:Maven高级篇 — 插件开发与多模块工程
目标理解 Maven 插件体系、多模块聚合、继承、Reactor 构建顺序与质量插件能够开发一个可运行的自定义 Maven 插件。目录Maven 插件体系常用核心插件自定义插件开发多模块工程设计Reactor 机制版本管理与打包策略代码质量集成实战 Demomaven-demo-plugin专家面试题1. Maven 插件体系Maven 本身只定义生命周期和项目模型真正执行编译、测试、打包、部署的是插件。mvn package ↓ default lifecycle ↓ compile phase - maven-compiler-plugin:compile test phase - maven-surefire-plugin:test package phase - maven-jar-plugin:jar 或 spring-boot-maven-plugin:repackageGoal 与 Phase 映射概念含义示例Plugin插件一组构建能力maven-compiler-pluginGoal插件中的一个目标compile、test、repackagePhase生命周期阶段compile、test、packageExecution把 Goal 绑定到 Phase 的配置default-compile手动执行 Goalmvn dependency:tree mvn help:effective-pom绑定 Goal 到生命周期plugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-source-plugin/artifactIdexecutionsexecutionidattach-sources/idphaseverify/phasegoalsgoaljar-no-fork/goal/goals/execution/executions/plugin2. 常用核心插件插件作用常见配置点maven-compiler-plugin编译 Java 源码release、source、targetmaven-surefire-plugin运行单元测试includes、并发、跳过测试maven-failsafe-plugin运行集成测试integration-test、verifymaven-jar-plugin打 JARmanifest、classifiermaven-source-plugin生成源码包发布 SDK 必备maven-shade-plugin打 Fat JARrelocate、filtersmaven-assembly-plugin自定义分发包zip/tar、文件布局maven-enforcer-plugin规则约束Java/Maven 版本、依赖收敛父 POM 建议用pluginManagement统一插件版本pluginManagementpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.13.0/versionconfigurationrelease17/release/configuration/plugin/plugins/pluginManagement注意pluginManagement只管理默认配置不会自动执行插件。要执行插件仍需在build.plugins中声明或由 Maven 默认生命周期绑定。3. 自定义插件开发Maven 插件本质是一个特殊 JARpackagingmaven-plugin里面包含一个或多个 Mojo。插件 POMartifactIddemo-maven-plugin/artifactIdpackagingmaven-plugin/packagingdependenciesdependencygroupIdorg.apache.maven/groupIdartifactIdmaven-plugin-api/artifactIdversion3.9.8/versionscopeprovided/scope/dependencydependencygroupIdorg.apache.maven.plugin-tools/groupIdartifactIdmaven-plugin-annotations/artifactIdversion3.12.0/versionscopeprovided/scope/dependency/dependenciesMojo 代码Mojo(nameversion-check,defaultPhaseLifecyclePhase.VALIDATE,threadSafetrue)publicclassVersionCheckMojoextendsAbstractMojo{Parameter(propertydemo.requiredJavaVersion,defaultValue17)privateintrequiredJavaVersion;Parameter(propertyjava.version,readonlytrue)privateStringactualJavaVersion;Overridepublicvoidexecute()throwsMojoExecutionException{getLog().info(Checking Java version.);}}注解说明注解作用Mojo声明插件目标名称、默认阶段、线程安全Parameter从 POM、命令行、系统属性注入参数defaultPhase插件 Goal 默认绑定的生命周期阶段threadSafe是否支持 Maven 并行构建4. 多模块工程设计Maven 多模块有两个容易混淆的概念聚合和继承。聚合 Aggregator父工程通过modules声明子模块modulesmodulemaven-demo-bom/modulemodulemaven-demo-api/modulemodulemaven-demo-core/modulemodulemaven-demo-plugin/modulemodulemaven-demo-web/module/modules作用在父目录执行一次mvn packageMaven 会构建所有模块。继承 Inheritance子模块通过parent继承父 POMparentgroupIdcom.example.maven.demo/groupIdartifactIdmaven-demo/artifactIdversion1.0.0-SNAPSHOT/version/parent作用继承版本、插件管理、属性、Profile 等。聚合和继承可以分离企业项目里常见三种形态形态用法同一个父 POM 同时聚合和继承中小项目最常见只继承不聚合多仓库项目共享公司 Parent只聚合不继承临时批量构建多个独立模块5. Reactor 机制Reactor 是 Maven 多模块构建调度器。它会根据模块依赖关系计算构建顺序而不只是按modules的文本顺序。本 Demo 依赖关系maven-demo-api ↓ maven-demo-core ↓ maven-demo-web maven-demo-plugin 独立构建用于演示插件开发 maven-demo-bom 是版本清单模块常用局部构建命令# 只构建 web 以及它依赖的模块mvn-plmaven-demo-web-ampackage# 从 core 继续构建下游模块mvn-plmaven-demo-core-amdpackage# 跳过测试mvn-plmaven-demo-web-ampackage-DskipTests# 并行构建mvn-T1C clean package参数说明参数含义-pl/--projects指定构建模块-am/--also-make同时构建该模块依赖的上游模块-amd/--also-make-dependents同时构建依赖该模块的下游模块-rf/--resume-from从失败模块继续构建6. 版本管理与打包策略versions-maven-plugin查看依赖可升级版本mvn versions:display-dependency-updates mvn versions:display-plugin-updates批量修改项目版本mvn versions:set-DnewVersion1.1.0-SNAPSHOT mvn versions:commitFat JARFat JAR 把应用和依赖打进一个 JAR适合命令行工具或非 Spring Boot 应用。常用maven-shade-pluginplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-shade-plugin/artifactIdversion3.5.3/version/pluginSpring Boot 应用通常使用spring-boot-maven-plugin:repackage生成可执行 JAR不需要自己用 Shade 打所有依赖。WARWAR 适合部署到外部 Servlet 容器。现代 Spring Boot 项目更多采用可执行 JAR 和容器镜像。7. 代码质量集成企业 Maven 构建不应只停留在“能打包”还要做质量门禁。工具作用推荐阶段Checkstyle代码风格validatePMD静态代码规则verifySpotBugs字节码缺陷扫描verifyJaCoCo覆盖率test/verifyOWASP Dependency Check依赖漏洞CI 定时或发布前EnforcerJDK/Maven/依赖规则validate本 Demo 父 POM 已配置 Enforcer强制 Java 17 和 Maven 3.9。8. 实战 Demomaven-demo-plugin目标开发一个自定义 Maven 插件version-check用于检查当前 Java 版本是否满足要求。构建插件cdmaven-demo mvn-plmaven-demo-plugin cleaninstall执行插件mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check-Ddemo.requiredJavaVersion17预期输出[INFO] Checking Java version. required17, actual22.0.1 [INFO] BUILD SUCCESS如果要求 Java 99mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check-Ddemo.requiredJavaVersion99预期失败Java 99 is required, but current version is 22.0.1插件开发关键点插件模块必须使用packagingmaven-plugin。maven-plugin-plugin:descriptor会生成插件描述符。插件参数通过Parameter(property ...)支持命令行传入。插件逻辑应尽量拆成普通 Java 类方便单元测试。9. 专家面试题Q1Maven 插件和生命周期是什么关系答生命周期定义阶段顺序插件提供具体执行能力。Phase 本身不做事必须由一个或多个插件 Goal 绑定后才有行为。例如compile阶段通常绑定maven-compiler-plugin:compile。用户既可以执行生命周期阶段也可以直接执行某个插件 Goal。Q2pluginManagement为什么配置了插件但不执行答pluginManagement的设计目标是统一插件版本和默认配置类似dependencyManagement。它不会把插件自动加入构建执行列表。真正执行插件需要放到build.plugins中或由 Maven 默认生命周期根据 packaging 自动绑定。Q3Reactor 为什么能按依赖关系构建而不是完全按 modules 顺序答Maven 在多模块构建时会收集所有 Reactor 项目读取模块间依赖关系、插件依赖和构建扩展然后进行拓扑排序。这样即使web写在前面只要它依赖coreMaven 也会先构建core。不过模块声明顺序仍会影响没有依赖关系的模块顺序。Q4自定义 Maven 插件开发时为什么插件 API 依赖通常是 provided答插件运行在 Maven 自己的插件容器中Maven 运行时已经提供maven-plugin-api。如果把它打进插件产物可能导致类加载冲突和版本不一致。插件开发应只把自身真正需要的第三方运行库打包进去。10. 高级篇扩展核查插件、多模块与构建工程化10.1 插件前缀解析机制当执行mvn dependency:treeMaven 并不是直接知道dependency是什么插件而是根据插件组和元数据解析dependency - maven-dependency-plugin - org.apache.maven.plugins:maven-dependency-plugin自定义插件在未发布插件前缀元数据前推荐使用完整坐标mvn com.example.maven.demo:demo-maven-plugin:1.0.0-SNAPSHOT:version-check这样最稳定也最适合文档和 CI。10.2 插件描述符Maven 插件必须包含插件描述符META-INF/maven/plugin.xml它记录插件有哪些 Goal。Goal 对应哪个 Mojo 类。参数名称、类型、默认值。是否线程安全。默认生命周期阶段。本 Demo 中由maven-plugin-plugin:descriptor生成。验证cdmaven-demo mvn-plmaven-demo-plugin package jar tf maven-demo-plugin/target/demo-maven-plugin-1.0.0-SNAPSHOT.jar|grepplugin.xml10.3 聚合 Mojo 与普通 Mojo普通 Mojo 会对 Reactor 中每个模块执行一次。聚合 Mojo 通常只在根项目执行一次用于生成聚合报告、统一检查或发布。类型执行范围例子普通 Mojo每个模块当前version-check聚合 Mojo根项目一次聚合依赖报告、聚合覆盖率如果插件会扫描整个 Reactor应该谨慎设计为聚合 Mojo避免每个模块重复执行。10.4 插件参数设计原则好的插件参数应满足原则示例有明确默认值defaultValue 17支持命令行覆盖property demo.requiredJavaVersion不把只读系统属性暴露为可配置项${java.version}只读注入错误信息可行动告诉用户当前值和期望值线程安全不写共享可变状态当前 Demo 的VersionCheckMojo体现了这些点。10.5 Reactor 构建故障恢复大型多模块项目构建失败后不必每次从头开始。mvn clean package# 假设 maven-demo-web 失败mvn-rf:maven-demo-web package如果失败模块依赖的上游模块也修改了应使用mvn-plmaven-demo-web-ampackage参数经验场景命令只测某模块mvn -pl module test模块依赖上游也要构建mvn -pl module -am test改了底层模块要构建下游mvn -pl module -amd test从失败模块继续mvn -rf :module package10.6 多模块边界设计模块不是越多越好。拆模块要有明确边界。拆分理由是否推荐API 与实现分离推荐不同部署单元推荐不同发布节奏推荐只是按包名机械拆分不推荐为了看起来复杂不推荐本 Demoapi - 对外契约 core - 业务逻辑 cli - 命令行入口和 Shade 打包 web - HTTP 入口和 Spring Boot 打包 plugin - 构建扩展 bom - 版本清单10.7 Shade 打包实战新增maven-demo-cli演示 Fat JAR。构建cdmaven-demo mvn-plmaven-demo-cli-ampackage运行java-jarmaven-demo-cli/target/maven-demo-cli-1.0.0-SNAPSHOT.jar Maven预期Hello, Maven environmentlocal version1.0.0-SNAPSHOTShade 常见用途命令行工具。Spark/Flink 作业。独立运行的批处理任务。需要 relocate 避免依赖冲突的 SDK。Spring Boot Web 应用通常不需要 Shade因为spring-boot-maven-plugin已经会生成可执行 JAR。10.8 relocate 解决依赖冲突当你开发 SDK内部依赖了某个容易冲突的库可以用 Shade relocaterelocationsrelocationpatterncom.google.common/patternshadedPatterncom.example.shaded.guava/shadedPattern/relocation/relocations风险反射引用可能失效。SPI 文件需要合并。日志和配置文件可能需要 transformer。所以 relocate 不是常规项目的默认选择而是 SDK 或平台组件的冲突隔离手段。10.9 质量插件实践本 Demo 增加了qualityProfilecdmaven-demo mvn-Pqualityverify质量门禁建议分层层级工具目标基础Checkstyle风格和低级错误缺陷SpotBugs字节码缺陷规范PMD代码规则测试Surefire/Failsafe单元和集成测试覆盖率JaCoCo分支和行覆盖率安全OWASP/CycloneDX漏洞和 SBOM10.10 高级篇新增面试题Q5为什么 Maven 插件 artifactId 不建议命名为maven-xxx-plugin答maven-xxx-plugin命名形式保留给 Apache Maven 官方插件。第三方插件推荐使用xxx-maven-plugin或业务语义更明确的名称。本 Demo 使用模块目录maven-demo-plugin但插件 artifactId 使用demo-maven-plugin避免官方保留命名警告。Q6Shade 和 Spring Boot repackage 的区别是什么答Shade 会把依赖 class 合并进一个 JAR必要时还能 relocate 包名Spring Boot repackage 会把依赖以嵌套 JAR 的形式放进BOOT-INF/lib由 Spring Boot Loader 加载。普通命令行工具适合 ShadeSpring Boot 应用适合 repackage。Q7多模块项目如何避免模块之间循环依赖答先定义清晰方向API 被 core 依赖core 被 web/cli 依赖入口模块不能反向被 core 依赖。出现循环依赖通常说明边界错误应抽取公共契约或公共工具模块而不是用插件或 scope 绕过去。