☰
Maven exec-maven-plugin 报错 Failed to execute goal 的根因排查与解决
2026/10/17 7:58:25 网站建设 项目流程

1. 这个报错到底在说什么

第一次看到Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.0.0:exec (default-cli)这行红字的时候,很多人第一反应是"插件版本不对",然后跑去 pom 里改版本号,改完发现还是报同样的错。这个思路方向就偏了。

先把这行报错拆开看。org.codehaus.mojo:exec-maven-plugin:3.0.0是 Maven 在告诉你,它正在执行exec-maven-plugin这个插件的exec目标,版本是 3.0.0。(default-cli)这个后缀是关键信息,它说明这次执行不是绑定在某个生命周期阶段上自动触发的,而是你从命令行直接敲了mvn exec:exec或者mvn exec:java这类命令手动触发的。Maven 把这种从命令行直接调用的执行标记为default-cli。

所以这行报错本身不是根因,它只是 Maven 的"执行入口"报出来的壳。真正的错误信息在这行下面,通常是Caused by:开头的那几行。我见过太多人只盯着第一行红字看,把下面几十行堆栈直接忽略掉,然后在网上搜这行标题,搜到的答案五花八门,试了一圈都没用。正确的做法是往下翻,找到第一个Caused by,那才是真正要解决的问题。

exec-maven-plugin这个插件的作用说白了就一件事:让 Maven 帮你执行一段外部程序或者一个 Java 类的 main 方法。它最常见的两个目标是exec:exec(执行系统命令或外部可执行文件)和exec:java(在 Maven 的 JVM 里直接跑某个类的 main 方法)。因为它的定位是"执行器",所以它报错的原因几乎都跟"要执行的那个东西"有关,而不是插件本身坏了。

理解这一点很重要。插件本身是稳定的,3.0.0 这个版本也没什么已知的致命 bug。报错意味着:Maven 成功加载了插件,成功解析了你的配置,然后在你指定的那个执行动作上失败了。失败可能发生在好几个环节——找不到要执行的类、找不到要执行的命令、命令执行了但返回了非零退出码、类路径不对导致 ClassNotFound、参数传错了等等。下面我按实际排查中遇到的频率从高到低,把每一类都拆开讲。

2. 先分清 exec:java 和 exec:exec 两条路

排查之前必须先确认你用的是哪个目标,因为这两个目标的失败原因几乎不重叠。搞混了就会在错误的方向上浪费时间。

2.1 exec:java 的失败特征

exec:java是在 Maven 自己的 JVM 进程里,通过反射加载你指定的类,然后调用它的main方法。它的配置长这样:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.0.0</version> <configuration> <mainClass>com.example.demo.App</mainClass> </configuration> </plugin>

这种模式下最常见的报错是ClassNotFoundException或者NoSuchMethodException。原因通常是:mainClass写错了包名、类根本没编译出来、或者这个类不在当前模块的 classpath 上。注意exec:java默认使用的是 Maven 运行时的 classpath,它会把当前项目的依赖加进去,但如果你是多模块项目,依赖模块的类不一定在。

还有一个很隐蔽的坑:exec:java默认在 Maven 的 JVM 里跑,如果你的程序调用了System.exit(),会把整个 Maven 进程干掉,表现就是构建突然中断,没有任何有意义的报错。这种情况要加<cleanupDaemonThreads>false</cleanupDaemonThreads>或者干脆改用exec:exec起独立进程。

2.2 exec:exec 的失败特征

exec:exec是起一个独立的操作系统进程去执行命令。配置通常是这样:

<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.0.0</version> <configuration> <executable>java</executable> <arguments> <argument>-classpath</argument> <classpath/> <argument>com.example.demo.App</argument> </arguments> </configuration> </plugin>

这种模式下报错通常是Cannot run program "xxx"或者进程返回了非零退出码。前者是找不到可执行文件,后者是被执行的程序自己失败了。exec:exec的退出码会直接决定 Maven 构建的成败,被执行的程序返回非零,Maven 就报Failed to execute goal。

判断方法很简单:看你的 pom 里配的是mainClass还是executable。配mainClass就是exec:java的路子,配executable就是exec:exec的路子。命令行敲的mvn exec:java和mvn exec:exec也要对应上,别配了exec:exec却敲exec:java,那样会走默认配置,行为完全不是你预期的。

提示:如果你在命令行直接敲mvn exec:exec而没有在 pom 里配置<executable>,插件会尝试执行一个空命令,直接报错。命令行调用时最好把参数带上,比如mvn exec:exec -Dexec.executable="java" -Dexec.args="..."。

3. 按报错堆栈逐层定位根因

这一节是核心。我把实际排查中最常遇到的几类根因列出来,每一类都给出完整的定位链路,你可以照着一步步走。

3.1 找不到主类:ClassNotFoundException 的完整排查

假设你看到的是这样的堆栈:

[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.0.0:exec (default-cli) on project demo: Execution default-cli of goal ... failed: An API incompatibility was encountered ... [ERROR] Caused by: java.lang.ClassNotFoundException: com.example.demo.App

第一步,确认这个类到底编译出来没有。去target/classes/com/example/demo/目录下看有没有App.class。如果没有,说明编译阶段就没过,或者你的源码目录结构不对。Maven 默认的源码目录是src/main/java,如果你把代码放在了src/java或者别的目录,Maven 根本不会编译它。

第二步,如果 class 文件存在,确认包名和类名是否完全匹配。com.example.demo.App对应的是target/classes/com/example/demo/App.class。大小写敏感,app和App是两个不同的类。我遇到过有人类名写对了但包声明写错,源码里是package com.example.demo;但文件放在了src/main/java/com/example/App.java,编译出来的路径和预期不符。

第三步,如果是多模块项目,确认你要执行的类所在的模块是否已经被安装或者被当前模块依赖。exec:java用的是当前模块的 classpath,兄弟模块的类不会自动进来。要么先mvn install把依赖模块装到本地仓库,要么在 pom 里显式加上模块依赖。

第四步,检查是否有provided或testscope 的依赖被漏掉。exec:java默认只包含compile和runtimescope 的依赖,provided的不会进 classpath。如果你的主类依赖了某个provided的库,运行时会 ClassNotFound。

3.2 找不到可执行命令:Cannot run program 的定位

exec:exec报Cannot run program "xxx": error=2, No such file or directory,这个xxx就是你要执行的可执行文件名。常见情况:

  • 写的是相对路径,但工作目录不是你以为的那个。exec:exec默认的工作目录是项目根目录,但如果你在<workingDirectory>里改了,就要按改后的算。
  • 写的是java,但系统 PATH 里没有 java。这种情况在 CI 环境里特别常见,构建容器里只装了 JDK 但没配 PATH,或者用的是 JRE 而不是 JDK。
  • Windows 上写的是mvn而不是mvn.cmd。Windows 下批处理文件必须带扩展名,Linux 下则不需要。

定位方法:把<executable>的值换成绝对路径试一次。比如 Linux 下换成/usr/bin/java,Windows 下换成C:\Program Files\Java\jdk-17\bin\java.exe。如果绝对路径能跑通,那就是 PATH 或者相对路径的问题。

3.3 进程返回非零退出码:被执行的程序自己失败了

这种情况最容易被误判。Maven 报的是Failed to execute goal,但实际上是你的程序返回了非零退出码,Maven 只是如实转达。堆栈里通常会有Result of execution: 1或者类似的字样。

这时候要做的不是改 Maven 配置,而是去看被执行的程序自己的输出。exec:exec默认会把子进程的 stdout 和 stderr 透传到 Maven 的控制台,所以往上翻,找到你的程序打印的错误信息。如果输出被吞了,加<outputFile>和<errorFile>把输出重定向到文件里看。

还有一种情况是程序被信号杀掉了,退出码是 137(128+9,SIGKILL)或者 143(128+15,SIGTERM)。这通常是内存不足被 OOM Killer 干掉,或者超时被外部机制终止。这种要看系统日志,不是 Maven 能解决的。

3.4 参数传递错误:arguments 和 classpath 的坑

exec:exec的<arguments>配置里,<classpath/>这个特殊元素代表 Maven 解析出来的完整 classpath。它的位置很关键,必须放在-classpath参数后面、主类名前面。放错位置会导致 classpath 被当成主类名,报 ClassNotFound。

<arguments> <argument>-classpath</argument> <classpath/> <argument>com.example.demo.App</argument> </arguments>

另外,<argument>里的值如果包含空格,Maven 会把它当成一个整体参数传递,这通常是对的。但如果你的参数本身需要被拆成多个,就要写多个<argument>。我见过有人在<argument>里写-Dfoo=bar baz,期望它拆成两个参数,结果被当成一个带空格的参数传过去,程序解析失败。

4. 几个高频踩坑场景的复盘

这一节讲几个我在实际项目里反复遇到的场景,每个都给出完整的排查过程和最终解法。

4.1 场景一:Spring Boot 项目里用 exec 启动失败

有个项目想在 Maven 构建后直接跑一下 Spring Boot 的 main 方法做冒烟测试,配了exec:java指向Application类。结果报ClassNotFoundException,但target/classes下明明有Application.class。

排查过程:先确认类存在,再确认包名,都没问题。然后看堆栈,发现 ClassNotFound 的不是Application本身,而是Application依赖的某个 Spring 类。这就说明 classpath 不全。原因是 Spring Boot 项目通常用spring-boot-maven-plugin打 fat jar,依赖被打进 jar 里了,但exec:java用的是 Maven 的 classpath,不是 fat jar 的 classpath。如果依赖的 scope 是provided(比如spring-boot-starter-tomcat在某些配置下),就不会进 classpath。

解法:要么把依赖 scope 改成compile,要么改用exec:exec直接执行打包好的 fat jar:

<executable>java</executable> <arguments> <argument>-jar</argument> <argument>${project.build.directory}/${project.build.finalName}.jar</argument> </arguments>

这样跑的是完整的 fat jar,classpath 由 jar 内部的 manifest 决定,不会缺依赖。

4.2 场景二:CI 环境里 exec 找不到 java

本地跑得好好的,一上 CI 就报Cannot run program "java"。本地 PATH 里有 java,CI 容器里没有。

排查过程:在 CI 的构建脚本里加一行which java或者echo $PATH,确认 java 到底在不在 PATH 里。很多时候 CI 镜像里装了 JDK,但 PATH 没配全,或者用的是JAVA_HOME但没把它加到 PATH。

解法有两个。一是把<executable>改成${java.home}/bin/java,java.home是 Maven 运行时能拿到的系统属性,指向当前 JVM 的安装目录,这样不依赖 PATH。二是显式用绝对路径。推荐第一种,跨环境更稳。

<executable>${java.home}/bin/java</executable>

注意 Windows 下要加.exe,可以用 profile 区分,或者干脆用exec:java避开这个问题。

4.3 场景三:exec 执行 shell 脚本权限不足

想用exec:exec执行一个项目里的 shell 脚本,报Cannot run program "./scripts/run.sh": error=13, Permission denied。error=13 就是权限问题。

排查过程:ls -l scripts/run.sh看权限位,如果没有x,就是没执行权限。Git 在 Windows 上 checkout 的文件默认不带执行位,推到 Linux CI 上就丢了。

解法:chmod +x scripts/run.sh然后提交权限变更。Git 里可以用git update-index --chmod=+x scripts/run.sh把执行位记进去。或者干脆不依赖执行位,用sh scripts/run.sh的方式调用,把sh作为 executable,脚本路径作为参数。

4.4 场景四:exec 的输出被吞导致排查困难

程序失败了但看不到任何输出,Maven 只报一个非零退出码。这是因为某些配置下子进程的输出没有透传。

解法:显式配置输出重定向。

<configuration> <executable>java</executable> <outputFile>${project.build.directory}/exec.out</outputFile> <errorFile>${project.build.directory}/exec.err</errorFile> </configuration>

跑完去看这两个文件,程序的真实报错就在里面。这个技巧在排查 CI 上的偶发失败时特别有用,因为 CI 的日志经常被截断。

5. 配置层面的预防性写法

与其每次报错再排查,不如一开始就把配置写稳。这一节给出几个经过验证的配置模板和原则。

5.1 用属性管理版本和路径

不要把版本号硬编码在插件配置里,用属性统一管理:

<properties> <exec.plugin.version>3.0.0</exec.plugin.version> <main.class>com.example.demo.App</main.class> </properties>

这样升级插件版本时只改一处。主类名也抽出来,多环境切换时方便覆盖。

5.2 显式声明 skip 和 failOnError

exec-maven-plugin有<skip>和<failOnError>两个配置。<skip>true</skip>可以临时跳过执行,<failOnError>false</failOnError>可以让执行失败不中断构建。后者要慎用,它会把真实错误藏起来,只在明确知道失败可接受时才用。

<configuration> <skip>${exec.skip}</skip> <failOnError>true</failOnError> </configuration>

把exec.skip定义成属性,默认 false,需要跳过时命令行-Dexec.skip=true覆盖。

5.3 绑定到生命周期阶段的正确姿势

如果不想每次手动敲mvn exec:exec,可以把它绑定到某个生命周期阶段,比如verify:

<executions> <execution> <id>run-smoke-test</id> <phase>verify</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>${java.home}/bin/java</executable> <arguments> <argument>-classpath</argument> <classpath/> <argument>${main.class}</argument> </arguments> </configuration> </execution> </executions>

注意绑定后执行 ID 不再是default-cli,报错信息里会显示你定义的<id>,比如run-smoke-test。这也是判断"是命令行触发还是生命周期触发"的一个依据。

5.4 多模块项目里的 classpath 处理

多模块项目里,如果要在子模块执行依赖其他模块的类,最稳的做法是先用maven-dependency-plugin把依赖复制到target/dependency,然后手动拼 classpath:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/dependency</outputDirectory> </configuration> </execution> </executions> </plugin>

然后在 exec 的 arguments 里用${project.build.directory}/dependency/*加上主类路径。这样 classpath 完全可控,不依赖 Maven 的解析结果。

6. 排查这类问题的通用心法

最后分享几条我在反复处理这类报错中总结出来的经验,不限于exec-maven-plugin,对大多数 Maven 插件报错都适用。

第一条,永远先看Caused by。Maven 的报错是层层包装的,最外层那行Failed to execute goal只是告诉你"哪个插件在哪个目标上失败了",真正的根因在堆栈深处。养成从下往上读堆栈的习惯,找到第一个Caused by,那才是起点。

第二条,区分"配置错误"和"运行时错误"。配置错误在 Maven 解析 pom 阶段就会报,堆栈里会有MojoExecutionException或者PluginConfigurationException。运行时错误是插件已经跑起来了,在执行动作时失败,堆栈里会有具体的业务异常。前者改配置,后者查程序。

第三条,用-X打开 debug 日志。mvn exec:exec -X会打印出插件解析到的完整配置、实际执行的命令行、classpath 的完整内容。很多时候看一眼实际执行的命令,问题就一目了然了。这个参数在排查"配置明明写对了但行为不对"的问题时是杀手锏。

第四条,最小化复现。把 pom 精简到只剩 exec 插件和必要的依赖,看问题还在不在。如果不在,说明是某个其他插件或依赖干扰了。逐步加回来,定位到具体的干扰源。这个方法笨但有效,尤其是处理那些"看起来毫无道理"的报错。

第五条,注意 Maven 版本和插件版本的兼容性。exec-maven-plugin3.0.0 要求 Maven 3.2.5 以上,JDK 8 以上。如果你用的是很老的 Maven,可能会遇到一些奇怪的 API 不兼容报错。升级 Maven 到 3.8 以上基本能避开这类问题。

我个人在实际操作中的体会是,这类报错九成以上都不是插件本身的问题,而是"你要执行的东西"和"Maven 以为你要执行的东西"之间存在偏差。把实际执行的命令打印出来,和你的预期对一遍,问题基本就现形了。与其在网上搜那行标题找现成答案,不如花五分钟把堆栈读完、把 debug 日志打开,效率高得多。

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

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

立即咨询