ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

IDEA中使用mvn exec:exec启动SpringBoot项目实战指南

IDEA中使用mvn exec:exec启动SpringBoot项目实战指南 作为一名常年跟SpringBoot和IDEA打交道的Java开发我太清楚这个场景了项目在IDE里点绿箭头启动一切正常但换了台机器、或者要在CI环境里模拟启动、或者想验证某个命令行参数对启动过程的影响时跑默认的spring-boot:run或Application主类总有各种水土不服。最后折腾一圈发现用mvn exec:exec反而是最干净利落的方式。这篇内容就围绕这个主题展开为什么会有这种需求、exec:exec和exec:java到底差在哪、IDEA里怎么配置才能让SpringBoot项目通过mvn exec:exec跑起来以及我在真实项目里踩过的那些坑和解决办法。无论你是刚接触SpringBoot的新手还是被IDEA启动行为折磨过的老手这篇内容都能给你一套可直接抄作业的方案。1. 为什么放着IDEA的Run按钮不用非要折腾mvn exec:exec1.1 什么场景下必须放弃IDEA默认启动方式先说个结论IDEA自带的SpringBoot启动方式右键Application类直接Run在绝大多数日常开发里没有任何问题这是最高效的方式。但你总会遇到一些它搞不定的场景我列一下我实际遇到过的启动逻辑不在SpringApplication.run里。比如项目里有一段自定义的启动前置逻辑要通过public static void main(String[] args)里的某种条件判断来决定是否加载Spring容器或者你用了自定义类加载器需要在Spring容器创建前干预类加载行为。这时右键跑主类没问题但问题是你可能需要给JVM传很长的-D系统参数或者--spring.profiles.active之类的参数IDEA的Run Configuration里配置这些字符串很容易出错尤其是参数里包含特殊字符和嵌套引号的时候。依赖Maven的动态classpath。IDEA的Run方式默认会把依赖解析到自己的模块依赖列表里这跟Maven仓库里实际解析出来的classpath并不完全一致个别依赖冲突场景下会出诡异问题。通过Maven启动能保证跟命令行环境的行为完全一致。预演生产启动方式。生产环境通常是用java -jar加上一堆JVM参数直接跑的但本地开发时直接打jar包再跑太慢。mvn exec:exec可以通过命令行参数精确控制JVM启动参数和主类能更接近生产环境的启动方式。脚本化启动。你需要写个脚本一键启动多个微服务项目或者按特定顺序启动。IDEA的Run按钮帮不了你但Maven命令可以。所以结论是IDEA里点Run是日常主力但当你需要精确控制启动过程时mvn exec:exec是那个更可靠的备选。它能把用Maven命令行启动这件事完完整整地搬进IDEA里让你既享受IDEA的编辑体验又享受Maven的构建一致性。1.2 IDEA的Run Configuration与Maven启动的底层差异那为什么直接跑main方法会跟Maven启动有差异核心在classpath的构建方式上IDEA RunIDEA内部维护了一套模块依赖模型它依据pom.xml、Gradle脚本和本地库解析出依赖列表但解析顺序和冲突处理策略就按它自己那套逻辑来。大多数时候没问题但遇到多个版本冲突、provided依赖和optional依赖混在一起时IDEA处理的结果可能跟Maven的mvn dependency:tree结果不一致。Maven严格按pom.xml的依赖树、dependencyManagement里的版本锁定和依赖仲裁规则来解析结果可预测、可复现。Maven启动后给你的是一个完全符合POM定义的classpath。所以当你怀疑代码在IDEA里能跑但打包后跑不了这类问题时用Maven启动方式去复现问题是一个值得养成的排查习惯。这背后的本质是构建工具与IDE工具在类路径语义上的差异。顺带说一句IDEA从2020版之后已经把Maven的import做得相当好了大多数情况两者一致但大多数不等于所有。2. exec:exec和exec:java到底差在哪2.1 两个目标的核心语义区别Maven的exec插件有两个跟启动Java程序相关的目标exec:java和exec:exec。名字相似行为差异非常大。我当年在这上面吃过亏——用exec:java配置了半天发现动态传参特别别扭换成exec:exec才找到正解。做一个表格直观对比维度exec:javaexec:exec运行方式在Maven进程中直接以Java类方式运行不重新创建外部进程在独立的新进程中执行指定命令可以执行任何命令只需指定command类加载隔离与Maven插件共享JVM类加载器和System属性环境都会受Maven进程影响完全独立的新进程不共享Maven的JVM状态是否可设置-classpath通过插件的classpathScope间接控制主类只能指定为Java类文件可以执行java可执行文件并显式传入-cp、-D等完整JVM参数配置复杂度简洁只需指定mainClass繁琐需要明确command、classpath和参数适用场景快速跑一个简单Java类不需要复杂JVM参数需要完整控制JVM参数、环境变量、工作目录、输出重定向的复杂场景或启动非Java程序进程退出行为与Maven进程绑定System.exit会触发Maven输出异常独立进程退出Maven返回对应退出码一句话总结exec:java是在Maven肚子里运行你的类exec:exec是让Maven替你开一个全新的外部进程来运行你的程序。2.2 为什么启动SpringBoot更推荐exec:execSpringBoot的启动是典型的需要完整控制JVM的场景SpringBoot应用通常需要设置-Dfile.encodingUTF-8、各类日志框架的系统属性、内存参数等用exec:java设置这些参数要么得通过systemProperties配置要么会弄脏Maven进程自身的JVM状态很不干净。SpringBoot应用的main方法执行完SpringApplication.run后会在主线程里保活因为SpringBoot的Web容器会持有非守护线程。Spring应用用来处理外部配置的特殊类加载器比如LaunchedURLClassLoader只在fat-jar启动时出现本地开发时我们一般用普通classpath但exec:exec的独立进程模式能更好地模拟真实JVM环境排查一些本地能跑部署后不行的问题时效果更好。所以当我想用Maven启动且希望完全掌控JVM行为时我会优先考虑exec:exec。它本质上是这样一条命令java [JVM参数] -cp [全量classpath] com.example.YourApplication [应用参数]其中classpath的生成依赖Maven来算而JVM参数和应用参数完全由你在配置里自由控制。要做到这一点需要Maven帮我们生成classpath并传给exec插件下面会详细讲配置。3. pom.xml里的关键配置让exec插件知道怎么拉起你的SpringBoot3.1 基础配置与完整示例你需要在项目的pom.xml中增加exec-maven-plugin的配置并注意这个是放在build/plugins下面不是放dependencies里。我贴一份我实际项目里的配置按用途做了注释build plugins plugin groupIdorg.codehaus.mojo/groupId artifactIdexec-maven-plugin/artifactId version3.1.0/version executions !-- 如果你想在mvn package阶段自动执行可以配置execution如果只是IDEA里手动跑就不需要executions -- /executions configuration !-- 方式一exec:exec这是你要的重点 -- exec executablejava/executable arguments argument-Dfile.encodingutf-8/argument argument-Xmx1024m/argument argument-classpath/argument classpath/ argumentcom.example.YourApplication/argument argument--server.port8089/argument /arguments /exec /configuration /plugin /plugins /build这里面的关键点executablejava/executable表示最终执行的是java命令如果你机器的java在PATH环境变量中可被直接找到直接写java如果需要在特定JDK下运行这里可以写JDK绝对路径如/usr/local/jdk1.8.0_281/bin/javaWindows下写C:\Program Files\Java\jdk1.8.0_281\bin\java.exe实测用绝对路径时建议用正斜杠转义避免反斜杠被Maven当转义符吃掉。arguments里的-classpath和classpath/是个组合标记Maven会把它替换成当前项目解析出来的全量classpath这是exec:exec最核心的变量。很多人在IDEA配置时报Error: Could not find or load main class基本都是这一块没配对。最后一个参数--server.port8089会被直接传给SpringApplication作为应用参数等价于在运行命令里指定端口覆盖application.properties。如果改用exec:java配置会简单些configuration mainClasscom.example.YourApplication/mainClass systemProperties systemProperty keyfile.encoding/key valueutf-8/value /systemProperty /systemProperties /configuration但你要设置多种JVM启动参数时systemProperties配置项的可读性远不如exec:exec的arguments列表清晰这也是我后来坚定用exec:exec的原因。3.2 classpath的计算方式与scope陷阱有一个非常重要的细节classpath/默认只包含项目依赖中compile和runtime scope的包不会包含provided、test scope的依赖。这句话背后来头很大。SpringBoot项目里有时用provided依赖比如某些只在编译期生效的注解处理器如果你用exec:exec启动时发现某个类报NoClassDefFoundError先别急着怀疑代码第一反应应该去看这个类所在的依赖的scope是不是provided或test启动了。这时候有几种解法用classpathScopecompile/classpathScope参数但默认为compile问题不在这或写成test来扩大classpath范围classpathScopetest/classpathScope这样会把Test目录下的类和测试依赖也加入classpath。另一个与此相关的常见设置是includePluginDependenciestrue/includePluginDependencies它不是设置classpath范围而是将插件的依赖比如某些需要出现在编译环境中的插件扩展也加入classpath非特殊需求不用开。使用maven-dependency-plugin先把依赖拷贝到指定目录再用java命令加-Djava.ext.dirs去加载就是一通更底层的骚操作。我不建议新项目这么做绕远了。对确实需要的依赖把scope改回默认compile。如果这个依赖本来就是只在编译期需要而运行时你又非要它出现在classpath里说明你依赖设计有需求上的冲突点这种情况建议直接在pom里审查依赖归属。另外如果你项目里用了SpringBoot的spring-boot-maven-plugin的repackage它的fat-jar结构会覆盖普通classpath启动逻辑。这时用exec:exec启动的classpath与打出来的jar包内部结构不是一回事所以不要用exec:exec去验证fat-jar的问题。验证fat-jar启动还是老老实实java -jar target/xxx.jar。3.3 把端口、profile等运行参数写进配置并支持动态覆盖开发时经常要切换端口、切换环境dev/test/prod。我习惯在pom里定义一组Maven的profile。profiles profile iddev/id properties spring.profiles.activedev/spring.profiles.active server.port8080/server.port /properties /profile profile idprod/id properties spring.profiles.activeprod/spring.profiles.active server.port8081/server.port /properties /profile /profiles然后在exec插件的arguments里引用它们argument--spring.profiles.active${spring.profiles.active}/argument argument--server.port${server.port}/argument这样在IDEA里切换Maven profile后启动配置能跟着环境切不用每次手改端口。这套玩法还适用于数据库连接串、日志级别等环境相关配置减少本地开发时的无效操作。4. IDEA里建一个能跑的Maven运行配置4.1 直接在IDEA里配置Maven运行任务的步骤标题说的是IDEA启动SpringBoot项目时使用mvn exec:exec启动实操核心就是在IDEA里新增一个Maven类型的Run/Debug Configuration让它执行exec插件的exec目标。步骤我给得很细跟着走基本不会出错打开IDEA右上角的Add Configuration...点击左上角的号往下找到Maven类型。Name处填一个易懂的名字比如Start App (exec:exec)。Working directory工作目录选择项目根目录默认就是但也检查一下别选成子模块。在Command line输入框里填exec:exec。注意这里不要填mvn exec:exec因为IDEA的Maven配置自动带mvn前缀如果填错了会尝试运行一个叫mvn exec:exec的goal导致报Invalid goal错误。如果需要激活某个profile比如前面说的dev环境在Profiles输入框填dev。在Environment variables里可以配置环境变量比如JDK_JAVA_OPTIONS-Dfile.encodingUTF-8这个在Windows控制台乱码时格外有用。在Runner页签下JRE选择项目使用的JDK版本。这里有个容易忽略的点IDEA的Maven Runner里选择的JRE决定了Maven进程本身的JVM但exec:exec启动的java命令用的是你executablejava/executable解析到的哪个java。如果你环境变量PATH里的java是JDK 8而Maven Runner用的是JDK 17那么最终运行SpringBoot的JVM是PATH里的那个跟Maven的JRE无关。想要精确一致建议配置里把executable写成绝对路径或者在IDEA的Environment variables里设置JAVA_HOME指向目标JDK。点击Apply保存。之后点IDEA右上角的小绿锤Debug按钮的左边那个旁边下拉选择这个配置点击运行就能看到控制台输出SpringBoot启动日志了。4.2 使用Application类型的配置配合Maven命令行这里额外给一个技巧如果你不想为了Maven启动单独建一个配置也可以直接使用IDEA的Application类型配置把Main class指向你的Application类。但这跟标题要求的用mvn exec:exec启动本质不同它绕开了Maven构建环节。如果非要两者兼顾可以这样做新建一个Shell Script类型配置脚本内容为mvn exec:exec工作目录指定项目根目录。不过这会把IDEA的控制台输出跟Maven日志糅在一起体验没有原生的Maven配置干净。实测下来原生Maven配置最稳。4.3 IDEA里mvn命令找不到怎么办热搜词里有一条非常典型mvn : 无法将“mvn”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。很多人想在IDEA的Terminal窗口里跑mvn命令结果提示找不到本质是Maven的bin目录不在系统PATH环境变量里。这跟IDEA的Maven配置是两码事IDEA的Maven配置走的是IDEA内部集成的Maven不需要PATH里有mvn但你在IDEA内嵌终端Terminal里敲mvn时走的是系统Shell系统Shell找的是操作系统的PATH。解决办法很简单在系统环境变量里把Maven的bin路径加进去Windows此电脑-属性-高级系统设置-环境变量在Path变量中新增Maven解压目录的bin路径比如D:\apache-maven-3.9.6\bin。配完后记得重启IDEA因为IDEA启动时的环境变量是从系统读取的不重启它拿不到新的PATH。macOS/Linux编辑~/.bashrc或~/.zshrc加入export MAVEN_HOME/path/to/apache-maven-3.9.6 export PATH$MAVEN_HOME/bin:$PATH然后source一次。另外提醒一句在IDEA的Terminal里它默认用的是项目配置的ShellWindows下可能是PowerShellPowerShell里测试mvn -version成功就说明PATH配好了。配好后Terminal里跑mvn exec:exec也能跑前提是项目pom里配置好了exec插件但这样启动的进程不受IDEA的Run配置管理断点调试功能没法用。想要Debug就得用IDEA的Maven运行配置然后点Debug按钮这样能在SpringBoot启动过程中打上断点。5. 跑不起来才是常态环境变量、依赖冲突与日志排查5.1 Error: Could not find or load main class的排查链路这是我用exec:exec时遇到频率最高的报错。直接给排查思路照着走能覆盖90%的场景确认主类名有没有写错。注意类名是全限定名比如com.example.YourApplication。确认classpath/是不是真的被替换成了正确的classpath。可以在IDEA的Maven面板中选中项目双击exec:exec运行一次后查看Run窗口上方的Command line详情IDEA会把最终的命令行打印出来看-classpath后面跟着的内容是否包含你的项目输出目录target/classes和你依赖的所有jar包路径。如果classpath里连target/classes都没有检查pom.xml里的classpathScope。检查编译输出。先手动跑一次mvn compile确认target/classes下确实有.class文件。很多时候是代码有编译错误但IDEA没报、终端又没跑编译导致启动时classpath指向了一个不存在的输出目录。检查exec:exec运行时的working directory。如果你的启动参数中用到了相对路径比如读取相对路径下的配置文件工作目录不对会导致启动后找不到文件表现可能不是NoClassDefFoundError而是各种诡异的配置加载失败。IDEA里Maven配置的工作目录默认是项目根目录特殊模块项目建议直接显式设置为模块目录。5.2 使用System.getProperty与Spring的Environment做对齐exec:exec的机制决定了它跟SpringBoot的配置加载有一些配合上的细节。比如我想给日志框架指定配置文件路径直接在arguments里加-Dlogging.config/absolute/path/logback-spring.xml就行。但如果你用exec:java这个-D系统属性会设置到Maven进程里SpringBoot的日志系统初始化时读的是当前JVM的System properties造成的效果是Maven进程的属性被改了但SpringBoot应用运行在Maven进程里所以也能读到。在exec:exec里这个属性直接传给新JVM二者语义一致但隔离更彻底。若你在IDEA的VM options里设置了-D参数那只在IDEA Fork出的进程里生效不会跑到Maven进程里理解三个进程IDEA、Maven、你的应用的边界会减少很多迷茫。有一点要注意SpringBoot的SpringApplication会优先使用命令行参数其次读取application.properties。通过exec:exec的arguments传参时凡是--开头的如--spring.profiles.activedevSpringBoot会把它们当作ApplicationArguments优先级高于配置文件。换句话说你用Maven启动时传的值能覆盖application.properties里的同名配置项这属于SpringBoot的官方设计按这个逻辑排查就够了。5.3 日志乱码和编码问题用mvn exec:exec启动后控制台中文日志乱码几乎每个人都会撞上。直接给结论乱码的核心是运行java命令的JVM默认字符集与IDEA控制台解码字符集不一致。我通常的做法组合拳在exec插件的arguments里加上argument-Dfile.encodingUTF-8/argument argument-Dconsole.encodingUTF-8/argument argument-Dsun.jnu.encodingUTF-8/argument注意sun.jnu.encoding这个属性比file.encoding更底层的控制JVM对文件名的编码解析在Windows上尤其关键。IDEA里的Help-Edit Custom VM Options加上一行-Dfile.encodingUTF-8确保IDEA自身的文件读取和终端输出使用UTF-8。如果还乱码检查Help-Edit Custom Properties加idea.jdk.encodingUTF-8这套组合大多数场景能解决但如果你项目里日志文件本身是GBK编码写的那控制台怎么配都没用——先把编码标准统一到UTF-8这是所有国产软件项目最该早做的一件事。5.4 隐藏问题插件版本与JDK版本不兼容最后一类坑也是比较容易让人抓狂的exec-maven-plugin版本与JDK版本不兼容。老版本exec-maven-plugin比如1.6.0、3.0.0之前的一些版本在JDK 11环境上跑SpringBoot时偶尔会出现奇怪的HttpMessageNotReadableException或启动验证异常其实并不是SpringBoot的问题而是插件fork进程时的字节码处理兼容性。我实测把插件升到3.1.0或3.2.0后很多莫名奇妙的启动问题自动消失了代价只是几分钟的版本升级和一次mvn clean verify回归验证。所以排查顺序我给一个优先级建议主类名对不对 - classpath是否带上了依赖 - 工作目录对不对 - JDK版本混用问题 - 插件版本问题。前面三步能解决90%的go wrong后面两步是锦上添花。6. 别只盯着startexec:exec这个入口还能干更多事6.1 用exec:exec跑SpringBoot的单元测试装配启动SpringBoot不只是为了跑main方法。有时你想验证某个Spring上下文环境但不想起完整Web服务可以在arguments里把主类换成测试环境专用的Runner类。比如写一个LocalContextTesterpublic class LocalContextTester { public static void main(String[] args) { SpringApplication app new SpringApplicationBuilder(YourApplication.class) .web(WebApplicationType.NONE) .run(args); } }然后在pom.xml的exec插件arguments中把主类从YourApplication换成com.example.LocalContextTester。这样通过mvn exec:exec就能快速起一个非Web的Spring上下文做定时任务调试或消息消费者联调比改配置文件再起完整服务高效得多。6.2 组合Maven lifecycle实现先构建后启动有时候你改了代码希望在启动前自动重新编译。单纯exec:exec不会自动编译IDEA的build实际上已经帮你编译了但如果你从纯命令行跑mvn exec:exec它就真的不编译。两种解决思路在IDEA的Maven配置里把Command line从exec:exec改成compile exec:exec。这样每次触发该配置时会先执行compile生命周期再执行exec目标并且构建过程可见推荐。或者配置exec插件的executions绑定到某个phase上实现自动执行。比如executions execution goals goalexec/goal /goals phasetest/phase /execution /executions这个不太常用因为每次mvn test都会拉起SpringBoot性能代价太高但用在我想在集成测试阶段启动一个真实服务的场景里是合理的。6.3 与其他Maven插件的组合生成classpath文件后用exec:exec执行如果你有更复杂的classpath需求也还有一个进阶玩法与maven-dependency-plugin的build-classpath目标结合mvn dependency:build-classpath -Dmdep.outputFilecp.txt这个命令会把全量依赖classpath写入cp.txt文件。接着在exec:exec里通过$(cat cp.txt)之类的方式引用classpath你需要一个shell环境这样在特殊场景下能拿到完全自定义的classpath。不过这个方案在Windows上有点别扭要配合PowerShell语法。大多数项目不需要这么绕了解有这么个能力就够了。6.4 在IDEA之外使用exec:exec写完IDE配置后有些团队喜欢在CI或运维脚本里统一用Maven启动服务做冒烟测试。这时exec:exec的价值就凸显出来了——它能让开发、测试、CI三方使用完全一致的启动方式而不是各写各的命令。在CI里跑冒烟测试时mvn -q compile exec:exec -Dexec.executablejava \ -Dexec.args-cp %classpath com.example.YourApplication --server.port18080注意这种命令行传参方式里%classpath是exec插件的命令行占位符它会由插件替换为正确的classpath这种方式在IDEA里同样可以写进Maven配置的Command line。如果你不方便在pom.xml里写配置命令行传参反而更灵活适合临时跑一个验证。7. 我在真实项目里总结的几条启动经验最后说几条这两年折腾SpringBoot启动方式积累下来的经验算是给这块内容收个尾。第一exec:exec不是要替代IDEA的Run它是那个武器库里多一把趁手工具。日常开发用IDEA Run最顺手一旦遇到环境差异、依赖诡异、需要完全掌控JVM参数时再切过来。我用两个配置并存的方式IDEA里同时保留SpringBoot的Application配置和Maven exec:exec配置各司其职。第二给exec:exec配置写注释。pom.xml里插件的arguments列表一长隔三个月自己都忘了哪段参数是干嘛的。我习惯每个argument一行并在上方写清楚这是给谁的、为什么给。将来接手这个项目的人会因为这几行注释少翻很多源代码。第三exec:exec里的classpath/是你最好的朋友但别把classpathScope随便改成test。改了之后classpath范围扩大能掩盖依赖隔离问题让本不该出现在运行期的东西混进来不利于发现真实的依赖冲突。保持默认的compile是符合Maven语义的。第四如果启动时SpringBoot的日志正常打印但应用在初始化过程中报错退出优先看是不是因为独立进程的工作目录指向了别的模块。多模块项目里尤其容易踩模块A的配置文件放在模块A的resources里但你的working directory指向了父目录导致file:开头的绝对路径读取失败。把工作目录设为模块目录问题立刻消失。第五也是最重要的一条如果你在IDEA里看到控制台输出跟平时不一样比如多了Maven的下载日志、没有IDEA那种快捷的自动重启这很正常因为exec:exec本来就是用Maven的方式启动。它给你的是更接近命令行的体验代价是放弃了IDEA Run方案里内置的DevTools自动重启。所以我一般在本地开发时还是会切回IDEA Run而exec:exec留在需要精确复现场景或排查构建问题时使用。想清楚它的定位就能跟它愉快共处了。
返回列表