Maven exec:java报错排查:从default-cli到Caused by的完整指南

Maven exec:java报错排查:从default-cli到Caused by的完整指南 1. 报错拆解exec-maven-plugin 与 default-cli 究竟是谁在说话先把这个报错放回它出现的场景里看。你在 IDEA 里写了一个带 main 方法的类想在 Maven 项目里快速跑一下或者在命令行敲了mvn exec:java又或者用 IDEA 的 Run Anything 窗口输入了某个 Maven Goal——紧接着 Build 面板就飘出一行[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.0.0:exec (default-cli) on project demo: ...很多人的第一反应是“Maven 坏了”重装、清缓存、重启三连折腾半天发现毫无变化。实际上 Maven 大概率没坏问题在于这行报错本身只是个“外包装”真正的错误原因还藏在后面的堆栈信息里。先说清楚这个报错里出现的几个角色。exec-maven-plugin是一个老牌的 Maven 插件归属org.codehaus.mojo组织它做的事情很简单在 Maven 构建过程中帮你执行 Java 类或者外部程序。官方文档里它的描述是“Executes Java and other programs in a separate process or the same JVM”翻译过来就是既能跑main方法也能跑系统命令。日常开发里最常见的用法是mvn exec:java -Dexec.mainClasscom.example.DemoMain或者直接在 pom.xml 里对插件的 goal 做绑定让它在某个生命周期阶段自动执行。3.0.0是插件版本。exec是插件暴露的 goal目标它和java、exec两个子命令的关系是这样的exec:java是在 Maven 当前 JVM 进程中直接运行 Java 主类exec:exec则是启动一个独立的外部进程去执行命令或类。报错信息里写的exec通常来自exec:exec但 IDEA 和命令行常见的exec:java也会产生同款错误签名。(default-cli)这个括号里的内容特别容易被忽略但它恰恰解释了“这个 goal 是从哪冒出来的”。当你在命令行直接输入mvn exec:java而没有在 pom.xml 里预先定义 execution 时Maven 会自动生成一个默认的执行 ID就叫default-cli。换句话说看到default-cli基本可以确定这次执行来自命令行或 IDE 的 Maven 面板而不是 lifecycle 阶段里的绑定额外配置。on project demo指的是当前执行失败的具体 Maven 模块名。如果你遇到的是多模块聚合项目还要注意它到底报的是哪个子模块——这个问题我在后文排查顺序里会再提。顺手补充一个容易搞混的知识点Maven 插件不光能在命令行手动触发还能绑定到 lifecycle 阶段。比如你在 pom 里这样写plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.0.0/version executions execution idmy-execution/id phasecompile/phase goals goaljava/goal /goals configuration mainClasscom.example.DemoMain/mainClass /configuration /execution /executions /plugin此时再执行mvn compile你会看到exec:java (my-execution)这样的日志而不是default-cli。区分这两者对下一步排查方向有很大帮助如果是default-cli那大概率是手抖写错了 mainClass 或者参数如果是自定义 ID 绑定了生命周期阶段那问题可能出在插件配置和项目生命周期设计上。2. Caused by 才是关键把隐藏异常从堆栈里挖出来“Failed to execute goal” 这行字本质上只是 Maven 在告诉你某个插件在执行时抛了异常整条构建链路断了。Maven 默认打印出的错误信息非常克制通常只有一行 ERROR最后跟着[Help 1]。很多新手就停在这行红色日志前拿着[Help 1]一通搜索越搜越迷茫。正确的做法是忽略第一行往下滚找 Caused by。我用一个真实场景来说明。假设我在一个新项目里执行mvn exec:java -Dexec.mainClasscom.example.HelloWorld控制台打出来的完整报错可能长这样[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:3.0.0:exec (default-cli) on project demo: The parameters mainClass for goal org.codehaus.mojo:exec-maven-plugin:3.0.0:exec are missing or invalid [ERROR] [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch. [ERROR] Re-run Maven using the -X switch to enable full debug logging. [ERROR] [ERROR] For more information about the errors and possible solutions, please read the following articles: [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/PluginExecutionException看清楚上面这个场景里“Failed to execute goal” 的下一行直接就告诉了真实问题mainClass缺失或无效。也就是说exec 插件在执行时根本不知道要运行哪个类。解决方案很简单pom.xml 里显式配置 mainClassplugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.0.0/version configuration mainClasscom.example.HelloWorld/mainClass /configuration /plugin或者执行时通过-Dexec.mainClass传入。但更多情况下mainClass已经配好了却依然报Failed to execute goal。这时候就一定要加参数看完整堆栈了。Maven 自己都提示了用-e或-X可以拿到完整线索mvn exec:java -Dexec.mainClasscom.example.HelloWorld -e-e是 error 模式的堆栈-X是 debug 模式日志量非常大平时用-e就够了。加完参数后再看通常会在堆栈中下部看到真正的Caused by: xxxException。我归纳了一下常见的真正的异常类型主要有这几种Caused by 类型含义常见触发原因ClassNotFoundException运行时找不到某个类mainClass 类名写错、依赖缺失NoClassDefFoundError类在编译期存在但运行期加载失败依赖 scope 问题、打包排除The parameters mainClass ... missing or invalidmainClass 没配或类不存在命令行参数拼写错误BindException: Address already in use端口被占用Spring Boot 等应用启动时端口冲突Command execution failed外部进程执行失败命令不存在、退出码非 0ExceptionInInitializerError静态初始化块炸了业务代码问题这里我想特别强调一点exec 插件的报错只是“门卫”它拦住的是后面那一堆业务异常。很多时候真正报错的是你自己写的main方法里的逻辑——比如 NPE、IO 异常、连接超时这些异常会被插件捕获并统一包装成Failed to execute goal然后错误地让开发者以为 Maven 出了问题。我见过一个印象很深的案例同事启动 Spring Boot 应用时一直报这个错排查了一下午最后发现是 8080 端口被另一个本地服务占了。堆栈里的Caused by: java.net.BindException: Address already in use才是真正的凶手。所以面对这个报错第一原则就是别修 Maven先看堆栈。3. exec:java 的 classpath 陷阱类找不到为什么如此高频如果说mainClass缺失是最基础的错误那类找不到ClassNotFoundException或NoClassDefFoundError就是最让人头大的错误。它的诡异之处在于项目明明能mvn compile通过IDEA 里代码也不飘红但一用 exec 插件运行就炸。根本原因是exec 插件运行 main 方法时的 classpath 和 Maven 编译期的 classpath 不完全一致。先理清一个基本概念。Maven 项目里的依赖有个scope属性常见的有compile、provided、runtime、test等。其中provided表示这个依赖在编译和测试时需要但运行时由外部容器或 JDK 提供。比如开发 Servlet 项目时servlet-api 通常就是provided。问题来了当你用mvn exec:java运行 main 方法时exec 插件默认构造的 classpath 会包含compile和runtime作用域的依赖但provided作用域的依赖不会被加进去。如果你在 main 方法里 import 了一个provided依赖的类编译没问题一运行就报ClassNotFoundException。举一个具体例子。假设 pom.xml 里有这样一段dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency然后你在 main 方法里写import javax.servlet.ServletContext; public class DemoMain { public static void main(String[] args) { System.out.println(ServletContext.class.getName()); } }mvn compile肯定能过因为编译期provided依赖是可见的。但mvn exec:java跑起来就会报NoClassDefFoundError。解决方案是把依赖的 scope 改成compile或者新建一个 profile 专门处理运行期的 classpath。另一个高频场景是依赖版本冲突。一个大型项目的依赖树深不见底某个类在编译期能解析到但运行时被另一个版本的 jar 抢了先导致NoSuchMethodError、NoClassDefFoundError这类看起来莫名其妙的错误。遇到依赖相关的问题我最常用的命令是mvn dependency:tree -Dincludescom.example:some-artifact-Dincludes可以精确过滤某个组织或某个模块的依赖输出结果会列出一棵树[INFO] - com.example:some-artifact:jar:1.2.3:compile [INFO] \- org.foo:bar:jar:2.0.0:compile [INFO] \- com.example:some-artifact:jar:1.0.0:compile (版本冲突实际可能被覆盖)看到版本冲突后有几种处理思路在 pom 里用dependencyManagement统一版本、用exclusions排除旧的传递依赖、或者直接在dependencyManagement里指定最终版本。这个处理逻辑对任何 Maven 项目都适用不只是 exec 插件场景。还有一个容易忽略的场景是父工程定义了 dependencies 但子模块没有继承干净。多模块项目中如果某个模块的主类依赖了兄弟模块的类而兄弟模块没被打进 classpath同样会报类找不到。这种情况建议在子模块 pom 里显式声明对兄弟模块的依赖或者编译时检查父 pom 的模块依赖关系。顺带提一下exec:exec和exec:java的选择。exec:java默认在 Maven 所在的 JVM 进程里直接运行classpath 由 Maven 构造exec:exec则 fork 一个独立进程更像你自己在命令行敲java -cp xxx YourMain。如果项目对 classpath 有非常麻烦的特殊要求我会用exec:exec配合commandlineArgs显式指定-cp虽然配置繁琐但灵活性高很多。需要特别小心的是某些项目在 main 方法里调用了System.exit()。如果用exec:java因为 main 跑在 Maven 进程里System.exit()会直接把整个 Maven 构建进程干掉IDEA 里通常表现为“Build 进程意外退出”。换成exec:execfork 出子进程后System.exit()只影响子进程就不会炸 Maven 本体了。4. 工作目录与控制台编码两个低调但高频的翻车点如果说类找不到是 80% 的人都会遇到的常规坑那工作目录working directory和控制台编码这两个问题就是那种“不遇到则已一遇到就卡半天”的隐藏雷区。先聊工作目录。exec 插件执行 main 方法时有一个大家都默认却容易忽略的设定进程的工作目录默认是模块的 basedir也就是存放 pom.xml 的那个目录。听起来可能觉得这有什么可说的问题往往出在 IDEA 的 Run Configuration 上。IDEA 里创建 Maven 项目的运行配置时会自动设置 Working directory 为$MODULE_WORKING_DIR$但开发者在排查问题过程中常常手动改过。还有一种情况更隐蔽你用命令行在项目根目录执行正常但用了mvn -f /path/to/pom.xml exec:java后工作目录可能变成你当前敲命令的目录而不是 pom 所在目录。举个例子。我在 main 方法里写了File configFile new File(config.properties);如果工作目录是模块根目录config.properties在根目录程序跑得好好的。但如果我把 working directory 换成了模块下的src/main/resources目录或者用 IDEA 的 Working directory 改成某个输出目录文件就找不到了抛FileNotFoundException然后又被包装成Failed to execute goal误导你往依赖方向排查。规避这个问题最靠谱的做法是不要在代码里依赖相对工作目录的路径。需要读取配置文件时优先用类路径资源InputStream in DemoMain.class.getClassLoader().getResourceAsStream(config.properties);这样无论工作目录在哪里资源都能被正确找到。如果项目确实需要指定工作目录可以在 exec 插件配置里显式设置configuration workingDirectory${project.basedir}/workingDirectory /configuration再来看控制台编码。这个问题的典型表现是main 方法里System.out.println(中文)控制台输出一堆乱码。这类问题本身不影响程序运行但如果你在 CI 或终端里通过断言匹配日志文本乱码就会直接导致判断失效。编码问题的根源是 Java 默认 charset 和项目文件编码不一致。Maven 自身有个最基础的项目属性project.build.sourceEncoding它控制着编译器读取源代码时的解码方式。如果 pom.xml 里没定义这个属性而项目文件是 UTF-8在中文 Windows 环境下很容易出现“编译时字符串字面量乱码 运行时乱码”的双重问题。建议在 pom.xml 的properties里显式声明properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties至于 exec 插件运行时 JVM 的file.encodingLinux 上受LANG环境影响Windows 上则跟随系统默认。如果实在遇到乱码可以在 exec 插件配置里加 JVM 参数configuration jvmArgs jvmArg-Dfile.encodingUTF-8/jvmArg /jvmArgs /configuration或命令行执行时直接传mvn exec:java -Dexec.mainClasscom.example.DemoMain -Dfile.encodingUTF-8IDEA 里如果控制台还是乱码可以在 Help - Edit Custom VM Options 里加一行-Dfile.encodingUTF-8重启 IDEA 基本能解决。这类问题最讨厌的地方在于它不会在构建日志里留下任何明显错误程序照样跑只是输出不符合预期而后续所有基于输出的判断都跟着错位。所以遇到莫名其妙的情况先排查编码和工作目录往往能省掉大量时间。5. 环境侧排查settings.xml、镜像仓库与 IDEA 的 Maven 面板前面几节讲的都是项目内部的问题但还有一类Failed to execute goal是环境层面的。这类问题有个鲜明的特征不是某个具体项目才有而是所有项目都报同一个错或者“同一个项目昨天还好好的今天突然就崩了”。先看镜像仓库。exec-maven-plugin 这个插件本身是从 Maven 仓库下载的。如果settings.xml里的镜像配置不当导致插件拉取失败Maven 也会把它包装成Failed to execute goal。最典型的现象是本地仓库一直缺org/codehaus/mojo/exec-maven-plugin/3.0.0目录或者日志里出现Could not resolve dependencies这样的关键字。国内开发者最常用阿里云镜像在 Maven 的settings.xml里配置一个 mirror 就好mirror idaliyunmaven/id mirrorOfcentral/mirrorOf namealiyun public repository/name urlhttps://maven.aliyun.com/repository/public/url /mirrormirrorOf配置成central只接管中央仓库的下载请求。如果你公司内部有私服需要把私服地址也考虑进去但平时个人项目用阿里云镜像就够了。配置完成后如果之前已经下载了损坏的插件缓存建议把本地仓库里org/codehaus/mojo/exec-maven-plugin整个目录删掉再重新解析一遍让 Maven 拉取完整的插件文件。再来看 IDEA 的 Maven 面板。IDEA 里有很多地方可以配置 MavenSettings - Build, Execution, Deployment - Build Tools - Maven。你要重点检查三个字段配置项说明常见问题Maven home pathMaven 安装目录指向了不存在的目录或过旧版本User settings filesettings.xml 文件路径指向了空文件或路径错误会导致私服/镜像不生效Local repository本地仓库目录如果手动指定了目录但目录里没有插件缓存会重新下载我遇到过一种情况IDEA 里 Maven home path 配置的是 IDEA 自带的 Maven但这个内置 Maven 版本偏老和一个需要较新插件的项目冲突构建时报奇怪的错。这时候改成自己安装的 Maven 版本问题立刻消失。还有一种“External Libraries 完全没有 Maven 依赖”的现象看着像编译环境坏了实际上就是 IDEA 还没正确加载依赖或者本地仓库地址变了导致所有依赖全部落空。这种情况先在 Maven 面板里点一下“刷新所有 Maven 项目”的图标Reload All Maven Projects如果还不行检查本地仓库目录是否被清理了。Maven 版本和 JDK 版本的兼容关系也不能忽视。exec-maven-plugin:3.0.0本身对 JDK 的要求不算高但如果你项目里用了高版本 JDK比如 17 或 21编译而 Maven 运行时的JAVA_HOME指向的还是 JDK 8构建时就可能出现各种不匹配的异常。用这条命令确认mvn -v看输出的 Java version 是否和你预期一致。如果不一致修改环境变量JAVA_HOME或者在 IDEA 里给项目配置对应的 JDK。环境侧排查的另一个方向是本地依赖缓存损坏。Maven 下载失败时会生成.lastUpdated后缀的文件后续构建看到这些文件就直接跳过重新下载于是出现“明明仓库有更新但项目永远拉到旧版本或报错”的情况。遇到这种找到对应目录把所有.lastUpdated文件删掉再执行mvn clean install -U-U强制更新快照版本和远程依赖排查环境问题时基本是必用的。6. 从报错到定位我稳定复现后总结的排查顺序前面内容比较发散有项目内部的坑也有环境侧的问题。为了避免每次遇到这个报错都从头试一遍我把自己的排查路径固定成了一套流程。每次踩这个坑我就按这个顺序走一遍大部分问题能在五到十分钟内定位。第一步看堆栈找 Caused by。不要盯着Failed to execute goal这行看超过三秒。直接往日志下方翻找到第一个Caused by或Exception。如果日志太短加-e参数重新跑一次。核心原则这个错误是包装出来的真实原因必定藏在异常链中。第二步确认 mainClass 配置。如果堆栈里出现The parameters mainClass for goal ... are missing or invalid说明 exec 插件没拿到有效的 mainClass。检查三种情况pom 里有没有配mainClass命令行有没有传-Dexec.mainClass类名是不是写错了包括包名。第三步定位是真找不到类还是依赖问题。如果报的是ClassNotFoundException或NoClassDefFoundError先判断这个类是自己项目的类还是第三方 jar 里的类。自己项目的类检查模块依赖关系第三方 jar 里的类用mvn dependency:tree查依赖树和冲突情况特别注意 scope 是provided的依赖。第四步排除业务异常和资源问题。看堆栈里有没有业务代码抛出的异常NPE、IO 异常、端口占用等。有则直接修业务逻辑别在 Maven 配置上浪费时间。同时快速确认端口有没有被占用、工作目录下相关的配置文件是否存在。第五步环境兜底检查。如果前面全部没问题进入环境排查模式。检查 Maven 设置、镜像仓库、本地仓库缓存可以尝试删掉.lastUpdated后mvn clean install -U或者用mvn -v确认 JDK 版本。第六步最小化复现。这是我压箱底的方法。当项目体积很大、依赖特别复杂、报错看得云里雾里时直接新建一个空白 Maven 项目只写一个输出 Hello World 的 main 类配好 exec 插件跑一次plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.0.0/version configuration mainClasscom.example.Main/mainClass /configuration /plugin如果空白项目能成功说明环境没问题问题一定出在项目本身的依赖或配置上如果空白项目也失败那十有八九是环境层面的问题。这个“最小化复现”的判断逻辑能把排查范围瞬间缩小一半远比在堆积如山的依赖里盲猜要高效。最后说一个我一直以来的习惯把常用的exec:java命令固化下来。因为每次敲-Dexec.mainClass很容易敲错还不如直接做成 IDEA 的 Maven Run Configuration或者写一行 Makefile/Scriptmvn exec:java -Dexec.mainClasscom.example.DemoMain -Dexec.args--configdev这样平时用起来无脑不容易因为参数敲错而再次触发这堆报错。实际上每次排查这类问题都是一次对 Maven 构建链路更深的理解踩过一次坑后以后再看到Failed to execute goal心里就会不慌——先看Caused by再对照依赖最后查环境这条路永远比盲目重装 Maven 靠谱得多。