1. 项目概述为什么一个“打包”能聊出这么多花样干了这么多年Java开发每次看到团队里新人对着Maven打包一脸懵或者项目上线时因为打包姿势不对导致各种灵异事件我就觉得这事儿值得好好掰扯掰扯。你以为mvn clean package就是打包的全部那可能只是踩坑的开始。从最基础的普通Jar到能直接java -jar运行的“胖Jar”可执行Jar再到分门别类、依赖分离的“瘦Jar”每一种打包方式背后都对应着不同的应用场景、部署策略和运维考量。这不仅仅是敲个命令更是一种项目架构和交付思维的体现。最近在社区和热搜里Maven打包、idea打jar包、springboot打jar包这些词热度一直不低连带出来的问题也是五花八门依赖冲突、包太大、启动慢、配置文件外置、不同环境适配…… 这说明很多开发者尤其是刚入行的朋友对Maven打包的理解还停留在表面。这篇文章我就结合自己趟过的坑把Maven工程打Jar包的这“N种方式”给你彻底讲透从原理到实操从选型到避坑让你下次打包时心里有底手上有谱。2. 核心打包方式全景解析与选型指南在动手之前我们得先搞清楚“战场”的全貌。Maven打包Jar根据其内容和用途主要可以分为三大流派每种流派下又有不同的实现“插件”选择哪种完全取决于你的项目要干什么。2.1 流派一普通JarLibrary Jar这是最基础、最原始的Jar包。它的目标很单纯作为类库供其他项目依赖使用。当你执行mvn clean packageMaven默认调用的maven-jar-plugin生成的就是这种包。核心特征不包含依赖打包结果里只有你自己项目编译后的.class文件和你放在src/main/resources下的资源文件。不可直接运行因为没有Main-Class清单信息你无法用java -jar your.jar来运行它。用途发布到Maven仓库本地或远程供其他项目通过dependency引用。比如你写了一个通用的工具包common-utils就需要打成这种Jar。关键配置pom.xml片段build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration !-- 可以指定生成的Jar包名称避免默认的 artifactId-version.jar -- finalNamemy-library/finalName !-- 也可以配置一些清单文件MANIFEST.MF信息但对于纯库Jar通常不需要Main-Class -- /configuration /plugin /plugins /build注意对于纯库Jar通常保持默认配置即可。除非你有特殊需求比如需要将一些额外的属性如Git Commit ID写入清单文件。2.2 流派二可执行JarExecutable Jar / Fat Jar这是目前Web应用或独立应用最常见的打包形式尤其是Spring Boot项目默认的打包方式。它的目标是把项目本身和所有依赖的第三方库全部打包进一个Jar文件形成一个自包含、可独立运行的单元。核心特征包含所有依赖使用“嵌套Jar”或“类路径展开”技术将依赖的Jar包全部打包进去。可直接运行清单文件MANIFEST.MF中指定了Main-Class通过java -jar app.jar一键启动。优缺点明显优点部署极其简单服务迁移、扩缩容时只需要传递一个文件。环境一致性高“在我这儿能跑在你那儿也能跑”。缺点包体积巨大动辄几十MB甚至上百MB。每次更新即使只改了一行代码也需要传输整个大包浪费带宽和时间。依赖重复如果多个服务使用相同的基础依赖如Spring Core每个Fat Jar都会包含一份副本。主流实现插件spring-boot-maven-plugin(王者首选) Spring Boot项目的标配。它打包出来的Jar结构独特是一个“Jar套Jar”的结构。外层Jar是启动引导器内层的BOOT-INF/lib/目录下存放所有依赖JarBOOT-INF/classes/存放项目类文件。这种结构使得它支持外部化配置--loader.path和优雅的依赖管理。plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin打包命令依然是mvn clean package打包后会在target目录下生成*.jar和*.jar.original两个文件前者是可执行的Fat Jar后者是原始的普通Jar。maven-shade-plugin(老牌劲旅) 在Spring Boot出现之前这是制作Fat Jar的主流选择。它的策略是“融合”shade将所有依赖的.class文件解压后和你项目的类文件重新打包到一个Jar里。它功能强大支持重命名依赖包路径解决依赖冲突但配置相对复杂。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.0/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.yourcompany.MainApp/mainClass /transformer /transformers /configuration /execution /executions /pluginmaven-assembly-plugin(灵活组装) 这个插件更通用它不仅可以打包Jar还能打包Zip、Tar等。通过自定义描述符descriptor你可以精确控制打包内容。用它来做Fat Jar通常是将依赖Jar包不解压直接复制到目标Jar内的某个目录如lib/并在清单文件中配置Class-Path。这种方式打包出来的Jar严格来说不是标准的“可执行Jar”因为依赖Jar是独立的文件但通过自定义的启动脚本如.bat或.sh来设置类路径也能达到类似效果。配置较为繁琐。2.3 流派三依赖分离式JarSkinny Jar Lib Directory这是一种兼顾部署效率和包体积的折中方案在微服务架构和容器化部署中越来越受青睐。它的核心思想是将项目代码打包成一个很小的“瘦Jar”而将所有第三方依赖统一放到Jar包外部的目录如lib/中。核心特征包体积小瘦Jar只包含业务代码通常只有几十KB到几MB。依赖共享与分层依赖被分离出来在Docker镜像构建时可以利用Docker的分层缓存机制。如果依赖没有变化这一层缓存可以被所有服务复用极大加速镜像构建和推送。启动命令稍复杂需要指定类路径-cp或-classpath来指向外部的依赖库目录。用途非常适合CI/CD流水线、Docker镜像构建追求构建速度和部署效率的场景。主流实现插件maven-dependency-pluginmaven-jar-plugin组合拳 这是最经典的实现方式。先用dependency:copy-dependencies将依赖复制到指定目录如target/lib然后用maven-jar-plugin打包业务代码并在清单文件中配置Class-Path指向lib/目录下的各个Jar文件。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-dependency-plugin/artifactId executions execution idcopy-dependencies/id phasepackage/phase goals goalcopy-dependencies/goal /goals configuration outputDirectory${project.build.directory}/lib/outputDirectory overWriteReleasesfalse/overWriteReleases overWriteSnapshotsfalse/overWriteSnapshots overWriteIfNewertrue/overWriteIfNewer /configuration /execution /executions /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration archive manifest addClasspathtrue/addClasspath classpathPrefixlib//classpathPrefix mainClasscom.yourcompany.MainApp/mainClass /manifest /archive /configuration /plugin打包后target目录下会有一个瘦Jar和一个lib文件夹。运行命令为java -cp ‘your-app.jar:lib/*’ com.yourcompany.MainApp(Linux/Mac) 或java -cp “your-app.jar;lib\*” com.yourcompany.MainApp(Windows)。spring-boot-maven-plugin的layered模式 Spring Boot 2.3.0 之后引入了分层架构支持。它打包的依然是一个Fat Jar但这个Jar内部被分成了不同的层Layer例如dependencies快照依赖、spring-boot-loader、snapshot-dependencies快照依赖、application应用代码。在构建Docker镜像时可以利用这些分层将变化频率低的层如依赖层放在镜像底层充分利用缓存。plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration layers enabledtrue/enabled /layers /configuration /plugin这并非传统意义上的依赖分离而是一种更智能的“Fat Jar内部分离”是面向容器化部署的优化。3. 五大核心场景下的打包策略实战了解了流派我们还得看菜下饭。不同的项目类型和部署环境决定了哪种打包方式是最优解。3.1 场景一传统Spring Boot单体应用首选方案spring-boot-maven-plugin(默认Fat Jar)对于绝大多数Spring Boot Web应用这是最简单、最省心的选择。你几乎不需要任何额外配置。部署直接scp上传到服务器用java -jar启动配合nohup或 systemd 做后台服务。配置外置这是关键技巧。不要把application.properties打进Jar包。使用java -jar app.jar --spring.config.location/path/to/your/config/来指定外部配置文件。这样修改配置无需重新打包和部署。启动脚本建议写一个简单的Shell脚本start.sh来封装启动命令包括JVM参数堆内存、GC策略等、日志路径配置等。#!/bin/bash JAVA_OPTS“-Xms512m -Xmx1024m -XX:UseG1GC” nohup java $JAVA_OPTS -jar /path/to/your-app.jar --spring.config.location/config/application-prod.properties /logs/app.log 21 echo $! pid.file3.2 场景二微服务与容器化Docker部署推荐方案依赖分离式 或 Spring Boot分层Jar在Kubernetes和Docker时代镜像构建速度直接影响CI/CD效率。方案A依赖分离使用maven-dependency-plugin组合。你的Dockerfile会是这样FROM openjdk:11-jre-slim WORKDIR /app COPY target/lib ./lib COPY target/your-app.jar ./app.jar ENTRYPOINT [“java”, “-cp”, “app.jar:lib/*”, “com.yourcompany.MainApp”]优势当pom.xml依赖不变时COPY target/lib ./lib这一层会被缓存后续构建只需复制很小的业务Jar速度极快。方案BSpring Boot分层使用spring-boot-maven-plugin并开启分层。你需要一个更复杂的Dockerfile来利用分层FROM openjdk:11-jre-slim as builder WORKDIR /app COPY target/your-app.jar ./app.jar RUN java -Djarmodelayertools -jar app.jar extract FROM openjdk:11-jre-slim WORKDIR /app COPY --frombuilder /app/dependencies/ ./ COPY --frombuilder /app/spring-boot-loader/ ./ COPY --frombuilder /app/snapshot-dependencies/ ./ COPY --frombuilder /app/application/ ./ ENTRYPOINT [“java”, “org.springframework.boot.loader.JarLauncher”]优势同样是利用Docker缓存但由Spring Boot插件智能管理分层更为规范。3.3 场景三公共工具库或SDK开发必须方案普通Jar (maven-jar-plugin)如果你在开发一个供其他团队或项目使用的工具包、客户端SDK或通用组件必须打成不包含依赖的普通Jar。关键点在pom.xml中要仔细管理依赖的scope。对于“仅编译期需要”的依赖如Lombok、APT注解处理器使用scopeprovided/scope。对于“测试期需要”的依赖使用scopetest/scope。确保最终打包进去的只有运行时真正需要的依赖默认compilescope或者干脆都不打进去由使用者项目自行声明依赖。发布使用mvn clean deploy将Jar包发布到公司的私有Nexus或Sonatype仓库其他项目通过dependency引用。3.4 场景四客户端桌面应用JavaFX/Swing经典方案maven-shade-plugin或 自定义Assembly桌面应用需要分发一个完整的包给最终用户。maven-shade-plugin的“融合”特性在这里很有用可以避免用户环境缺少某个依赖的问题。你还可以用它来重命名一些有冲突的依赖比如不同库都引入了不同版本的ASM。 更进一步你可以使用maven-assembly-plugin或javapackagerJDK自带来生成包含JRE的安装包exe, dmg, deb/rpm实现真正的“一键安装”。3.5 场景五多模块项目Maven Multi-Module多模块项目的打包需要分层次考虑。父POM通常打包类型为pom只做依赖和管理声明不产生实际构件。子模块公共库如common-core打成普通Jar (jar)。子模块服务提供者如user-service,order-service根据部署方式打成Fat Jar或分离式Jar。聚合打包可以在最顶层的聚合模块中使用maven-assembly-plugin将所有子模块的发布包和依赖、脚本等收集起来打包成一个完整的发布版ZIP便于运维统一部署。4. 高级配置与深度优化技巧选好了插件配置才是决定打包结果是否好用的关键。这里分享几个实战中总结的高级技巧。4.1 清单文件MANIFEST.MF的精细控制清单文件是Jar包的“身份证”Main-Class和Class-Path是核心属性。通过插件可以自定义更多信息。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration archive manifest addClasspathtrue/addClasspath classpathPrefixlib//classpathPrefix mainClasscom.yourcompany.MainApp/mainClass !-- 添加自定义属性便于排查问题 -- addDefaultImplementationEntriestrue/addDefaultImplementationEntries addDefaultSpecificationEntriestrue/addDefaultSpecificationEntries /manifest manifestEntries Built-By${user.name}/Built-By Build-Jdk${java.version}/Build-Jdk Implementation-Version${project.version}/Implementation-Version Implementation-Title${project.name}/Implementation-Title Git-Commit${git.commit.id.abbrev}/Git-Commit !-- 需要git-commit-id-plugin -- /manifestEntries /archive /configuration /plugin这样打包出来的Jar用jar tf your.jar | grep -i manifest查看清单文件或在运行时通过Package.getImplementationVersion()都能读到这些构建信息对线上问题定位非常有帮助。4.2 资源过滤与多环境适配项目中的配置文件如.properties,.yml经常需要根据环境dev, test, prod变化。Maven的Resources插件支持过滤可以在打包时动态替换占位符。在pom.xml中定义环境变量或使用Profilesprofiles profile idprod/id properties app.envproduction/app.env db.urljdbc:mysql://prod-db:3306/app/db.url /properties /profile /profiles在src/main/resources的配置文件中使用占位符# application.properties spring.datasource.urldb.url environmentapp.env启用资源过滤build resources resource directorysrc/main/resources/directory filteringtrue/filtering !-- 关键开启过滤 -- /resource /resources /build打包时使用mvn clean package -PprodMaven就会将db.url和app.env替换为prod profile中定义的值。4.3 排除不必要的文件为Jar包“瘦身”Fat Jar里经常包含一些我们根本不需要的文件比如文档、源码、测试用例等徒增体积。排除依赖中的文件spring-boot-maven-plugin和maven-shade-plugin都支持配置excludes。plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration excludes exclude groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /exclude /excludes /configuration /plugin排除项目资源在maven-jar-plugin中配置excludes可以过滤资源目录下的文件。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId configuration excludes exclude**/*.md/exclude exclude**/test-data/**/exclude /excludes /configuration /plugin一个常见的优化是排除*.map文件前端Source Map这些文件在生产环境无用且体积不小。4.4 使用Maven Profiles实现一键多环境打包通过Profiles我们可以将不同环境的打包配置如最终Jar名、激活的Spring Profile、资源过滤规则封装起来。profiles profile iddev/id activation activeByDefaulttrue/activeByDefault !-- 默认激活 -- /activation properties build.profile.iddev/build.profile.id spring.profiles.activedev/spring.profiles.active /properties build finalName${project.artifactId}-${project.version}-dev/finalName /build /profile profile idprod/id properties build.profile.idprod/build.profile.id spring.profiles.activeprod/spring.profiles.active /properties build finalName${project.artifactId}-${project.version}-prod/finalName resources resource directorysrc/main/resources/directory filteringtrue/filtering excludes excludeapplication-dev.properties/exclude /excludes /resource /resources /build /profile /profiles然后在application.properties中设置spring.profiles.activespring.profiles.active。打包时mvn clean package -Pprod即可生成生产环境专用的Jar包。5. 高频问题排查与实战避坑指南理论说再多不如踩几个坑记得牢。下面这些是我和团队在多年实践中总结的典型问题。5.1 问题一“没有主清单属性”或“找不到或无法加载主类”这是新手最常遇到的错误。执行java -jar xxx.jar时提示no main manifest attribute。原因打出来的Jar包是普通的库Jar其META-INF/MANIFEST.MF文件中没有Main-Class属性。排查检查使用的打包插件。你是否错误地只使用了默认的maven-jar-plugin如果使用了spring-boot-maven-plugin检查是否被其他插件配置覆盖或冲突。用解压工具或命令jar tf your.jar | grep -i manifest查看清单文件内容确认Main-Class是否存在且路径正确。解决确保引入了正确的可执行Jar打包插件如spring-boot-maven-plugin并正确配置了mainClass。5.2 问题二依赖冲突NoSuchMethodError, ClassNotFoundException, NoClassDefFoundError运行时抛出令人困惑的类加载错误。原因项目中引入了多个不同版本的相同依赖如Guava 20.0和Guava 30.0Maven依赖仲裁dependency mediation选择了其中一个但另一个版本中的某个类或方法被你的代码或另一个间接依赖所引用导致冲突。排查使用mvn dependency:tree tree.txt命令生成完整的依赖树搜索冲突的依赖。在IDE中查看依赖分析图寻找版本不一致的库。解决排除法在引入依赖的地方使用exclusions排除掉传递进来的冲突版本。dependency groupIdcom.xxx/groupId artifactIdsome-client/artifactId exclusions exclusion groupIdcom.google.guava/groupId artifactIdguava/artifactId /exclusion /exclusions /dependency统一版本管理在父POM或dependencyManagement中强制指定某个依赖的版本。使用Shade插件重命名对于无法排除的深度冲突可以用maven-shade-plugin将冲突的依赖包路径重命名relocate隔离起来。configuration relocations relocation patterncom.google.guava/pattern shadedPatternmyapp.shaded.guava/shadedPattern /relocation /relocations /configuration5.3 问题三资源文件配置文件找不到代码里用ClassLoader.getResource(“config.json”)或Value(“classpath:template.txt”)加载资源在IDE里运行正常但打成Jar包后报错。原因资源文件没有被正确打包进Jar包或者路径发生了变化。Jar包中的资源路径与文件系统路径不同。排查检查资源文件是否位于src/main/resources或其子目录下。用jar tf your.jar命令查看资源文件是否在预期的路径内如BOOT-INF/classes/config.json。解决确保资源文件在标准Maven资源目录下。加载资源时不要使用File而应始终使用ClassLoader.getResourceAsStream()或Spring的ResourceLoader。因为Jar包中的资源不是一个独立的文件无法用File路径访问。如果资源文件在非标准位置需要在pom.xml中配置maven-resources-plugin将其复制到构建目录。5.4 问题四Jar包体积过大导致上传/部署慢一个Spring Boot应用动辄50MB每次发布都是漫长的等待。优化手段使用依赖分离模式如前文所述将依赖外置业务Jar通常只有几百KB。排除无用依赖用mvn dependency:analyze分析哪些依赖是“未使用但已声明”的将其移除。特别注意provided和test范围的依赖是否被错误地打成compile。使用轻量级依赖比如用OkHttp代替老旧的HttpClient用HikariCP代替Druid如果不需要监控。压缩资源对Jar包内的静态资源如图片、JS/CSS进行压缩。可以使用maven-antrun-plugin在打包阶段调用压缩工具。使用JDK模块化JPMS对于Java 9项目可以创建module-info.java只声明必要的模块依赖JLink可以生成只包含所需模块的精简运行时。但这有一定复杂度。5.5 问题五不同操作系统下的路径与编码问题在Windows开发Linux部署常常遇到路径分隔符;vs:和文件编码问题。路径问题在配置Class-Path或写启动脚本时使用File.pathSeparator或File.separator的Java系统属性或者让构建插件如maven-jar-plugin自动处理。在Shell脚本中使用$CLASSPATH变量或直接使用:Linux和;Windows的硬编码时要准备两套脚本。编码问题确保所有源码文件、资源文件的编码统一为UTF-8。在pom.xml中全局设置properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding /properties同时在maven-resources-plugin中也要配置过滤时的编码。打包这件事看似简单实则贯穿了开发、构建、部署的整个生命周期。没有最好的方式只有最适合当前项目阶段和运维环境的方式。对于刚起步的项目直接用Spring Boot的Fat Jar简单粗暴见效快。当项目发展到一定规模特别是走上微服务和容器化道路后就需要开始考虑依赖分离、分层优化等更精细的打包策略以提升整个团队的交付效率。