碰到Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.6.0这串报错的时候,大多数人第一反应是去搜某个具体异常,然后越搜越乱。我可以直接告诉你,这个报错本质上是Maven在执行exec插件时,子任务没跑成功,整个构建被中断了。真正的问题藏在后面的Caused by或者Execution部分的细节里,比如mainClass找不到、ClassNotFoundException、工作目录不对、环境变量没传进去等等。
这篇文章我会完整梳理这个报错背后的机制、常见的触发场景,以及对应不同根因的解决路径。不管你是刚学Maven不久,还是已经在生产项目里被这个插件折磨过,都能从里面找到能直接抄的解决方案。
1. 认识错误:exec-maven-plugin到底帮你做了什么
1.1 这个插件存在的意义
exec-maven-plugin在Maven生态里的地位很特殊。Maven本身只管依赖解析、编译、打包,它并不负责把你的Java程序“跑起来”。但你有时需要在构建过程中执行一下自己写的工具类、启动一个辅助服务、跑一段脚本,甚至把某个main方法临时拉起来做个联调——这时候就需要exec-maven-plugin出场。
它的两种核心能力:
exec:java:在Maven所在的JVM进程中直接执行一个Java类的main方法,不需要先打包成jar。exec:exec:把命令交给操作系统去执行,比如启动某个可执行文件、调用系统脚本,这种方式可以脱离当前JVM进程运行。
也就是说,这个插件是“构建”和“运行”之间的一个桥。你可能在pom.xml里显式调用了它,也可能是因为某个项目模板自带了exec相关的执行配置。无论哪种情况,它执行失败都会导致整个Maven命令失败。
1.2 为什么看到的是1.6.0这个版本
报错里出现的1.6.0是插件版本,这个版本在老旧项目中特别常见。它不是官方最新版,但很多项目的pom.xml至今还锁定着它。1.6.0放在今天看有两个先天问题:
第一,它发布时Java 8是主流,没有充分考虑后续JDK版本在模块化、类加载、安全管理器方面的变化。在高版本JDK下运行,容易出现一些奇怪的兼容性问题。第二,它内部的默认配置方式比较简陋,很多参数需要你显式指定,一旦漏配就会出现看似莫名其妙的报错。
不过要注意,1.6.0本身只是“脚手架”,真正决定是否报错的还是它的执行目标。如果你在命令行运行mvn exec:java,可能报错的是你指定的mainClass不存在;如果你在mvn test或mvn package的生命周期里绑定了执行,那报错还会干扰其他阶段。
1.3 错误堆栈的解读思路
先看一个典型的报错长什么样:
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.6.0:java (default-cli) on project demo-service: Execution default-cli of goal org.codehaus.mojo:exec-maven-plugin:1.6.0:java failed: ClassNotFoundException: com.example.DemoApplication这句话里能提取出三个关键信息:
- 执行的目标是
:java。 - 绑定的执行ID是
default-cli,说明你是在命令行直接调mvn exec:java触发的,不是生命周期绑定。 - 失败原因是
ClassNotFoundException,后面跟着具体的类名。
看懂这个结构很重要。因为接下来你要去pom.xml里找的,就是那个exec插件的<configuration>块。很多人在这一步会去搜ClassNotFoundException怎么解决,但其实只要抓住报错里的类名,去项目里核对类是否存在、包名是否一致就知道了。
2. 排查路径:从报错日志反向定位根因
2.1 把完整的异常链打出来
遇到这种报错,第一件事永远是把mvn命令带上-e参数重新跑一遍。-e会让Maven把完整的堆栈异常输出出来,尤其是Caused by部分会指到最底层的问题。
比如这样:
mvn exec:java -e有时候你会看到这样的内容:
Caused by: java.lang.RuntimeException: Cannot find class com.example.DemoApplication或者:
Caused by: java.io.IOException: Cannot run program "python3": CreateProcess error=2, The system cannot find the file specified前者说明类路径有问题,后者说明系统PATH里找不到你指定的可执行程序。这两种根因的解决办法完全不同,不把Caused by拉出来就瞎改,大概率越改越乱。
2.2 mainClass找不到的典型场景
这是exec:java最常见的失败原因。报错提示ClassNotFoundException或者Cannot find class,实际项目里对应的场景往往就这几类:
mainClass里写的全限定名和你实际的包路径不一致。比如实际类是com.example.demo.Application,配置里写的是com.example.DemoApplication。- 类不在
src/main/java而在test目录、resources目录或者其他自定义源码目录下,Maven默认不会把它们编入classpath。 - 类名大小写写错。Java类名区分大小写,
application和Application是两个完全不同的类。
这里我建议你先在项目里手动确认一下类的确切位置:
find src -name "*.java" | xargs grep -l "static void main"这个方法能帮你快速定位哪个文件里有main方法,然后照着全限定名去配置。
2.3 Classpath被截断或依赖缺失
还有一种情况是类的全限定名完全正确,编译也通过,但运行时依然提示找不到依赖类。这类问题的根源不是你的类没了,而是classpath里缺少某些依赖。
exec:java在执行时会把当前项目的依赖全部拼进classpath。如果你是运行在IDE里,IDE通常会自动把依赖传进去,所以本地能跑;但命令行执行时,依赖没有被正确解析,就会抱错。常见原因:
- 项目打包类型是
pom或者依赖配置有问题。 - 某些依赖被标成
provided或optional,运行时没有传递进去。 - 本地私服或中央仓库下载依赖失败,
mvn静默跳过。
排查方法也不难,执行下面命令看依赖树:
mvn dependency:tree -Dverbose重点确认你需要的那个依赖是不是真的被打进来了。另外,如果你在插件配置里手动设置了<classpath>或<classpathScope>,那就要检查是不是把test等作用域的依赖漏掉了。
2.4 工作目录和外部程序路径问题
如果说mainClass问题是新手最容易遇到的,那工作目录问题就是老手也容易踩的坑。因为你用IDE跑和你用命令行跑,当前工作目录可能完全不同。
exec:java默认的工作目录是项目根目录,也就是${basedir}。当你的程序依赖读取某个相对路径文件时,比如config/app.properties,如果它不存在于当前执行目录下,就会报FileNotFoundException。但日志会被JVM封装成Caused by: java.io.FileNotFoundException,很多人的思路会卡在“为什么文件明明在项目里却找不到”。
这种情况的解决思路是:
- 确认程序读取的是什么路径。
- 在插件配置里显式指定
<workingDirectory>。 - 让程序改为读取classpath下的资源,而不是文件系统的相对路径。
如果使用的是exec:exec执行外部命令,那么还会遇到操作系统PATH找不到命令的情况。Windows和Linux的PATH解析规则不同,我在Windows上踩过一次python命令找不到的坑,后来发现是没加.exe后缀,且Python没有自动加入系统PATH。
3. 解决故障:按场景逐个击破
3.1 修复POM配置:三个关键属性
如果你是第一次接触这个插件,建议直接把配置模板写到pom.xml里,放到<build><plugins>下。以下配置适用于最常见的exec:java场景:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.1.0</version> <configuration> <mainClass>com.example.demo.Application</mainClass> <classpathScope>runtime</classpathScope> <cleanupDaemonThreads>false</cleanupDaemonThreads> </configuration> </plugin>三个关键属性:
mainClass:必填,指定要运行的入口类。classpathScope:控制classpath的作用域,不写默认是runtime。如果你还想把test目录的类和依赖也包含进来,改成test。cleanupDaemonThreads:当你的程序里有非守护线程残留时,设为false可以避免构建卡住不动。
这里要特别说下版本问题。我不建议继续用1.6.0,直接升级到3.1.0或更新版本即可。3.x系列修复了大量JDK9+兼容性问题,也支持更清晰的配置结构。如果你是因为公司老项目不能随便升版本,那至少要确保Java版本和插件版本之间的匹配关系。
3.2 升级后从命令行验证
改完pom.xml后,不要急着跑到IDE里点运行按钮。先在命令行来一次干净验证,这样能看到最真实的输出:
mvn clean compile exec:java -Dexec.mainClass=com.example.demo.Application注意我在命令行里临时覆盖了mainClass,这个技巧很有用。如果你不想改POM,或者想在多个入口类之间快速切换,-Dexec.mainClass是你最好的帮手。它相当于临时传一个属性,优先级高于POM里的默认配置。
如果还想改变传给main方法的参数,用:
mvn exec:java -Dexec.mainClass=com.example.demo.Application -Dexec.args="--server.port=8081"3.3 处理Java进程与路径问题
升级插件后如果还报错,就按Caused by的提示继续往下挖。
如果是UnsupportedClassVersionError,说明你编译的字节码版本比运行时JDK高。比如项目用JDK17编译,但你用JDK8执行mvn exec:java。这种情况要检查JAVA_HOME是否指向正确的JDK:
echo $JAVA_HOME java -version mvn -version三个命令的输出必须一致。如果mvn -version显示的Java version不对,说明Maven使用的是别的JVM环境,需要在环境变量里修正JAVA_HOME,或者把正确JDK的bin目录放到PATH最前面。
如果是FileNotFoundException,定位是读取外部配置文件导致的,那就给你的程序加上启动参数,指定绝对路径,或者改掉程序里读取资源的方式:
// 不要这样 new File("config/app.properties"); // 建议改成classpath加载 getClass().getClassLoader().getResourceAsStream("app.properties");如果你必须依赖工作目录下某个文件,那就把<workingDirectory>显式指出来:
<configuration> <workingDirectory>${project.basedir}</workingDirectory> </configuration>4. 更优做法:实际项目里的稳健执行方案
4.1 exec:java还是exec:exec
这个问题很多人纠结,我直接给判断标准:
- 如果你要运行Java类,且它不依赖外部系统环境,就选
exec:java。 - 如果你要执行脚本、启动原生程序、激活虚拟环境、调用系统命令,选
exec:exec。
exec:exec的配置方式如下:
<configuration> <executable>python3</executable> <workingDirectory>${project.basedir}/scripts</workingDirectory> <arguments> <argument>demo.py</argument> </arguments> </configuration>注意exec:exec需要你配置<executable>,它的取值会被系统解析。如果你不确认程序的绝对路径,可以用which命令先查一下:
which python3把输出结果直接填到<executable>里,能省掉很多环境变量问题。
4.2 和常用框架的集成模式
如果你的项目是基于Spring Boot的,你会发现exec:java和spring-boot:run都可以启动应用,区别在于:
spring-boot:run会经过Spring Boot的启动器,自动处理资源目录、DevTools热部署等。exec:java只是最朴实地把main方法拉起来,不会做特殊处理。
所以如果是Spring Boot项目,我推荐优先用spring-boot:run,它不是exec插件,但两者的报错结构有相似之处。如果你确实需要exec:java,那有一点要特别小心:Spring Boot应用如果被中断后留下非守护线程,可能导致构建一直挂起。在配置里设<cleanupDaemonThreads>false</cleanupDaemonThreads>,并且确认你的应用能正常退出。
还有一类场景是在集成测试阶段需要在测试前启动一个服务,测试完再杀掉。exec-maven-plugin可以结合maven-failsafe-plugin使用,通过<executions>把执行绑定到pre-integration-test和post-integration-test阶段。这里核心思路是定义一个start一个stop,两个目标用同一个配置块管理。
4.3 入参传递和资源加载的注意事项
你以后一定会遇到需要传参数给main方法的情况。直接用:
mvn exec:java -Dexec.mainClass=com.example.cli.Generator -Dexec.args="--format json --output out/"如果你在Windows的命令行下传参,引号里面的空格会被拆分,记得用双引号把整个args包起来。如果参数里有中文字符,还可能出现编码问题,这时可以在POM里加上:
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>另外,如果你的程序需要读取JVM参数,比如-Xmx,需要修改exec:java的<jvmArgs>:
<configuration> <jvmArgs> <jvmArg>-Xms512m</jvmArg> <jvmArg>-Xmx1g</jvmArg> </jvmArgs> </configuration>在命令行临时覆盖时用:
mvn exec:java -Dexec.jvmArgs="-Xms512m -Xmx1g"注意Windows下多个JVM参数之间的关系,少一个空格都会导致传入参数异常。
5. 常见问题速查表与兜底方案
5.1 高频场景对照表
| 现象 | 可能根因 | 解决动作 |
|---|---|---|
ClassNotFoundException: xxx.xxx.Xxx | mainClass写错或类不存在 | 核对全限定名,用find确认类位置 |
Cannot find class | 类未被编译进入target | 先执行mvn compile,检查源码目录 |
UnsupportedClassVersionError | JDK版本不匹配 | 统一JAVA_HOME并重新编译 |
FileNotFoundException | 工作目录不对或路径错误 | 显式配置workingDirectory或用classpath资源 |
Cannot run program "...": error=2 | 系统找不到可执行程序 | 在配置中使用绝对路径解析executable |
| 构建一直挂起 | 残留非守护线程 | 设置cleanupDaemonThreads为false |
osdetector failed | Maven自身版本过旧 | 升级Maven到3.6.3以上 |
| 插件标签报红 | 插件版本太旧不兼容 | 升级到3.x最新版本 |
5.2 上面全试过还是不行?
这种情况我最常遇到的是两件事:
第一,Maven安装目录里有全家settings.xml配置拉低了插件版本,或者本地仓库里缓存过损坏的插件。手动清理对应目录再重新拉取:
rm -rf ~/.m2/repository/org/codehaus/mojo/exec-maven-plugin mvn -U clean compile exec:java第二,项目存在父子模块,但子模块没有正确继承插件配置,导致它找不mainClass。检查父级POM的pluginManagement里是否定义了版本,而子模块是否真的用了这个插件。
如果你已经跑到了这一步,我的建议是换个路径重新跑一次,用一个极简的测试类验证你的环境是不是干净的:
public class QuickStart { public static void main(String[] args) { System.out.println("exec plugin is working"); } }放在src/main/java下,然后在命令行执行:
mvn compile exec:java -Dexec.mainClass=QuickStart如果这个都能跑成,说明环境没毛病,问题出在你的具体项目配置上。如果不能跑成,那就是构建环境本身需要考虑重置依赖或修复Maven安装。
最后再分享一个小技巧:遇到这类插件报错,不要总想着“换一种构建工具”,也不要第一反应去改代码。先把Maven的完整输出贴到文本编辑器里,高亮所有Caused by行,这会直接帮你定位到真正的底层原因。很多时候你离答案并不远,只是被最上面那一行红字吓到了。