ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Maven exec插件报错排查:从Process exited到ClassNotFound

Maven exec插件报错排查:从Process exited到ClassNotFound 先说结论遇到Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.6.3:exec (default-cli)这个报错时exec插件本身大概率没坏真正的问题通常藏在它下面那一行Process exited with an error: 1或者某个Caused by里。我是从Maven项目里一步步踩过来的在本地、CI、甚至刚接手的老项目里都见过这个错一度也以为是exec-maven-plugin配置写错了后来才发现它只是一个“壳”核心是外部进程没跑成。如果你现在正卡在这条报错前面不用急着改pom也别急着卸载重装Maven。这篇文章我会按实际排障的顺序把这条报错的执行链路、常见根因、可以直接复用的配置模板以及我在真实项目里总结出的避坑点都写清楚。无论你是刚接触Maven的新手还是在Java服务、多模块工程里长期使用exec插件的老手应该都能从中找到对应的解决思路。1. 先搞明白这条报错到底在说什么1.1 exec-maven-plugin是干什么的Maven本身的核心能力是编译、测试、打包、发布这类生命周期动作但它不太擅长“跑程序”。可你在日常开发中又经常需要启动一个Java类或者在构建过程中临时执行某个命令行工具于是就有了exec-maven-plugin。它的坐标是org.codehaus.mojo:exec-maven-plugin常见的goal有三个exec:exec在新的外部进程中执行一条命令或一个可执行文件。示例场景是启动一个shell脚本、拉起一个二进制程序或者自己去拼java -cp xxx com.example.Main。exec:java在Maven所在的JVM里直接运行某个public static void main(String[] args)主类类路径会使用当前项目的运行期classpath。exec:script执行一段内嵌脚本实际用的人相对少。标题里出错的是exec:exec也就是说你原本的意图是“启动一个外部进程”。这一点非常重要因为很多后来排查无从下手的人其实连这层都没区分清楚他们会用exec:exec去跑Java主类又没把classpath传给子进程最后永远在ClassNotFound里打转。1.2 一行报错应该怎么读先看这句完整格式org.codehaus.mojo:exec-maven-plugin:3.6.3:exec (default-cli)Maven里一个goal的完整叫法是groupId:artifactId:version:goal所以这段信息是在告诉你执行的是exec-maven-plugin3.6.3版本的exec目标。后面括号里的default-cli是个很有用的线索。它意味着这次执行不是来自pom.xml里某个execution绑定而是你直接在命令行敲出来的。举例来说当你运行mvn exec:exec或者通过IDE的Maven面板手动运行了execMaven会自动给这次调用生成一个执行ID默认就叫default-cli。反过来如果这个错是pom里配置的executionidrun-app/id/execution触发出来的报错里通常会显示run-app而不是default-cli。换句话说这个错误提示本身已经告诉你了“问题大概率出在命令行手动执行的那次调用上。”排障方向可以立刻锁定。1.3 最容易踩到问题的三个场景根据我在不同项目里的经验这条报错最常出现在下面三类场景里你在命令行手动运行了mvn exec:exec或mvn exec:java通过-Dexec.executable、-Dexec.mainClass之类的参数去启动程序。pom里配置了exec-maven-plugin并且在某个phase上做了绑定运行mvn test或mvn package时插件被自动触发。IDE里配置了Maven启动项例如在IntelliJ IDEA里添加了一个“Run Maven Goal”的配置启动项目时实际执行的是mvn exec:exec。这三个场景我都碰到过。个人体感第1种情况占比最高而且多见于团队里某个项目同时存在多个可执行入口时大家为了省事直接用命令行传参。第2种容易在别人没有预期的情况下触发属于令人莫名其妙的那种失败。第3种则往往伴随着IDE控制台日志折叠报错被压成一小段需要先展开才能看到真正原因。2. 快速定位根因别只盯第一行后面三行才是关键2.1 先把输出完整展开很多人在IntelliJ IDEA或VS Code里看到红字第一反应是去搜索引擎把第一行粘进去。这个做法不能说完全没意义但效率太低。因为Failed to execute goal只是顶层包装每个案例真正的差异都在后面。我建议立刻做两件事第一在IDE的Maven控制台里找到类似“Show stack trace”或“Show Log”的按钮把日志展开到完整模式。IDEA默认有时候会隐藏掉Caused by之后的内容而这个隐藏部分往往就是核心。第二直接回到终端完整跑一次并且加上-e参数让Maven打印详细堆栈mvn exec:exec -e如果还想看到Maven内部更细节的执行过程就在后面再加一个-X也就是mvn exec:exec -X-X输出会很吵但排障时需要的信息通常会出现在里面。2.2 把所有根因归成两大类在我的经验里遇到这类报错时先不需要逐字分析日志你先问自己一个问题那个外部进程到底有没有被真正拉起来如果进程压根没起来报错通常长这样Cannot run program /home/user/tools/run.sh (in directory ...) error2, No such file or directory“error2”在Linux/macOS里代表文件不存在在Windows下常见的是CreateProcess error2。这类问题的根因是路径不对、命令不在PATH里、或者要启动的文件没有执行权限。如果进程确实起来了但很快就退出报错通常会写成Process exited with an error: 1这意味着你指定的程序已经运行但返回了一个非0退出码。它可能是Java程序抛出异常导致的也可能是Shell脚本执行到某个命令时报错退出。这两类的处理方向完全不同前者去查路径和权限后者去查程序自己的日志和输出。如果你能把Process exited with an error: 1和Cannot run program区分清楚就已经解决了一半。2.3 必须分清exec:exec和exec:java这一点再强调都不为过。很多类似的报错其实是因为搞混了两个goal。exec:java会复用Maven进程的classpath把所有依赖jar包都放在java.class.path里。因此执行mvn exec:java -Dexec.mainClasscom.example.Application时大多数第三方依赖都能被加载上只需要确认主类在编译产物里。exec:exec则不是这样。它更像你在终端里手动敲了一条命令默认情况下不会自动帮你带上项目的classpath。如果你想跑Java程序必须自己拼-classpath参数否则ClassNotFoundException就会接踵而来。所以如果你的本意是运行一个Java主类优先选择exec:java如果必须运行的是外部脚本、二进制程序、Node脚本、Python程序才选exec:exec。2.4 一条真实报错的完整阅读过程给你看一个我在实际项目里修过的简化例子当时日志大概是这样的[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.6.3:exec (default-cli) on project order-service: Command execution failed. [ERROR] Process exited with an error: 1 [ERROR] - [Help 1]只看这三行你只能确定“进程退出码是1”但不知道程序为什么退出。继续往下滚动控制台能看到程序自己打印的业务日志Exception in thread main java.net.BindException: Address already in use at sun.nio.ch.Net.bind0(Native Method) ...到这里真相就清楚了不是exec插件的问题而是Spring Boot应用要监听的8080端口已经被别的进程占用了。所以排障口诀就是**第一行看是哪个goal第二行看进程起没起来往下翻看程序自己的输出。**后面的Caused by和程序异常堆栈才是你真正需要关心的地方。3. 高频根因逐个击破3.1 端口被占用最常见的原因之一如果你的项目是Spring Boot或基于内嵌容器的Web服务这种错几乎每天都在发生。原因很简单exec插件启动了一个Java进程这个Java进程启动内嵌Tomcat/Jetty时发现端口被占用于是进程直接退出返回退出码1exec插件接收到非0码后就把整次Maven执行标记为失败。排查命令很直接。Linux/macOS下可以用lsof -i :8080Windows下用netstat -aon | findstr :8080看到占用端口的PID后再判断要不要处理。如果确定是上次残留的Java进程可以先杀掉再重新运行。我在实战中踩过的坑是开发机上同时开了多个微服务每个人习惯的端口还不一样有人用8080有人用9090结果别人一启动就互相碰撞。后来我们统一在每个模块的pom里给exec执行加上--server.port参数不同模块用固定端口这才消停。如果你当前项目的需求是临时验证某个功能最省事的办法就是把端口换掉mvn exec:java -Dexec.mainClasscom.example.Application -Dexec.args--server.port80813.2 classpath不完整导致ClassNotFoundException这种情况在exec:exec里特别明显。前面已经提到exec:exec不会自动帮你带上Maven项目的classpath。当你用下面的方式去跑Java主类时mvn exec:exec -Dexec.executablejava -Dexec.args-cp target/classes com.example.Application如果Application依赖了某个第三方jar包运行时就会报Caused by: java.lang.ClassNotFoundException: com.example.SomeDependency除非你手动把依赖路径拼完整否则这种错误是无法避免的。而在exec:java里Maven会自动为当前项目构建classpath依赖都在通常不会出这个问题。因此最省心的建议是只要目标是一个Java主类就优先用exec:java别让exec:exec来背这个锅。如果确实只能用exec:exec就需要在pom的arguments里插入classpath/占位符Maven会把它替换成项目当前的classpath。具体模板我放到第4节说明。另外多模块项目里还有一个隐性坑当前模块依赖了同仓库里的其他模块但那些模块没有先mvn install到本地仓库甚至没有编译到当前模块的classpath中。此时需要先在父工程目录运行mvn install -DskipTests把依赖模块装进本地仓库然后再执行当前模块的exec命令。3.3 可执行文件路径不对或权限不够这是Cannot run program这类错误的直接原因我大概总结了下面几种具体情况。Linux/macOS下如果executable里配置的是相对路径或者脚本名但系统PATH里没有这个命令就会出现error2, No such file or directory。很多人看到No such file会以为是文件不存在实际上它可能是“命令找不到”。还有一种典型情况是你明确写了脚本路径但脚本没有执行权限/bin/sh: /home/user/project/tools/start.sh: Permission denied这种情况下需要给脚本加上执行权限chmod x tools/start.shWindows下最常见的坑则是路径里包含空格。例如executableC:\Program Files\Java\jdk-17\bin\java.exe/executable如果配置不当Maven解析参数时会把Program当成一段单独的命令导致CreateProcess error2。建议要么给完整路径加转义要么尽量把带空格的路径放到arguments里单独传参不要在executable里塞一整条带空格的shell命令。我在团队里长期使用的规范是把本地工具统一放到tools/目录下然后用${project.basedir}前缀拼绝对路径。这种写法在团队内所有成员机器上的一致性会好很多也不会出现“我本地明明能跑为什么别人机器上就error2”的奇怪问题。3.4 JVM参数或系统编码引发的间接失败有时候exec启动的Java进程确实起来了但JVM自己就不好好工作。比如你在exec.args里配置了过大的堆内存-Xmx8192m而开发机剩余内存不足JVM启动时会报Error occurred during initialization of VM Could not reserve enough space for object heap这类错误同样会让进程以非0码退出最终落到Failed to execute goal上。解决思路就是调低Xmx或者关闭电脑里其他占用大内存的程序。中文环境下另一个高发问题是编码。如果控制台输出中文乱码甚至程序打出的日志包含非法字节导致某些解析失败可以考虑在JVM参数里加上argument-Dfile.encodingUTF-8/argument同时在Maven的启动环境里设置MAVEN_OPTSexport MAVEN_OPTS-Dfile.encodingUTF-8Windows下还要注意CMD的代码页如果继续乱码就改成chcp 65001这些参数看起来和服务逻辑无关但恰恰是很多奇怪报错的最终原因。3.5 插件本身或依赖下载不完整还有一种情况不是进程启动失败而是exec插件自己在加载阶段就挂了。如果你看到这样的错误Plugin org.codehaus.mojo:exec-maven-plugin:3.6.3 or one of its dependencies could not be resolved那就说明插件jar包在本地仓库里缺失或者Maven无法从远程仓库下载。这个问题更多跟Maven本身的环境、settings.xml配置、仓库网络有关系。可以用下面命令强制刷新插件元数据mvn -U org.codehaus.mojo:exec-maven-plugin:3.6.3:help如果本地网络访问默认中央仓库较慢可以检查~/.m2/settings.xml里的mirror配置换成国内可达的公共镜像仓库。这里给一个最小化的mirror示例mirror idaliyunmaven/id mirrorOf*/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror要注意的是mirror配置只影响下载地址不影响你pom里写的插件坐标。改好之后建议清理一下本地仓库里残留的目录再重新跑一次命令往往就能恢复。4. 几种可以直接抄的配置模板4.1 最简单的方案用exec:java跑主类如果你的需求是“把这个Java主类跑起来”pom里这样配置就够用plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.6.3/version configuration mainClasscom.example.Main/mainClass /configuration /plugin然后命令行运行mvn exec:java这是我在普通Java项目中用得最多的方式优点是不需要关心classpathMaven会把项目编译产物和依赖都处理掉。唯一需要注意的是如果程序里启动了非守护线程比如Swing界面或某些定时任务Maven进程会因为线程仍活着而一直不退。此时可以在configuration里加一个参数cleanupDaemonThreadsfalse/cleanupDaemonThreads至于能不能用取决于你的具体场景需要时可以试一下再决定。4.2 用exec:exec拉起外部命令并正确传classpath如果目标不是Java类而是一个外部脚本或二进制程序exec:exec是更合适的。以下是一个标准的配置用java命令启动类同时把项目的classpath传进去plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.6.3/version configuration executablejava/executable arguments argument-Xmx512m/argument argument-Dfile.encodingUTF-8/argument argument-classpath/argument classpath/ argumentcom.example.Application/argument argument--server.port8081/argument /arguments /configuration /plugin这里classpath/会被Maven替换成项目完整的classpath路径串。如果你不想把配置写死在pom里也可以用命令行传参mvn exec:exec -Dexec.executablejava -Dexec.args-cp %classpath com.example.Application但要注意命令行传参的方式带%classpath这类占位符有时会在不同版本中表现不一致不如pom里用classpath/来得稳。我曾经在一个微服务项目里用上面的pom配置启动内部工具类结果一直报ClassNotFoundException。后来排查发现本地仓库里的exec插件版本是3.1.0根本不支持后来版本的classpath/占位行为升级到3.6.3才正常。所以如果你在同一个项目里手动锁定了多个版本建议先执行mvn help:effective-pom看看最终生效的插件版本到底是什么。4.3 同时跑多个不同入口用命令行参数覆盖有些项目不只一个主类不可能每换一个入口就改一次pom。这种情况下建议只在pom里放一份最常用的默认配置然后通过-Dexec.mainClass或-Dexec.executable在命令行覆盖。比如你想临时跑另一个类mvn exec:java -Dexec.mainClasscom.example.Tool想临时启动外部脚本mvn exec:exec -Dexec.executable/usr/local/bin/some-tool这种参数覆盖的机制非常实用但它也带来一个团队协作问题如果你把这些命令写进Markdown文档或交接给别人的时候不够完整对方照着敲就会复现标题里的报错。我见过太多同事只复制了前半段mvn exec:exec却漏掉了后面的-Dexec.executable于是Maven立刻报错提示executable没有设置。解决方法是在执行pom里设置默认executable或者在命令行写完整二选一不要依赖“我以为大家都知道”。4.4 Spring Boot项目到底怎么选如果你当前项目是Spring Boot而且只是想把应用启动起来个人建议直接用Spring Boot官方插件mvn spring-boot:run -Dspring-boot.run.profilesdev它有更好的fork进程管理支持devtools热重载也能方便地指定profile。exec-maven-plugin虽然也能启动Spring Boot但你需要额外处理好classpath、编码、环境变量、进程退出码等问题有点绕远路。如果你的工程并不是Spring Boot或者正在做一个包含多个本地工具的纯Java项目exec插件反而更灵活。一些老项目里的定时任务、数据迁移工具本质都是一个个独立的main类用exec插件的profile配置来切换入口是很常见的做法。profiles profile idrun-tool/id build plugins plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.6.3/version configuration mainClasscom.example.data.MigrationTool/mainClass /configuration /plugin /plugins /build /profile /profiles运行时mvn compile exec:java -P run-tool这个模式对于需要维护多个调度脚本的项目非常友好后续加新工具只需要新增一个profile不用折腾新的启动脚本。5. 常见错误速查表和我的避坑经验5.1 把日志关键片段变成速查表我把排障中经常遇到的典型日志片段整理成下面这张表你在看到报错时可以先对着查能省不少时间。日志关键片段可能原因第一排查动作Cannot run program xxx error2可执行文件路径不存在或命令不在PATH里检查executable完整路径用which确认命令位置Permission denied脚本或二进制没有执行权限执行chmod x赋予权限Process exited with an error: 1且下方有BindException启动的Web服务端口被占用用lsof -i :端口或netstat -aon查占用进程Process exited with an error: 1且下方有业务异常栈Java程序运行时报错退出顺着异常栈定位代码而不是研究exec插件ClassNotFoundExceptionexec:exec没带classpath或依赖模块未install改用exec:java或在pom中加入classpath/Could not reserve enough space for object heapXmx参数超过可用内存降低JVM堆内存参数Plugin ... could not be resolved插件jar包下载失败或settings.xml镜像问题检查Maven本地仓库和镜像配置执行-U刷新中文乱码或编码相关错误默认编码不是UTF-8加上-Dfile.encodingUTF-8Windows下调整代码页这张表不是万能药但能帮你快速把问题归类。归好类之后处理路径基本就清晰了。5.2 我在实战中总结的三条避坑经验第一条日志一定要“往后看”。标题里的错误只是Maven包装后的结果程序真正的报错在更下面。如果你在IDE里只看到红色折叠消息那就去终端完整跑一次或者点开日志的堆栈按钮。我见过很多同事在群里贴了第一行就开问实际上真正的异常明明就在截图下方点开就能看到。第二条exec:exec不是exec:java。每次有人把Failed to execute goal ...:exec的问题发给我我都会先问一句你原本要执行的到底是Java主类还是一个外部程序如果是Java主类我通常直接建议他换exec:java。这个改动不一定所有场景都适用但在九成以上“启动Java程序”的场景里classpath问题会瞬间消失。第三条执行环境要尽量可复现。如果你把execive路径写成了自己机器上的绝对路径比如/Users/yourname/tools/xxx另一位同事拿到的代码一定会在他的电脑上报Cannot run program。更好的做法是复用项目相对路径或环境变量例如用${project.basedir}或${env.JAVA_HOME}拼出可执行文件路径。这台机器能跑只是第一步团队里每个人都能跑才是真正减少了无谓的报错。写在最后的一段个人经验如果你已经按前面步骤排查到Process exited with an error: 1但还是不知道程序为什么退出我的建议是给你要运行的Java程序临时加一个顶层异常捕获或者把启动类的main方法外层包上try-catch并打印堆栈。别小看这个土办法很多程序在启动阶段抛出的异常会被系统打印到stderr但控制台日志一多就不容易看到。把关键异常输出明确标记出来比反复去Maven日志里翻要快得多。我也想说exec-maven-plugin这个插件本身在Maven生态里属于轻量且高频的工具一旦你理解了它只是负责把进程拉起来的角色后续遇到各种五花八门的错误都会淡定很多。顺着“进程有没有起来——起来后为什么退出”这条线去排查绝大多数问题都能在五到十分钟内定位。要是以后你再看到这个熟悉的红色错误不妨先按这篇文章的顺序试一遍说不定能帮你省下不少搜搜索引擎的时间。
返回列表