IntelliJ IDEA中Java项目打包实战:从普通JAR到Spring Boot可执行JAR 1. 项目概述从代码到可执行JAR的旅程在Java开发的世界里无论你是在开发一个微服务、一个工具库还是一个桌面应用最终都需要将你的源代码“打包”成一个可交付的单元。这个过程就像是把散落的零件组装成一台可以独立运行的机器。而JARJava ARchive包就是Java世界里最标准、最通用的“成品”格式。它不仅仅是一个压缩文件更是一个包含了编译后的类文件、资源文件以及元数据如清单文件MANIFEST.MF的完整封装。对于使用IntelliJ IDEA以下简称IDEA的开发者来说虽然IDE提供了便捷的图形化打包按钮但如果不理解其背后的机制一旦遇到“打包成功但运行报错”、“依赖找不到”或者“配置文件丢失”等问题就会陷入困境。这篇文章我将结合十多年的实战经验为你彻底拆解在IDEA中如何将项目代码打包成一个“健壮、可部署、无坑”的JAR包。无论你的项目是普通的Java SE应用、使用了Maven的Spring Boot应用还是复杂的多模块项目我们都会一一覆盖并深入那些官方文档很少提及的细节和陷阱。2. 打包前的核心认知JAR包的类型与选择在动手点击“打包”按钮之前我们必须搞清楚要打一个什么样的包。不同的打包方式决定了JAR包的运行方式、体积大小和部署复杂度。这一步选错了后面所有的操作都可能事倍功半。2.1 三种主流JAR包类型详解1. 普通JARJAR with dependencies这是我们最常说的“胖JAR”或“可执行JAR”。它的目标是将项目自身的代码和所有第三方依赖库即lib文件夹下的那些.jar文件全部打包进同一个JAR文件中。这样产生的JAR包通常体积较大但部署极其简单只需要一个JAR文件和Java运行环境JRE即可运行。优点部署简单环境依赖少真正做到“开箱即用”。缺点包体积大依赖库更新麻烦需要重新打包整个应用多个应用无法共享相同的依赖库可能造成磁盘空间浪费。适用场景微服务、独立桌面应用、需要快速分发给最终用户的小工具。2. 瘦JARJAR without dependencies这种JAR包只包含项目自身编译后的类文件和资源不包含任何第三方依赖。运行时需要依赖的JAR包必须通过-cp或-classpath参数指定或者放在特定的目录下如lib/。优点包体积小结构清晰依赖库可以独立管理和共享。缺点部署复杂必须确保运行环境中有所有正确版本的依赖否则会抛出ClassNotFoundException。适用场景作为公共库Library被其他项目引用时必须打瘦JAR。某些对启动速度和包体积有极致要求且部署环境可控的场景。3. Spring Boot可执行JAR这是Spring Boot项目特有的打包方式它本质上也是一种“胖JAR”但内部结构更为精巧。它使用一个特殊的“嵌套JAR”结构Nested JAR将应用本身的代码和所有依赖库打包在一个JAR文件内的BOOT-INF/目录下同时使用一个独立的“启动加载器”Launcher来引导应用。这种结构解决了传统胖JAR中依赖资源路径冲突的问题比如多个依赖JAR里都有META-INF/MANIFEST.MF文件。优点继承了胖JAR部署简单的优点同时解决了资源冲突问题是Spring Boot应用的官方推荐和默认打包方式。缺点结构特殊直接解压后无法像普通JAR一样通过java -cp直接运行其中的某个类需要通过启动器。适用场景所有基于Spring Boot框架的Web应用或微服务。注意选择哪种类型取决于你的项目类型和部署需求。对于现代Java后端开发尤其是微服务架构Spring Boot可执行JAR是绝对的主流。而如果你在开发一个工具库给其他团队用那么瘦JAR是唯一的选择。2.2 工具链选择Maven vs. Gradle vs. IDEA原生IDEA支持多种构建工具不同的工具决定了打包配置的方式。Maven目前Java生态的“事实标准”配置文件为pom.xml。它的插件体系如maven-jar-plugin,maven-shade-plugin,spring-boot-maven-plugin是打包能力的核心。本文将以Maven项目为重点进行讲解因为其用户基数最大遇到的问题也最具代表性。Gradle以灵活和性能著称使用Groovy或Kotlin DSL编写build.gradle脚本。其打包逻辑与Maven类似但语法不同。IDEA原生Artifacts在不使用Maven/Gradle的纯Java项目里你可以通过IDEA的“Project Structure - Artifacts”来手动配置打包。这种方式更直观但不够灵活且难以融入CI/CD流程不推荐在正式项目中使用。核心原则对于任何正经项目都应该使用Maven或Gradle进行构建和打包管理IDEA只是提供了一个操作这些构建工具的图形界面。你的打包逻辑应该固化在pom.xml或build.gradle中而不是依赖IDE的某个特定配置。3. 实战演练一打包普通Java SE项目含依赖假设我们有一个简单的Java SE项目它使用了commons-lang3和gson这两个第三方库。我们的目标是生成一个可执行的胖JAR。3.1 项目结构与Maven配置项目结构如下my-app ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── example │ │ │ └── App.java (包含main方法) │ │ └── resources │ │ └── config.properties │ └── test │ └── java └── pom.xml关键的pom.xml配置如下。这里我们使用maven-shade-plugin它是Apache提供的一个非常强大的Maven插件专门用于创建包含所有依赖的可执行JAR并能处理依赖冲突、重命名类等高级操作。project ... modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdmy-app/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.12.0/version /dependency dependency groupIdcom.google.code.gson/groupId artifactIdgson/artifactId version2.10.1/version /dependency /dependencies build plugins !-- 编译插件 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration source8/source target8/target /configuration /plugin !-- 核心打包插件 -- plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.0/version executions execution phasepackage/phase goals goalshade/goal /goals configuration !-- 创建可执行JAR -- transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer !-- 指定主类程序入口 -- mainClasscom.example.App/mainClass /transformer !-- 处理Spring的配置文件冲突如果是Spring项目需要 -- !-- transformer implementationorg.apache.maven.plugins.shade.resource.AppendingTransformer resourceMETA-INF/spring.handlers/resource /transformer -- /transformers !-- 可选过滤掉不需要的依赖如测试依赖 -- filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters /configuration /execution /executions /plugin /plugins /build /project3.2 在IDEA中执行打包操作配置好pom.xml后在IDEA中打包就变得非常简单。打开IDEA右侧的“Maven”工具窗口通常在最右边栏如果没有可以通过View - Tool Windows - Maven打开。展开你的项目找到Lifecycle生命周期。双击package命令。IDEA会开始执行Maven的打包生命周期。执行过程解读 当你执行package时Maven会按顺序执行validate、compile、test、package等阶段。maven-shade-plugin绑定在package阶段所以它会在此阶段被触发。你会在IDEA的“Run”工具窗口或“Maven”窗口的日志中看到类似下面的输出[INFO] --- maven-shade-plugin:3.5.0:shade (default) my-app --- [INFO] Including org.apache.commons:commons-lang3:jar:3.12.0 in the shaded jar. [INFO] Including com.google.code.gson:gson:jar:2.10.1 in the shaded jar. [INFO] Replacing original artifact with shaded artifact. [INFO] Replacing /path/to/my-app/target/my-app-1.0-SNAPSHOT.jar with /path/to/my-app/target/my-app-1.0-SNAPSHOT-shaded.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS打包成功后你可以在项目的target/目录下找到生成的JAR文件my-app-1.0-SNAPSHOT.jar注意shade插件默认会替换掉原始的瘦JAR。3.3 验证与运行打包完成后务必进行验证。检查JAR内容你可以使用解压软件如7-Zip直接打开生成的JAR包或者使用命令jar tf target/my-app-1.0-SNAPSHOT.jar。你应该能看到com/example/App.class你的主类org/apache/commons/lang3/...依赖库的类com/google/gson/...依赖库的类META-INF/MANIFEST.MF清单文件检查清单文件查看META-INF/MANIFEST.MF确认Main-Class属性是否正确设置为com.example.App。运行测试打开终端进入项目根目录执行java -jar target/my-app-1.0-SNAPSHOT.jar如果程序正常启动并输出预期结果恭喜你打包成功实操心得插件版本尽量使用较新稳定版本的插件旧版本可能有已知Bug。例如老版本的maven-shade-plugin在处理某些特定资源文件时可能会出错。主类确认mainClass一定要填写完整类名包含包路径这是最常见的打包成功但无法运行的错误原因之一。资源文件处理如果你的resources目录下有配置文件如.properties,.xml,.ymlmaven-shade-plugin默认会将它们打包进去。但如果多个依赖JAR中存在同名资源文件例如META-INF/services/javax.xml.parsers.SAXParserFactory可能会被覆盖。这时就需要使用AppendingTransformer如上面配置中注释掉的部分或ServicesResourceTransformer来合并这些服务文件而不是覆盖。4. 实战演练二打包Spring Boot项目Spring Boot项目的打包是当今Java后端开发中最常见的场景。Spring Boot通过spring-boot-maven-plugin插件让打包变得异常简单几乎无需额外配置。4.1 标准Spring Boot项目打包配置一个标准的Spring Boot项目的pom.xml继承自spring-boot-starter-parent并引入了Web等起步依赖。project ... modelVersion4.0.0/modelVersion !-- 继承Spring Boot父POM统一管理依赖版本 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.14/version !-- 注意版本号需与依赖匹配 -- relativePath/ /parent groupIdcom.example/groupId artifactIdmy-spring-boot-app/artifactId version0.0.1-SNAPSHOT/version namemy-spring-boot-app/name descriptionDemo project for Spring Boot/description properties java.version1.8/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 其他依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins !-- 核心Spring Boot Maven 插件 -- plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId !-- 通常无需指定版本由parent管理 -- /plugin /plugins /build /project就是这么简单spring-boot-maven-plugin插件默认绑定了repackage目标它会在Maven的package阶段之后执行将Maven生成的普通JAR重新打包成Spring Boot可执行JAR。4.2 执行打包与结果分析在IDEA的Maven工具窗口中同样双击Lifecycle下的package。你会看到日志中出现了spring-boot-maven-plugin的执行信息[INFO] --- spring-boot-maven-plugin:2.7.14:repackage (repackage) my-spring-boot-app --- [INFO] Replacing main artifact with repackaged archive打包完成后target/目录下会生成两个JAR文件my-spring-boot-app-0.0.1-SNAPSHOT.jar.original这是Maven标准插件maven-jar-plugin生成的原始“瘦JAR”。my-spring-boot-app-0.0.1-SNAPSHOT.jar这是spring-boot-maven-plugin重新打包后的“胖JAR”也就是我们最终需要的可执行JAR。解构Spring Boot JAR 用解压软件打开最终的可执行JAR你会看到其独特的内部结构my-spring-boot-app-0.0.1-SNAPSHOT.jar ├── META-INF/ │ └── MANIFEST.MF (包含Main-Class: org.springframework.boot.loader.JarLauncher) ├── BOOT-INF/ │ ├── classes/ (你的应用类文件和资源如com/example/Application.class, application.yml) │ └── lib/ (所有的第三方依赖JAR包) └── org/ └── springframework/ └── boot/ └── loader/ (Spring Boot的JarLauncher类)这个结构完美解决了传统胖JAR的资源冲突问题因为所有依赖库都被隔离在了BOOT-INF/lib/下。4.3 运行与部署运行Spring Boot JAR和运行普通胖JAR一样java -jar target/my-spring-boot-app-0.0.1-SNAPSHOT.jar应用会启动内嵌的Tomcat服务器并开始监听端口默认8080。关于“未解析的依赖项”错误 在配置过程中你可能会遇到类似未解析的依赖项: org.springframework.boot:spring-boot-starter-web:jar:2.7.14的错误。这几乎总是由以下原因之一造成的网络问题Maven无法从中央仓库或你配置的镜像仓库下载依赖。检查网络或尝试使用阿里云等国内镜像。版本不匹配parent中定义的Spring Boot版本与依赖中显式指定的版本冲突。最佳实践是依赖项不要写版本号由父POM统一管理。如果非要写必须确保一致。本地仓库损坏删除本地Maven仓库默认在~/.m2/repository中对应的依赖目录如org/springframework/boot/spring-boot-starter-web/2.7.14/然后让Maven重新下载。IDE缓存在IDEA中尝试File - Invalidate Caches and Restart...。5. 高级配置与深度优化掌握了基础打包后我们来看看如何应对更复杂的需求和进行优化。5.1 定制化MANIFEST.MF文件清单文件包含了JAR包的元数据。除了主类我们经常需要添加一些自定义属性。在普通Maven项目中使用maven-jar-pluginplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest mainClasscom.example.App/mainClass addClasspathtrue/addClasspath !-- 将依赖添加到Class-Path属性 -- classpathPrefixlib//classpathPrefix !-- 指定依赖目录前缀 -- /manifest manifestEntries Built-By${user.name}/Built-By Implementation-Version${project.version}/Implementation-Version Build-Time${maven.build.timestamp}/Build-Time /manifestEntries /archive /configuration /plugin这样打出的瘦JAR其MANIFEST.MF会包含Class-Path指导JVM从lib/目录下寻找依赖JAR。在Spring Boot项目中spring-boot-maven-plugin会自动生成必要的清单信息。如果你想添加自定义属性可以通过配置实现plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration mainClasscom.example.MyApplication/mainClass layoutJAR/layout !-- 添加自定义清单条目 -- manifest addDefaultEntriesfalse/addDefaultEntries addClasspathtrue/addClasspath /manifest manifestEntries Built-ByMyTeam/Built-By /manifestEntries /configuration /plugin5.2 排除不必要的依赖与资源为了减小JAR包体积我们需要排除一些仅在开发或测试阶段需要的依赖和文件。排除依赖在pom.xml中通过scope标签管理依赖作用域。test仅用于测试不会被打包。provided由运行环境如Tomcat服务器、JDK提供打包时排除。dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId scopeprovided/scope !-- 编译时需要运行时不需要 -- /dependency dependency groupIdjunit/groupId artifactIdjunit/artifactId scopetest/scope !-- 仅测试 -- /dependency排除资源文件在pom.xml的build部分配置资源过滤。build resources resource directorysrc/main/resources/directory excludes exclude**/*.keystore/exclude !-- 排除密钥文件 -- exclude**/test-*.yml/exclude !-- 排除测试配置文件 -- /excludes filteringtrue/filtering !-- 是否启用属性过滤如${}替换 -- /resource /resources /build使用Maven Profile进行环境隔离这是一个非常实用的技巧。你可以为开发、测试、生产环境定义不同的Profile打包时激活对应的Profile从而引入不同的配置或依赖。profiles profile iddev/id activation activeByDefaulttrue/activeByDefault !-- 默认激活 -- /activation properties envdev/env /properties build resources resource directorysrc/main/resources/directory includes includeapplication.yml/include includeapplication-${env}.yml/include !-- 包含application-dev.yml -- /includes /resource /resources /build /profile profile idprod/id properties envprod/env /properties !-- 生产环境可能排除开发工具依赖 -- dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scopeprovided/scope !-- 或者直接不引入 -- /dependency /dependencies /profile /profiles打包时在IDEA的Maven工具窗口的Profiles里勾选prod或者使用命令mvn clean package -P prod。5.3 多模块项目的打包策略对于大型项目我们通常采用多模块Multi-Module结构。父pom.xml中packaging为pom子模块可以是jar或war。聚合模块父POM只负责管理公共依赖和插件版本不包含源代码也不打包。子模块每个子模块是一个独立的可编译、可打包的单元。例如一个common工具模块打jar包供其他模块依赖一个web应用模块使用spring-boot-maven-plugin打可执行jar包。打包操作在父项目根目录执行mvn clean packageMaven会依据模块依赖关系按顺序编译和打包所有子模块。最终每个子模块的target/目录下会生成自己的JAR包。对于需要部署的应用模块如web直接取用其生成的JAR即可。关键点确保子模块间的依赖在pom.xml中正确定义。在打包应用模块时Maven会将其依赖的其他模块JAR如果它们也被打包为JAR以及第三方依赖一并处理对于Spring Boot项目会打包进BOOT-INF/lib。6. 常见问题排查与实战技巧即使按照步骤操作打包路上依然坑洼不断。这里记录了我踩过的一些典型问题和解决思路。6.1 打包后运行报“找不到主清单属性”或“主类”这是最常见的问题没有之一。症状java -jar your-app.jar提示no main manifest attribute, in your-app.jar或Error: Could not find or load main class com.example.App。排查步骤检查MANIFEST.MF用解压工具或jar tf your-app.jar | grep META-INF/MANIFEST.MF找到并查看该文件。确认Main-Class属性是否存在且值正确。确认插件配置检查pom.xml中是否正确配置了maven-shade-plugin普通项目或spring-boot-maven-pluginSpring Boot项目的mainClass。检查打包结果对于Spring Boot项目确认你运行的是your-app.jar而不是your-app.jar.original。后者是瘦JAR没有启动器。类名拼写确保mainClass的值与项目中实际包含public static void main(String[] args)方法的类完全一致包括大小写。6.2 依赖冲突与类找不到NoClassDefFoundError/ClassNotFoundException症状程序在IDEA里运行正常打包后运行抛出NoClassDefFoundError或ClassNotFoundException。原因分析瘦JAR问题打了瘦JAR但运行时没有将依赖JAR放入classpath。依赖作用域错误某个必需的依赖被错误地声明为test或provided作用域导致打包时被排除。依赖传递冲突两个依赖引入了不同版本的同名JARMaven根据“最近原则”选择了其中一个导致另一个版本中的类缺失。解决方案检查依赖树在项目根目录执行mvn dependency:tree查看最终的依赖关系确认所需的JAR是否在列表中版本是否正确。检查作用域在dependency:tree的输出中注意每个依赖后面的compile,runtime,test,provided标识。排除冲突依赖如果发现冲突可以在引入依赖时排除掉不需要的传递依赖。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-tomcat/artifactId !-- 例如排除Tomcat改用Jetty -- /exclusion /exclusions /dependency使用maven-shade-plugin重命名对于无法排除的底层库冲突如不同组件依赖了不同版本的asm可以使用shade插件重命名其中一个包。configuration relocations relocation patternorg.objectweb.asm/pattern !-- 原包名 -- shadedPatterncom.example.shaded.asm/pattern !-- 重命名后的包名 -- /relocation /relocations /configuration6.3 资源文件配置文件丢失或读取错误症状代码中通过ClassLoader.getResource()或getResourceAsStream()读取src/main/resources下的文件在IDEA中运行正常打包后却返回null。原因与解决路径问题打包后资源文件位于JAR的根目录或BOOT-INF/classes/下。使用ClassLoader.getResource(filename)时路径前不要加/。正确的做法是假设资源文件与你的类文件在同一“目录”下。资源未被包含检查pom.xml的resources配置看是否通过excludes无意中排除了该文件。Spring Boot特定资源Spring Boot应用通常使用Value或ConfigurationProperties来注入配置或者通过ResourceLoader加载。确保你的配置文件如application.yml位于src/main/resources下并且没有被过滤掉。Spring Boot在打包后能正确识别BOOT-INF/classes/下的资源。6.4 打包速度优化与JAR瘦身项目大了之后打包会变得很慢JAR包也动辄上百MB。跳过测试打包时使用mvn clean package -DskipTests可以跳过单元测试大幅提升速度。在IDEA的Maven执行配置中也可以永久设置。使用并行构建在~/.m2/settings.xml中配置paralleltrue/parallel可以启用并行构建对多模块项目效果明显。JAR瘦身使用spring-boot-thin-launcher对于Spring Boot可以考虑使用Thin Launcher它只打包应用代码依赖在运行时从Maven仓库下载或缓存。这能极大减小初次部署的包体积。排除开发依赖严格检查所有依赖的scope确保test和provided的依赖不会被打包。清理无用资源定期清理src/main/resources中不再使用的图片、文档等。使用maven-dependency-plugin分析执行mvn dependency:analyze可以分析出项目中声明了但未使用的依赖Unused declared dependencies以及使用了但未声明的依赖Used undeclared dependencies帮助优化pom.xml。6.5 IDEA特定问题排查“Maven项目打包报错”首先检查IDEA右侧Maven工具窗口顶部是否选择了正确的Maven版本推荐使用自带的Bundled (Maven 3)或你本地安装的版本和settings.xml。然后尝试点击Maven工具窗口的刷新按钮Reimport All Maven Projects。很多时候问题在于IDEA的索引和Maven的实际状态不同步。“程序包不存在”但Maven命令可以编译这是IDEA的模块编译问题。尝试File - Invalidate Caches and Restart...。如果不行检查File - Project Structure - Modules看是否所有源目录Sources和依赖Dependencies都正确配置了。打包时编码警告在pom.xml中全局设置编码避免中文乱码。properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding /properties打包看似是开发的最后一步但一个稳定、高效的打包配置却是项目可维护、可交付的基石。理解背后的原理善用Maven/Gradle插件掌握排查问题的基本方法就能让你在交付环节从容不迫。记住最好的配置是那些写在pom.xml或build.gradle里与代码一起被版本管理的配置而不是依赖某个开发人员IDE里的一次性操作。