ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IDEA Maven 项目打包:图形化与命令行生成可执行 jar 指南

IDEA Maven 项目打包:图形化与命令行生成可执行 jar 指南 现在 Java 项目做交付绕不开的一件事就是把代码变成一个能直接扔给运维、扔给测试、扔到服务器上跑起来的 jar 包。我在团队里带过几批新人发现大家卡住的地方往往不是业务代码写不出来而是 IDEA 里那个 Maven 面板点哪个按钮、命令行敲什么、打出来的包为什么java -jar一跑就报no main manifest attribute。这几个问题看起来小但每次都能耗掉半天时间。这篇就围绕 IDEA 里 Maven 项目的打包这件事把两种最常用、最省心的方式完整捋一遍顺带把普通 jar 和可执行 jar 的区别、依赖没打进去怎么处理、报错怎么查这些坑一起说清楚。不管你是刚接触 Maven 的在校生还是已经写了两年 CRUD 想搞明白打包链路的开发者看完基本能自己独立完成从代码到可运行包的整个过程。1. 打包之前先想明白Maven 到底在替你做什么1.1 jar 包的本质以及为什么会有两种打包路径很多人对 jar 的理解停留在一个压缩包的层面实际上它跟 zip 的差别很小——都是归档格式只不过 jar 里多了一个META-INF/MANIFEST.MF清单文件这个文件决定了这个包被别人拿去用的时候能不能找到入口类、依赖在哪里。你可以把它想象成一个快递箱箱子里装的是编译好的.class文件和资源文件而MANIFEST.MF就是贴在箱子外面的面单写着收件人是谁Main-Class、里面还有没有其他包裹Class-Path。理解这一点之后就能明白为什么打包会分出两条路。一种是把你的业务代码打成一个瘦 jar依赖仍然靠外部的 jar 或者本地仓库提供另一种是把所有依赖一起塞进同一个包里做成一个几十兆甚至上百兆的胖 jar拿到任何装了 JDK 的机器上都能跑。绝大多数 Spring Boot 项目走的是第二条路因为部署的时候没人愿意去lib目录里一个个对依赖版本。这两种打法在 Maven 里对应的是完全不同的插件配置选错了就会出现包能打出来但是跑不起来的尴尬局面。1.2 从 pom.xml 到 target 目录这条链路你该心里有数Maven 的生命周期是打包这件事的骨架。默认的default生命周期里顺序是validate → compile → test → package → verify → install → deploy。你在 IDEA 里双击packageMaven 并不是只执行最后那一步而是从validate开始一路往下跑到package为止。这就是为什么有时候你只想打个包结果它把单元测试全跑了一遍某个测试用例连了生产库直接失败包也就打不出来了。pom.xml里有一个容易被忽略的标签——packaging。不写的时候默认是jar写了war就会打出 war 包写了pom说明这个模块只是个聚合父工程根本不产出构件。我在做多模块项目的时候见过有人把父工程的 packaging 写成了 jar结果mvn package在父目录报一堆莫名其妙的错实际上父工程只该写pom。这个小标签决定了后续插件绑定哪个生命周期阶段出问题的时候值得先回来看一眼。target目录是整个打包过程的产物区classes是编译输出generated-sources是注解处理器生成的代码maven-status记录了编译元信息。理解这个目录结构后面排查配置文件没被打进包这类问题时就能直接进去看比漫无目的地翻 pom 高效得多。1.3 动手之前先把环境自检做一遍打包报错里有一大半跟环境有关。我给自己的检查清单是这么几条。第一JDK 版本和maven.compiler.source是否对齐用 JDK 8 去编译写了var的代码或者用 JDK 17 编译指定了 source 1.8 却引用了高版本 API 的代码都会在 compile 阶段挂掉。第二MAVEN_HOME和PATH是否配好命令行敲mvn -v能不能正常输出 Maven 版本和 JDK 版本这一步在 IDEA 的内置终端里同样要验证因为 IDEA 终端继承的环境变量有时和系统终端不是一套。第三settings.xml在哪。默认在用户目录/.m2/settings.xml但 IDEA 里可以单独指定一个位置两边不一致会导致命令行能下载依赖IDEA 里就是飘红。第四本地仓库的位置和磁盘剩余空间.m2/repository撑到几十个 G 是很正常的事磁盘满了会以一种极其隐晦的方式报错比如下载到一半的lastUpdated文件让后续重试全部失败。把这四件事确认一遍能省掉后面大量的猜测时间。2. 方式一IDEA 图形化面板一键打包2.1 Maven 侧边栏的操作路径与几个容易点错的按钮IDEA 右侧竖着那一列里有个 Maven 图标点开之后默认折叠着项目名展开会看到Lifecycle和Plugins两大块。Lifecycle下面列的clean、validate、compile、test、package、verify、install、deploy就是标准生命周期阶段双击其中任意一个就能触发执行执行结果会输出在底部的 Run 面板里。日常打包最常用的是先双击clean把 target 清掉再双击package这样能避免上一次编译的残留 class 混进新包里造成的灵异问题。这里有个很多人搞混的点package和install不是一回事。package只把构件生成到当前模块的target目录工程里其他模块如果依赖它是拿不到这个新包的。install会在package之后把构件安装到本地仓库~/.m2/repository多模块项目里被依赖的模块必须走install否则下游模块永远用的是旧版本。我见过有人改了公共模块的代码只点package然后死活不明白为什么业务模块跑起来还是老逻辑问题就出在这。还有一个按钮要慎用就是Plugins展开后那一长串插件目标。比如spring-boot插件下面的repackage你单独双击它它会尝试对已经存在的 jar 做二次加工如果没先package就会因为找不到源文件而报错。图形化面板的便利之处在于不用记命令但它并没有帮你做任何顺序上的保证顺序还是得自己想清楚。2.2 图形化方式下打包产物到底落在哪执行完package产物在模块根目录下的target文件夹里。普通 jar 一般叫模块名-版本号.jar比如demo-service-1.0.0.jar。如果项目用了 Spring Boot 的打包插件你会同时看到两个文件demo-service-1.0.0.jar和demo-service-1.0.0.jar.original。体积大的那个是可执行包体积小的.original是插件在repackage之前留下的原始包。很多人第一次看到.original会以为是垃圾文件顺手删了其实它有时候是排查问题的关键——对比一下两个包的MANIFEST.MF和内部结构就能看出可执行包到底被额外塞了什么。target目录还会生成maven-archiver/pom.properties里面记录着 groupId、artifactId、version这个文件在排查线上跑的到底是哪个版本的包时特别好用。另外像classes、test-classes这些中间产物也在里面清理的时候统一clean就行不用手动一个个删。如果你想确认包里的结构IDEA 里可以直接双击 jar 文件它会以目录树的形式展开你能看到BOOT-INF/classes、BOOT-INF/lib、META-INF这些目录。Spring Boot 的胖包结构跟普通 jar 不一样业务类和依赖都不在根目录而是被塞进了BOOT-INF下面这也是它必须靠org.springframework.boot.loader.JarLauncher启动的原因。搞清楚这个结构后面读启动日志的时候就能对上号。2.3 图形化打包适合谁什么时候该换方式这种方式最大的优势是零记忆成本对刚接触 Maven 的人非常友好点一下就能看到结果出错信息也直接打在 Run 面板里红色的BUILD FAILURE醒目得很。做本地调试、临时验证、写个小工具类打个小包用它完全够用。但它的短板也很明显。第一参数传递不方便想加-DskipTests得去 Run Configuration 里手动改 VM Options 或者命令行参数配一次麻烦一次。第二不容易复现你没法把点了哪几个按钮写进部署文档交给同事。第三多环境切换要靠切换 profile图形化面板里虽然能勾选 profile但稍不留神就忘了当前激活的是哪个打出来的包连的是测试库还是生产库自己都说不清。所以一旦进入 CI 或者需要交付可复现的构建流程就该转到命令行方式了。2.4 图形化方式实操中的几个细节提醒注意双击package之前建议先确认 IDEA 右下角当前激活的 Maven profile 和 JDK 版本这两个地方选错包能打出来但行为完全不对。另外IDEA 的 Maven 面板有重新加载的图标改了 pom 之后如果不点它面板里可能还显示旧结构新建的模块、新加的插件都不会出现。这个动作做一次几秒钟但我见过太多人对着没更新的面板反复排查插件为什么没生效。还有一个小习惯值得养成打包成功后顺手看一下控制台最后几行的BUILD SUCCESS以及耗时信息如果比平时长很多往往是在下载新依赖这时候要留意网络状况必要时切到国内镜像仓库。3. 方式二命令行 mvn 打包可控性拉满3.1 从零到包一条命令的完整拆解命令行打包最基础的形态就是mvn clean package。别小看这条命令它每一段都有含义。clean属于clean生命周期作用是删除target目录保证这次构建是干净的。package是default生命周期里的阶段会触发从validate到package的全部前置动作。两个生命周期写在一起Maven 会按顺序执行先清理再构建。如果只想打某一个模块可以在父目录用mvn clean package -pl 模块名 -am。-pl指定要构建的模块-am表示同时构建它依赖的模块。多模块项目里这组参数非常实用改了一个底层模块只想验证它和它的下游不需要把整个工程全部构建一遍。反过来-amd是构建被它依赖的模块方向正好相反用的时候别搞混。构建成功之后控制台会打印BUILD SUCCESS和总耗时失败则是BUILD FAILURE并且会把失败的模块和原因列出来。养成从最后一行往上读错误的习惯Maven 的报错堆栈通常很长但真正的原因往往藏在最后几行中间大段是依赖树或者插件执行的日志。3.2 跳过测试正确姿势和它带来的风险本地快速打包最常用的参数是-DskipTests它会跳过测试用例的执行但仍然会编译测试代码。如果测试代码本身都编译不过那就得用-Dmaven.test.skiptrue这个参数连测试代码的编译都跳过。两者的差别在实际项目里很关键前者能保证你的测试代码至少是能编译的后者则是彻底不管。注意-DskipTests跳过的是执行不是编译。如果测试类里引用了某个只在 test scope 存在的依赖而这个依赖没配好-DskipTests一样会失败在编译阶段这时候才需要-Dmaven.test.skiptrue。我在生产环境的构建流程里从来不敢默认跳过测试CI 上一定是完整跑一遍的。跳过测试只在本地快速验证代码能不能编过、包能不能打出来的时候用。很多人图快一直带着这个参数结果某次把带 bug 的代码提交上去CI 一跑测试全红回滚都来不及。所以这个参数的定位是临时提速工具不是日常默认配置更不要在团队共享的构建脚本里写死。3.3 多环境、多 profile 的打包参数怎么给真实项目通常有 dev、test、prod 几套配置Maven 的 profile 就是干这个的。在pom.xml里定义好 profile每个 profile 里通过properties指定不同的配置目录配合maven-resources-plugin在资源过滤阶段替换。命令行激活用-P比如mvn clean package -P prod多个 profile 可以用逗号分隔。这里最容易踩的坑是resources 的过滤没配好application.yml里的${}占位符被 Maven 当成属性去解析结果打出来的配置文件里出现一堆空值或者直接报错。解决办法是把不需要替换的文件排除掉或者在不需要过滤的目录上用filteringfalse/filtering。另一个坑是 profile 默认激活。activationactiveByDefaulttrue/activeByDefault/activation这类配置会让本地不明显地激活某个环境命令行里再-P指定另一个两者会不会叠加取决于配置写法很容易打出一个半测试半生产的畸形包。我的做法是干脆不用默认激活永远在命令行显式指定谁构建谁负责出了问题一眼就能从构建记录里看出用的是哪个 profile。3.4 命令行方式在自动化和排错上的优势命令行的最大价值就是可复现和可脚本化。一条mvn clean package -P prod -DskipTests写进构建脚本或者流水线配置任何人执行的结果都一样不存在你那边能打出来我这边不行的玄学。在排查问题时你还能往上加各种调试参数比如-X打开 debug 日志能看到 Maven 每一步的详细决策过程-e打印完整错误堆栈dependency:tree直接输出依赖树找冲突特别快。举个实际场景某次打包报NoClassDefFoundError我第一反应就是用mvn dependency:tree -Dverbose把依赖树打出来verbose会把被忽略的冲突依赖也标出来一眼就看到同一个库被两个版本引进来最终生效的是低版本缺了高版本才有的类。这种排查在图形化界面里做起来要绕好几个弯命令行一条命令就够了。4. 普通 jar 和可执行 jar这两件事必须分清4.1 为什么你的 java -jar 会报 no main manifest attribute这个报错几乎是所有 Java 新手都会撞上的第一面墙。原因很简单MANIFEST.MF里没有Main-Class这一行JVM 不知道该从哪个类的main方法开始执行。默认情况下maven-jar-plugin打出来的包就是个普通 jar它只负责编译产物归档不会自动帮你写入口类。解决办法有两个层面。如果只是写个小工具可以在maven-jar-plugin里配置archive的manifest手动指定mainClass。但更常见的做法是引入spring-boot-maven-plugin它提供的repackage目标会自动生成可执行结构写入Main-Class: org.springframework.boot.loader.JarLauncher同时用Start-Class记录你真正的启动类。JVM 先加载JarLauncher由它去构建一个能识别BOOT-INF/lib的类加载器再反射调用你的启动类。理解这个两段式启动对排查启动失败特别有用。看到JarLauncher相关的异常说明包结构层面出了问题比如签名验证失败、jar 被二次压缩破坏看到Start-Class相关的异常说明是业务启动阶段出错比如配置文件缺失、数据库连不上。把这两类错误分开排查范围一下就缩小了。4.2 Spring Boot 打包插件的配置细节与 classifierspring-boot-maven-plugin的默认行为是绑定到package阶段执行repackage把原本maven-jar-plugin打出的包替换成可执行包原始包重命名为.original。如果你想把两个包都保留并且名字更清晰可以配置classifierexec/classifier这样可执行包会变成xxx-exec.jar普通包保留原名。这个配置在把模块作为依赖给别的项目用的场景下很有必要因为别的项目引用的是普通 jar不能被可执行结构污染。还有一个executions配置容易被忽略。有些项目在父 pom 里统一管理插件版本子模块继承后如果没有正确配置 executions插件可能根本不会执行包打出来就是普通 jar。判断方法很简单看 target 目录里有没有.original文件有就说明 repackage 执行过。如果没有去父 pom 里找pluginManagement是不是只做了版本管理子模块里还需要自己声明一次plugin。对于非 Spring Boot 的传统项目常用的替代方案是maven-assembly-plugin配jar-with-dependencies描述符或者maven-shade-plugin。assembly 的优点是配置直观缺点是多个 jar 里的META-INF/services文件会互相覆盖SPI 机制会失效。shade 插件提供了ServicesResourceTransformer来解决这个问题还会做类重定位relocation避免版本冲突代价是构建慢一些。4.3 引入外部 jar 的正确做法别再被 system scope 坑有些第三方 SDK 没有发布到中央仓库只能手动下载 jar 引入。很多教程会说用scopesystem/scope加systemPath这在 IDE 里确实能编译过但打包的时候这个 jar 不会被打进去运行时报ClassNotFoundException。原因在于 system scope 的依赖 Maven 认为是由运行环境提供的不参与安装和部署。正确的做法是把 jar 先安装到本地仓库用这条命令mvn install:install-file -Dfilexxx.jar -DgroupIdcom.example -DartifactIdxxx -Dversion1.0.0 -Dpackagingjar。装完之后就像普通依赖一样在 pom 里引用即可打包也会正常带上。如果团队多人协作本地仓库各自装一遍太麻烦那就搭一个私有仓库服务器统一管理把这类 jar 传上去大家在 pom 里直接引用坐标就行。注意install:install-file安装成功后本地仓库里会按你给的坐标生成目录结构。如果 later 想换版本必须用新版本号重新安装直接覆盖旧版本号会导致 Maven 缓存和实际文件不一致。4.4 打包时配置文件、静态资源丢了怎么办这个问题在前后端混合部署的项目里特别常见。打包后进target/classes一看application-prod.yml不见了或者前端打包出来的静态文件没进去。大多数情况是resources配置写错了。默认src/main/resources会被自动收集但如果你把配置文件放在了别的目录比如src/main/resources/config又自定义了resources很容易漏掉。我的习惯是在 pom 里显式声明资源目录把application*.yml、*.properties、mapper/*.xml这几个模式列全避免依赖默认行为。MyBatis 的 XML 映射文件是个典型坑点它的默认位置在src/main/java下的包路径里而 Maven 默认只把src/main/java下的.java文件编译进去XML 会被忽略。解决办法是在buildresources里把src/main/java也加进去并且配置包含**/*.xml。5. 打包报错怎么查一份可对照的排查手册5.1 常见报错与对应病因速查表报错信息大概率病因处理方向no main manifest attributeMANIFEST 缺 Main-Class加 spring-boot 插件或配置 jar 插件 manifestUnable to find main class没有找到带 main 的类或启动类包路径异常检查启动类位置确认 repackage 的 mainClassCould not resolve dependencies仓库不通或坐标写错检查网络、镜像配置、坐标拼写ClassNotFoundException运行期依赖未打进包检查 scope排除 system scopeInvalid byte tag in constant pool编译 JDK 版本与运行 JDK 不匹配统一 source/target 与运行环境程序包 xxx 不存在依赖没下载完整清理.lastUpdated后重新拉取BUILD FAILURE且无更多信息缺-e/-X加参数打印完整堆栈中文乱码编码未统一设project.build.sourceEncodingUTF-8这张表是我自己攒下来的遇到问题先扫一眼能覆盖八成场景。剩下的两成往往跟环境、网络、公司内网策略有关那就得靠日志一步步缩小范围。5.2 依赖拉不下来先查仓库配置再看网络依赖解析失败是最常见也最耗时的类别。排查顺序我的建议是这样先看settings.xml里mirrors配的镜像地址是否正确如果是内网环境检查镜像地址能不能通再看profiles里配的仓库地址有些公司要求走内部 Nexus地址写错了会一直超时重试。第三步看本地仓库.m2/repository下对应目录里如果只有xxx.jar.lastUpdated而没有真正的 jar说明之前下载失败过Maven 会在一定时间内不再重试需要手动把这些lastUpdated文件删掉再重新构建。还有一种情况是 SNAPSHOT 依赖。快照版本 Maven 会按更新策略定期检查远程是否更新本地有缓存但远程已变更时会出现明明更新了代码却没生效。可以加-U强制检查更新或者干脆在本地开发阶段禁用快照更新。生产构建里尽量不用 SNAPSHOT用固定版本号构建才可复现。5.3 包打出来跑不起来的排查顺序包能打出来但跑不起来我一般按这个顺序走。第一步java -jar xxx.jar看第一段异常是什么。如果报no main manifest attribute是包结构问题回到上一章。第二步如果能启动但立刻退出看日志里有没有Application run failed往下找Caused by。第三步如果报数据库或 Redis 连接失败说明包是好的只是配置文件里的环境地址不对检查命令行有没有传--spring.profiles.activeprod或者环境变量有没有生效。第四步如果报ClassNotFoundException或NoSuchMethodError多半是依赖冲突。NoSuchMethodError尤其典型编译时用的是 A 版本运行时加载的是 B 版本方法签名对不上。用mvn dependency:tree -Dverbose定位冲突然后用exclusions排掉不需要的那个版本或者用dependencyManagement统一版本。这类问题在引入多个中间件 SDK 的项目里出现频率极高值得单独留半天时间梳理一遍依赖树。5.4 想确认包里到底装了什么反编译和结构检查都能帮上忙有时候线上跑到某个类报错你想确认包里那个 class 到底是什么版本。最简单的办法是把 jar 拖进 IDEAIDEA 内置了反编译能力双击 class 文件就能看到类似源码的内容。注意这是反编译结果变量名可能被混淆逻辑也可能和原始代码有细微差别但用来确认某个方法存不存在、某个常量值是多少完全够用。如果只是想看结构用jar tf xxx.jar列出所有条目配合grep找特定的类特别快。想看 MANIFEST 内容用unzip -p xxx.jar META-INF/MANIFEST.MF直接输出。这几个命令组合起来排查包里到底有没有这个文件基本就是几秒钟的事比反复重新打包猜测要高效太多。5.5 打包速度慢和构建不稳定的优化方向打包慢通常出在两个地方依赖下载和测试执行。依赖下载可以通过配置国内镜像仓库缓解镜像地址在settings.xml的mirrors里配一次全局生效。测试执行慢的话可以考虑把耗时的集成测试拆到单独的 profile 里日常构建只跑单元测试。maven-surefire-plugin支持按标签或者类名排除配置好了本地构建能从几分钟压到几十秒。构建不稳定大多跟时间、随机端口、临时文件有关。测试用例里写了固定端口本地和 CI 并发跑就冲突测试依赖当前时间跨零点就可能挂。这类问题的解法是把不确定性从测试里赶出去用随机端口、固定时钟、独立的临时目录。构建稳定性这件事前期投入一点后面省下的是持续的排查成本。6. 两种方式怎么选以及我踩过的几个真实坑图形化方式和命令行方式不是对立的实际工作中我基本是混着用。写业务代码的过程中临时验证顺手点一下 Maven 面板的package看包能不能出来准备提测或者上线一律走命令行把完整命令记在项目文档里谁执行都一样。多模块项目里被依赖的公共模块本地改完必须先install到本地仓库否则下游模块引用的还是旧包这个动作我一般也直接用命令行mvn install -pl common -am -DskipTests比在面板里点两下更不容易漏。踩过的坑里印象最深的有三个。第一个是打包跳过了测试结果测试类本身编译不过自己没发现提交后 CI 直接挂从此我在本地也至少保留一次不跳过测试的构建。第二个是 pom 里配置了 resources 过滤把application.yml里的${}全都替换掉了本地没感觉到了测试环境配置全空排查了两个小时才想到是打包阶段动了手脚后来改成只对特定目录开过滤。第三个是用 system scope 引了一个外部 SDK本地 IDE 跑得好好的打成包扔到服务器就ClassNotFoundException最后老老实实走install:install-file装进仓库才解决。如果让我给刚上手的人一句建议那就是别把打包当成一个点按钮的动作把它当成一条有输入有输出的流水线。输入端是 pom、profile、命令行参数输出端是 target 里的那个 jar中间每一环都能出问题也都能被查出来。等你把这几个环节都亲手验证过一遍后面无论换什么框架、什么项目结构这套方法论都是通用的剩下的只是插件名字不同而已。
返回列表