ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Maven多模块构建目录未创建异常排查与解决方案

Maven多模块构建目录未创建异常排查与解决方案 Maven 多模块项目构建报“目录未创建”这类异常我踩过的次数两只手数不过来。说句实在话这个错在日志里往往只有一行路径但背后的原因能绕晕一多半刚上手 Maven 的人。文件系统里那个目录明明存在构建却一口咬定“不存在”或者目录确实还没生成但你不明白它为什么没生成。今天把这套排查逻辑和解决手段完整整理出来适合所有在用 Maven 管理多模块工程的人尤其是那些经常碰Reactor Build Order、antrun、build-helper以及跨模块文件拷贝场景的团队。1. 异常现象与最典型的触发场景1.1 报错长什么样先学会看日志先说现象。Maven 多模块构建时目录未创建的报错常见表现形式有这几种[ERROR] Failed to execute goal org.apache.maven.plugins:maven-antrun-plugin:3.1.0:run (create-dir) on project module-b: An Ant BuildException has occurred: Warning: Could not find file /xxx/module-a/target/generated/web to copy. [ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.1.0:exec (generate-web) on project module-c: Command execution failed: org.apache.commons.exec.ExecuteException: Process exited with an error: 2 (Exit value: 2)还有一类是在maven-resources-plugin拷贝资源时直接报[ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.3.1:copy-resources (copy-web-resources) on project module-web: The parameter outputDirectory is missing or invalid: /xxx/module-web/target/classes/static/web如果你看到日志里出现了Could not find file、does not exist、is missing or invalid这类字眼而且路径指向的是某个模块的target目录或自定义输出目录那大概率就是我今天要聊的“目录未创建”问题。1.2 最容易踩坑的三个场景根据我实际见过的项目这种异常集中爆发在下面三类场景里。第一类是跨模块引用生成产物。比如模块 A 通过代码生成器OpenAPI Generator、ANTLR、JAXB 等在target/generated-sources或target/generated下产出文件模块 B 在构建时想直接读取这些文件却没有通过依赖声明而是用绝对路径或相对路径去引用。第二类是自定义插件脚本假设目录存在。很多老项目用maven-antrun-plugin或exec-maven-plugin执行 shell 脚本、复制静态资源、打压缩包脚本里默认某个目录已经在磁盘上比如/target/classes/static。如果 nobody 负责提前创建这个目录构建到这一步就会直接炸掉。第三类是清理后重新构建的时序问题。执行mvn clean install时clean阶段把所有 target 目录删了后续某个插件直接往target/xxx/yyy写文件但该插件不负责创建父目录也没有绑定任何显式创建目录的任务。目录没了文件自然无处安放。注意这三种场景的根因不完全一样排查路径也不同。第一种多半是构建顺序问题第二种是生命周期绑定的阶段问题第三种纯粹是插件本身不会自动建目录。后面我会分别说怎么解。2. 排查思路先判断是“没创建”还是“没找对”2.1 第一步确认物理目录到底存不存在遇到报错先别急着改 pom。用最简单直接的方式确认去目标模块下ls一下那个路径或者用find全盘找。比如ls -la module-a/target/generated find . -type d -name generated这一步能快速区分两种情况目录真的不存在或者目录存在但构建时用的路径不对。如果目录存在但你构建时报不存在那问题出在路径解析上常见原因是插件工作目录是某个模块目录但你用的是相对路径比如target/generated可此时当前目录是module-b实际找的是module-b/target/generated。路径拼接用了basedir但是拼接错误比如${project.basedir}/../module-a/target在子模块里解析出来的路径不是你想象的那个。模块 A 和模块 B 的构建工作目录在 reactor 里被切换了某些插件会改变user.dir。如果目录真的不存在那重点就要转移到“这个目录应该由谁在什么时候创建”。2.2 第二步看构建顺序确认模块执行先后多模块项目里最容易忽略的信息就是 Maven 启动时打印的Reactor Build Order。这一小段日志直接决定目录创建的先后顺序。执行构建时注意看这段[INFO] Reactor Build Order: [INFO] [INFO] demo-parent [pom] [INFO] module-a [jar] [INFO] module-b [jar] [INFO] module-web [war]如果你发现“生成目录的模块”排在了“读取目录的模块”后面那不用往下查了根因基本就是 reactor 顺序不对。构建顺序由 Maven 根据模块间的依赖关系自动计算被依赖的模块排在前面依赖别人的模块排在后面。但如果两个模块之间没有显式的dependency声明顺序就退回到pom.xml里modules的声明顺序。很多人就在这栽了模块 B 引用模块 A 的文件但 B 没有在 pom 里声明依赖 AA 又写在 B 后面于是 B 先构建A 还没执行目录自然不存在。2.3 第三步用调试参数定位具体执行阶段想确认“目录是在哪个生命周期阶段缺失的”最有效的办法是用-X开启调试日志或者至少在命令行加-Dorg.slf4j.simpleLogger.defaultLogLeveldebug。构建时把日志保存下来mvn clean install -X build-debug.log 21然后在日志里搜索Generating、Copying、executing这些关键词重点看目标插件是在哪个phase触发的。Maven 默认生命周期的阶段顺序是固定的你要检查的关键点是创建目录的插件和读取目录的插件分别绑定在哪个阶段绑定的顺序是否合理。3. 根因解析Maven 多模块构建的顺序与目录生成机制3.1 reactor 构建顺序的核心逻辑Maven 多模块项目之所以叫 reactor 构建是因为它在内存里构建了一张依赖图把所有参与构建的模块按拓扑排序形成执行顺序。这张图的边来自每个模块的dependency声明。换句话说你想让模块 A 的输出目录先被建好、再让模块 B 读取最简单可靠的办法不是在脚本里等待、sleep而是让 B 在 pom 中显式依赖 Adependency groupIdcom.demo/groupId artifactIdmodule-a/artifactId version${project.version}/version /dependency这样 Maven 在计算 reactor 顺序时会把 A 排在 B 前面A 的标志性产物——比如target目录下的文件——才会在 B 开始读取前就位。如果两个模块之间没有任何依赖关系Maven 就按modules里的书写顺序执行。这带来一个隐性问题你调整了模块列表的顺序可能就会导致目录生成的先后变化而构建结果也随之改变。很多团队在整合代码时不小心调整了modules里模块的顺序第二天 CI 就开始报“目录未创建”原因就在这里。3.2 “同阶段交错执行”带来的隐藏坑这里要讲一个很多人没注意到的细节。在多模块执行过程中如果两个模块的插件绑定在同一个生命周期阶段它们的执行顺序是严格按照 reactor 顺序来的但不会等前一个模块跑完整个生命周期再跑下一个模块而是按阶段交错推进。说具体一点当构建推进到generate-resources阶段时Maven 会依次执行所有模块中绑定在该阶段的插件然后再统一推进到下一个阶段。这种机制带来的后果是假如模块 B 有一个插件在generate-resources阶段读取模块 A 在process-resources阶段才生成的文件那么无论 A 在 reactor 顺序里排多前B 都会在 A 还没生成文件之前就尝试读取照样报目录不存在。所以你不仅要看模块顺序还得看插件绑定的阶段顺序。3.3 一个最小复现案例建议照着实验为了方便理解我写一个最小的复现工程。父 pom 声明两个模块modules modulemodule-a/module modulemodule-b/module /modules模块 A 的 pom 里用一个exec-maven-plugin在generate-resources阶段创建文件plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.1.0/version executions execution idgenerate-file/id phasegenerate-resources/phase goals goalexec/goal /goals configuration executablemkdir/executable arguments argument-p/argument argument${project.basedir}/target/generated/argument /arguments /configuration /execution /executions /plugin模块 B 在generate-resources阶段用 antrun 去复制 A 生成的目录plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId version3.1.0/version executions execution idcopy-dir/id phasegenerate-resources/phase goals goalrun/goal /goals configuration target copy todir${project.basedir}/target/merged fileset dir${project.basedir}/../module-a/target/generated / /copy /target /configuration /execution /executions /plugin在这个配置下即使 A 排在 B 前面两者都在generate-resources阶段执行B 的 antrun 也会在 A 的 exec 执行完毕后才轮到它吗这里实际执行时可能会正常因为同一阶段内会按 reactor 顺序先完整执行 A 的插件再执行 B 的插件。但如果你把 A 的 exec 绑到process-resourcesB 的 antrun 还在generate-resources那 B 必然报错。这种最小复现工程的核心价值是验证一个问题你的“目录创建”插件到底绑定在哪个阶段你的“目录读取”插件又绑定在哪个阶段。只要读取阶段不晚于创建阶段大概率没问题一旦读取阶段早于创建阶段必然失败。4. 解决方案与实操步骤4.1 方案一让负责目录的插件显式创建目录如果你已经定位到是某个插件的输出目录没有被自动创建最简单直接的办法是在该插件执行前用maven-antrun-plugin或exec-maven-plugin先执行一次mkdir。绑定在对应插件之前或同一阶段靠前位置即可plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-antrun-plugin/artifactId version3.1.0/version executions execution idcreate-directory/id phasegenerate-resources/phase goals goalrun/goal /goals configuration target mkdir dir${project.build.directory}/generated/web / /target /configuration /execution /executions /plugin注意Ant 的mkdir会递归创建所有父目录比java.io.File.mkdir()那种“只建一层”的行为可靠很多。而且它不会因为目录已存在而报错幂等性很好可以放心重复构建。有人会问为什么不用 exec 执行mkdir -p也可以但跨平台性差Windows 上mkdir -p不是原生支持的写法Ant 任务在这一点上明显更稳妥。4.2 方案二调整插件绑定的生命周期阶段有些目录实际上“晚一点就有”问题纯粹是读取方动手太早。这类场景不需要新建目录只需要把读取文件的插件改绑到更靠后的阶段。比如模块 B 要从模块 A 的target/generated-sources读取文件而这个目录是 A 在generate-sources阶段生成的。那么 B 的读取操作就不该放在generate-sources或更早的阶段至少要放到process-resources或compile阶段。具体调整方式是在插件的execution里改phase参数。这里给个建议的阶段对应关系你的目录由什么生成读取方安全绑定的阶段generate-sources阶段的代码生成至少process-resources或compileprocess-resources阶段的资源复制至少compile或后续阶段compile阶段生成的文件process-classes或后续阶段依赖模块整个package之后的 jar 产物package之后的阶段或先用 dependency 插件解压这个思路的本质是“错峰”让读取方永远比创建方晚一步而不是靠运气。4.3 方案三用模块依赖声明修正 reactor 顺序前面说了reactor 顺序取决于dependency声明。如果你确认目录创建方是另一个模块而读取方没有声明依赖那就补上依赖。即使读取方只是“借用”创建方的输出文件而不需要它的类也可以声明依赖这样 Maven 会确保创建方先被构建。有一个细节如果你不需要依赖模块的代码进入 classpath只想约束构建顺序可以加scopeprovided/scope或用optionaltrue/optionaldependency groupIdcom.demo/groupId artifactIdmodule-a/artifactId version${project.version}/version scopeprovided/scope /dependency这种依赖不会传到下游模块也不会打进最终的包里但足以让 reactor 把模块 A 排在模块 B 前面。如果你不想动依赖关系还有一种偏门但有效的办法把modules的顺序改成“创建方永远在读取方前面”。但我不推荐因为它一旦遇到模块间隐式依赖换人调整模块顺序就会再次翻车属于治标不治本。4.4 方案四用 build-helper 把生成目录纳入构建这一类方案适合“目录是生成代码目录”的场景。比如模块 A 生成了 Java 源码到target/generated-sources/xxx模块 B 需要编译这些源码。与其自己去目录里找文件不如用build-helper-maven-plugin把该目录补充为 B 的源码目录或资源目录。在模块 B 的 pom 里添加plugin groupIdorg.codehaus.mojo/groupId artifactIdbuild-helper-maven-plugin/artifactId version3.4.0/version executions execution idadd-source/id phasegenerate-sources/phase goals goaladd-source/goal /goals configuration sources source${project.basedir}/../module-a/target/generated-sources/xxx/source /sources /configuration /execution /executions /plugin加了这段配置后maven-compiler-plugin会把这个目录里的.java文件当作工程源码来编译不再需要手动复制文件。不过我要提醒一句跨模块用${project.basedir}/../module-a这种相对路径有一定风险。如果模块目录结构变化或者有人单独构建模块 Bmvn -pl module-b路径会失效。更稳妥的做法是让模块 A 把生成好的资源打包成 jar然后模块 B 通过依赖引用如果一定要在 reactor 里共享即时生成的文件就把这种跨模块的相对路径提取到父 pom 的属性里统一维护。4.5 方案五跨模块文件复制用 dependency-plugin 代替文件系统操作最后再讲一个更彻底、更符合 Maven 哲学的方案。如果模块 B 要的其实就是模块 A 打包后的产物jar、zip、war那没必要在构建过程中去 A 的 target 目录里找文件。让 A 先把东西安装到本地仓库再由 B 用maven-dependency-plugin复制到指定位置。配置如下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-dependency-plugin/artifactId version3.6.1/version executions execution idcopy-artifact/id phaseprocess-resources/phase goals goalcopy/goal /goals configuration artifactItems artifactItem groupIdcom.demo/groupId artifactIdmodule-a/artifactId version${project.version}/version typejar/type outputDirectory${project.build.directory}/lib/outputDirectory /artifactItem /artifactItems /configuration /execution /executions /plugin这个方案要求模块 A 至少在package阶段完成并且其产物已经可以被 Maven 解析。为了保证顺序同样需要模块 B 对 A 有依赖声明或者至少让maven-dependency-plugin的 copy goal 能取到 A 的构建产物。简单说依赖声明依赖插件比手动复制文件目录要规范得多。提示如果你在使用mvn clean install时方案的执行顺序没问题但只运行mvn compile时失败了多半是因为package阶段的产物不存在。此时要把复制操作绑定到package之后的阶段或者改用前面说的process-resources之前就生成文件的方案。5. 常见问题与排查技巧实录5.1 常见问题速查表报错现象可能原因快速解法Could not find file /xxx/module-a/target/...读取方早于创建方执行调整 phase或补依赖声明Directory ... does not exist插件不自动创建目录用 antrun 的mkdir显式创建构建顺序不稳定时好时坏modules顺序或依赖声明不完整补充dependency或重构模块职责mvn compile失败但mvn install成功依赖了 package 阶段产物把读取操作绑定到 package 后或用 dependency 插件目标目录存在但构建还报不存在相对路径解析错误工作目录不在预期模块下改用${project.basedir}拼接绝对路径避免纯相对路径生成源码没有被编译生成目录不在 compile source 里用 build-helper 的add-source5.2 避坑经验我踩过的几个细节第一个经验是尽量别在插件配置里手写相对路径。多个模块构建时不同插件的工作目录并不总是模块根目录有的插件在 fork 进程时会把工作目录切得乱七八糟。统一用${project.basedir}、${project.build.directory}这种内置属性比什么都可靠。第二个经验是注意clean阶段的坑。mvn clean install会先清空所有 target 目录所以那些“构建一次成功第二次失败”的诡异问题十有八九是某个目录只被上一轮构建手工创建过而不是本轮的某个插件负责任地创建的。排查这种问题先跑一遍mvn clean再访问那个目录看看在不在立刻就能现原形。第三个经验是不要过度相信 antrun 的错误信息。Ant 处理copy任务时如果源目录不存在有时只是打印一句Warning: Could not find file并不是直接BUILD FAILURE。如果你看到日志里有 Warning 但构建最终失败了要把 warning 当作 error 一样对待它往往就是根因。第四个经验关于“目录未创建”与“目录创建了但内容不对”。如果你遇到目录存在、文件也有但内容不是最新的大概率不是目录问题而是缓存或增量构建问题。这种时候我习惯先mvn clean再试一次能排除一大堆干扰因素。真正的问题如果只在增量构建中出现那多半是某个插件输出的文件没有正确标为generated导致 Maven 的增量编译判断依赖关系时漏掉了它。5.3 一个真实的调试流程脚本最后分享一个我常用的调试流程。遇到目录未创建时别直接改 pom先按这个流程过一遍# 1. 打印构建顺序确认模块先后 mvn clean install -DskipTests | grep -A 20 Reactor Build Order # 2. 单模块构建验证目录由谁创建 mvn -pl module-a clean package -DskipTests ls -la module-a/target/generated # 3. 单模块构建另一个验证它在没有 module-a 的情况下能否成功 mvn -pl module-b clean package -DskipTests # 4. 加上 -X 看插件具体执行顺序 mvn clean install -DskipTests -X debug.log 21 grep -n generate-file\|copy-dir\|create-directory debug.log这套流程能帮你精准定位是谁在什么阶段试图读取目录又是谁负责创建目录。只要这两个“谁”和“何时”清晰了解决方案自然就出来了。我个人在实际操作中的体会是这类问题 70% 以上是“读取方比创建方心急”剩下 20% 是插件不会自动创建多级目录最后 10% 才是路径解析和相对路径错误。优先级最高的做法永远是先看Reactor Build Order再看插件的phase最后才去动目录创建逻辑。如果你能把这三个信息一次性对齐基本五分钟内就能定位根因。这个思路也适用于平时的代码评审只要看到有模块之间直接引用对方的 target 目录就应该立刻提醒“想想有没有更好的模块边界”因为这种文件系统级的耦合迟早会在某次clean或模块调整时给你来一次“目录未创建”的惊喜。
返回列表