ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

exec-maven-plugin 报错排查:从类找不到到构建环境修复

exec-maven-plugin 报错排查:从类找不到到构建环境修复

碰到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

这句话里能提取出三个关键信息:

  1. 执行的目标是:java。
  2. 绑定的执行ID是default-cli,说明你是在命令行直接调mvn exec:java触发的,不是生命周期绑定。
  3. 失败原因是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,很多人的思路会卡在“为什么文件明明在项目里却找不到”。

这种情况的解决思路是:

  1. 确认程序读取的是什么路径。
  2. 在插件配置里显式指定<workingDirectory>。
  3. 让程序改为读取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.XxxmainClass写错或类不存在核对全限定名,用find确认类位置
Cannot find class类未被编译进入target先执行mvn compile,检查源码目录
UnsupportedClassVersionErrorJDK版本不匹配统一JAVA_HOME并重新编译
FileNotFoundException工作目录不对或路径错误显式配置workingDirectory或用classpath资源
Cannot run program "...": error=2系统找不到可执行程序在配置中使用绝对路径解析executable
构建一直挂起残留非守护线程设置cleanupDaemonThreads为false
osdetector failedMaven自身版本过旧升级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行,这会直接帮你定位到真正的底层原因。很多时候你离答案并不远,只是被最上面那一行红字吓到了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进