Maven exec插件报错排查:从Process exited到ClassNotFound
2026/9/7 17:52:29 网站建设 项目流程

先说结论:遇到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面板手动运行了exec,Maven会自动给这次调用生成一个执行ID,默认就叫default-cli。反过来,如果这个错是pom里配置的<execution><id>run-app</id></execution>触发出来的,报错里通常会显示run-app而不是default-cli

换句话说,这个错误提示本身已经告诉你了:“问题大概率出在命令行手动执行的那次调用上。”排障方向可以立刻锁定。

1.3 最容易踩到问题的三个场景

根据我在不同项目里的经验,这条报错最常出现在下面三类场景里:

  1. 你在命令行手动运行了mvn exec:execmvn exec:java,通过-Dexec.executable-Dexec.mainClass之类的参数去启动程序。
  2. pom里配置了exec-maven-plugin,并且在某个phase上做了绑定,运行mvn testmvn package时插件被自动触发。
  3. 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 "...") error=2, No such file or directory

“error=2”在Linux/macOS里代表文件不存在,在Windows下常见的是CreateProcess error=2。这类问题的根因是路径不对、命令不在PATH里、或者要启动的文件没有执行权限。

如果进程确实起来了,但很快就退出,报错通常会写成:

Process exited with an error: 1

这意味着你指定的程序已经运行,但返回了一个非0退出码。它可能是Java程序抛出异常导致的,也可能是Shell脚本执行到某个命令时报错退出。

这两类的处理方向完全不同:前者去查路径和权限,后者去查程序自己的日志和输出。如果你能把Process exited with an error: 1Cannot run program区分清楚,就已经解决了一半。

2.3 必须分清exec:exec和exec:java

这一点再强调都不为过。很多类似的报错其实是因为搞混了两个goal。

exec:java会复用Maven进程的classpath,把所有依赖jar包都放在java.class.path里。因此执行mvn exec:java -Dexec.mainClass=com.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时发现端口被占用,于是进程直接退出,返回退出码1,exec插件接收到非0码后就把整次Maven执行标记为失败。

排查命令很直接。Linux/macOS下可以用:

lsof -i :8080

Windows下用:

netstat -aon | findstr :8080

看到占用端口的PID后,再判断要不要处理。如果确定是上次残留的Java进程,可以先杀掉再重新运行。

我在实战中踩过的坑是:开发机上同时开了多个微服务,每个人习惯的端口还不一样,有人用8080,有人用9090,结果别人一启动就互相碰撞。后来我们统一在每个模块的pom里给exec执行加上--server.port参数,不同模块用固定端口,这才消停。如果你当前项目的需求是临时验证某个功能,最省事的办法就是把端口换掉:

mvn exec:java -Dexec.mainClass=com.example.Application -Dexec.args="--server.port=8081"

3.2 classpath不完整导致ClassNotFoundException

这种情况在exec:exec里特别明显。前面已经提到,exec:exec不会自动帮你带上Maven项目的classpath。当你用下面的方式去跑Java主类时:

mvn exec:exec -Dexec.executable=java -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里没有这个命令,就会出现error=2, No such file or directory。很多人看到No such file会以为是文件不存在,实际上它可能是“命令找不到”。

还有一种典型情况是你明确写了脚本路径,但脚本没有执行权限:

/bin/sh: /home/user/project/tools/start.sh: Permission denied

这种情况下需要给脚本加上执行权限:

chmod +x tools/start.sh

Windows下最常见的坑则是路径里包含空格。例如:

<executable>C:\Program Files\Java\jdk-17\bin\java.exe</executable>

如果配置不当,Maven解析参数时会把Program当成一段单独的命令,导致CreateProcess error=2。建议要么给完整路径加转义,要么尽量把带空格的路径放到<arguments>里单独传参,不要在<executable>里塞一整条带空格的shell命令。

我在团队里长期使用的规范是把本地工具统一放到tools/目录下,然后用${project.basedir}前缀拼绝对路径。这种写法在团队内所有成员机器上的一致性会好很多,也不会出现“我本地明明能跑,为什么别人机器上就error=2”的奇怪问题。

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.encoding=UTF-8</argument>

同时在Maven的启动环境里设置MAVEN_OPTS

export MAVEN_OPTS="-Dfile.encoding=UTF-8"

Windows下还要注意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> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <url>https://maven.aliyun.com/repository/public</url> </mirror>

要注意的是,mirror配置只影响下载地址,不影响你pom里写的插件坐标。改好之后建议清理一下本地仓库里残留的目录,再重新跑一次命令,往往就能恢复。

4. 几种可以直接抄的配置模板

4.1 最简单的方案:用exec:java跑主类

如果你的需求是“把这个Java主类跑起来”,pom里这样配置就够用:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.6.3</version> <configuration> <mainClass>com.example.Main</mainClass> </configuration> </plugin>

然后命令行运行:

mvn exec:java

这是我在普通Java项目中用得最多的方式,优点是不需要关心classpath,Maven会把项目编译产物和依赖都处理掉。唯一需要注意的是,如果程序里启动了非守护线程,比如Swing界面或某些定时任务,Maven进程会因为线程仍活着而一直不退。此时可以在configuration里加一个参数:

<cleanupDaemonThreads>false</cleanupDaemonThreads>

至于能不能用,取决于你的具体场景,需要时可以试一下再决定。

4.2 用exec:exec拉起外部命令,并正确传classpath

如果目标不是Java类,而是一个外部脚本或二进制程序,exec:exec是更合适的。以下是一个标准的配置,用java命令启动类,同时把项目的classpath传进去:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.6.3</version> <configuration> <executable>java</executable> <arguments> <argument>-Xmx512m</argument> <argument>-Dfile.encoding=UTF-8</argument> <argument>-classpath</argument> <classpath/> <argument>com.example.Application</argument> <argument>--server.port=8081</argument> </arguments> </configuration> </plugin>

这里<classpath/>会被Maven替换成项目完整的classpath路径串。如果你不想把配置写死在pom里,也可以用命令行传参:

mvn exec:exec -Dexec.executable=java -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.mainClass=com.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.profiles=dev

它有更好的fork进程管理,支持devtools热重载,也能方便地指定profile。exec-maven-plugin虽然也能启动Spring Boot,但你需要额外处理好classpath、编码、环境变量、进程退出码等问题,有点绕远路。

如果你的工程并不是Spring Boot,或者正在做一个包含多个本地工具的纯Java项目,exec插件反而更灵活。一些老项目里的定时任务、数据迁移工具,本质都是一个个独立的main类,用exec插件的profile配置来切换入口是很常见的做法。

<profiles> <profile> <id>run-tool</id> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.6.3</version> <configuration> <mainClass>com.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" error=2可执行文件路径不存在,或命令不在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.encoding=UTF-8,Windows下调整代码页

这张表不是万能药,但能帮你快速把问题归类。归好类之后,处理路径基本就清晰了。

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生态里属于轻量且高频的工具,一旦你理解了它只是负责把进程拉起来的角色,后续遇到各种五花八门的错误都会淡定很多。顺着“进程有没有起来——起来后为什么退出”这条线去排查,绝大多数问题都能在五到十分钟内定位。要是以后你再看到这个熟悉的红色错误,不妨先按这篇文章的顺序试一遍,说不定能帮你省下不少搜搜索引擎的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询