Maven本地依赖与可执行jar打包:install-file到shade详解 简介面向正在学习Maven仓库机制与项目构建的Java开发者这份资源围绕仓库分类、本地包引入和可执行JAR打包三条主线展开仓库部分梳理本地仓库、远程仓库与中央仓库的定位及依赖检索顺序本地包引入则演示目录放置与pom.xml依赖声明打包部分覆盖maven-jar-plugin、maven-assembly-plugin和maven-shade-plugin三种配置方案并给出Main-Class配置示例可直接参照落地。资源内含6个文件以pom.xml构建配置、Java源文件及Eclipse工程文件.project/.classpath为主压缩包仅5KB适合作为轻量级配置参考或教学配套材料。目前已有2770人学习内容凝练但覆盖实际开发中的高频操作适合需要快速完成本地依赖引入和可执行JAR构建的开发者。1. Maven 仓库机制本地包到底卡在哪一环很多人刚接触 Maven 时都有一个误解依赖只能从中央仓库拉。真到接手一个带本地 SDK 的项目公司内部封装的 jar 包私服上没有、外网下不到随手丢进 lib 目录一引用编译报错、打包报错、换台机器直接起不来折腾半天也说不清问题在哪。这套资源的主线就是两件事先把 Maven 仓库的查找顺序和覆盖机制搞清楚再把本地 jar 以正规姿势引入工程最后用不同插件把项目打成能java -jar直接跑的可执行包。主线不绕弯适合被NoClassDefFoundError折磨过的人也适合要把交付物交给别人双击运行、又不想解释“你得配 IDE”的联调场景。2. 引入本地包install-file 与 systemPath两条路各有代价2.1 先把 jar 装进本地仓库maven-install-plugin 的 install-file本地包进不了项目根子在于它不在 Maven 的坐标体系里。中央仓库的 jar 能直接被引用是因为它有一个明确的groupId:artifactId:version三维坐标且这个坐标指向的文件已经在本地仓库里。本地包缺的不是代码而是这个坐标绑定。常见做法是用mvn install:install-file把 jar 装进本地仓库这一步本质是往~/.m2/repository下按坐标路径放文件。mvn install:install-file \ -Dfile./lib/company-sdk-2.1.0.jar \ -DgroupIdcom.example \ -DartifactIdcompany-sdk \ -Dversion2.1.0 \ -Dpackagingjar命令里的四个-D参数分别对应坐标三要素与包装类型-Dfile指向 jar 文件本身-DgroupId、-DartifactId、-Dversion决定这个 jar 将来被引用时的坐标-Dpackaging默认就是jar大部分情况下可以省略。执行完之后去本地仓库检查一下是否真的安装成功ls ~/.m2/repository/com/example/company-sdk/2.1.0/能看到company-sdk-2.1.0.jar和自动生成的company-sdk-2.1.0.pom就说明安装完成了。此时在项目pom.xml里写普通依赖就能引用和拉中央仓库依赖的写法没有任何区别dependency groupIdcom.example/groupId artifactIdcompany-sdk/artifactId version2.1.0/version /dependencyinstall-file是最接近 Maven 设计意图的引入方式因为它让本地包进入了统一的坐标寻址体系。要注意的是命令行没指定-DpomFile时Maven 会临时生成一个极简 pom里面没有任何传递依赖声明。如果这个本地 SDK 本身还依赖了某个日志库或工具包单装一个 jar 是不够的还得在项目 pom 里手动补齐它依赖的坐标否则运行期照样ClassNotFoundException。2.2 不想装进仓库也能引systemPath 与 system 作用域有些场景不适合执行 install-file公司统一管理本地仓库、不允许个人往里塞东西或者你只是临时联调一个 jar不想污染公共环境。Maven 提供了system作用域搭配systemPath的走法直接在依赖声明里写死 jar 的文件路径。dependency groupIdcom.example/groupId artifactIdcompany-sdk/artifactId version2.1.0/version scopesystem/scope systemPath${project.basedir}/lib/company-sdk-2.1.0.jar/systemPath /dependencysystem作用域的意思是“这个依赖由我手动指定位置请你不要从仓库里找”。systemPath可以写绝对路径但我一般会写成${project.basedir}/lib/...这种基于项目根目录的相对路径这样别人 clone 下来也能用。这条路的优点是快不用敲命令、不用重启 IDE刷新依赖就能看到。代价也很明确系统作用域并不参与常规的依赖解析和打包流程mvn package时它不会自动被塞进产物很多插件对system依赖默认是忽略的。我自己的习惯是systemPath只用于本地十分钟内的验证确认逻辑没问题之后立刻换成install-file正规安装。如果把systemPath写进正式项目还提交到仓库换机器那天就是大型翻车现场别人拉完代码编译通过、跑起来缺类根本不知道要去改哪里。2.3 坑装进仓库的 jar 缺了 pom传递依赖全丢install-file自动生成的极简 pom 里有且只有坐标和 packaging 两个信息。假设这个本地 SDK 内部依赖了commons-lang33.12 版本安装时命令行没带-DpomFile那么引用方的依赖树里根本不会出现 commons-lang3。表现是编译期没事一运行就报NoClassDefFoundError: org/apache/commons/lang3/StringUtils而且位置很偏不是项目自己的代码。解决思路有两个如果拿得到 SDK 自带的 pom 文件安装时补上-DpomFilecompany-sdk-2.1.0.pom让依赖关系跟着一起进入本地仓库拿不到的话就在项目 pom 里把 SDK 的传递依赖手动补全。补全后跑一次mvn dependency:tree验证依赖树是否符合预期这一步能省大量排查时间。3. 打可执行 jar 的基础盘maven-jar-plugin 与 classpath 参数3.1 最小配置让 jar 带 Main-Class 入口可执行 jar 的前提是MANIFEST.MF里有Main-Class属性。java -jar启动时 JVM 只读这个属性来找入口类没有它就会报no main manifest attribute。maven-jar-plugin 是最轻量的解决方案它不负责把依赖塞进 jar只负责在构建期写 manifest。build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.4.1/version configuration archive manifest mainClasscom.example.Main/mainClass /manifest /archive /configuration /plugin /plugins /build这段配置的核心是manifestmainClass它决定写入MANIFEST.MF的Main-Class值。mainClass必须是类的完全限定名不带.class后缀而且要确保这个类确实有public static void main(String[] args)方法否则启动时同样会报错。这个插件打出来的 jar 是“瘦 jar”体积很小因为它只包含项目自身的 class 与资源文件。3.2 Class-Path 不配好入口找到了也白搭Main-Class只是第一步。瘦 jar 不包含依赖JVM 运行时还需要知道去哪里找依赖类这就靠 manifest 里的Class-Path属性。常见的做法是配合 maven-dependency-plugin 把依赖复制到 jar 旁边的lib目录然后让 manifest 里的所有依赖路径统一带上lib/前缀。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-dependency-plugin/artifactId version3.6.1/version executions execution idcopy-dependencies/id phasepackage/phase goals goalcopy-dependencies/goal /goals configuration outputDirectory${project.build.directory}/lib/outputDirectory excludeScopetest/excludeScope /configuration /execution /executions /plugincopy-dependencies会把所有compile与runtime作用域的依赖拷贝到target/lib目录。随后在 maven-jar-plugin 的 manifest 配置里增加classpathPrefixarchive manifest mainClasscom.example.Main/mainClass classpathPrefixlib//classpathPrefix /manifest /archive完成后的 manifest 中Class-Path会是一长串以lib/开头的相对路径比如lib/company-sdk-2.1.0.jar lib/log4j-api-2.20.0.jar。这里有个隐藏要求lib目录必须和产物 jar 放在同一级。你运行java -jar target/demo.jar时JVM 是以当前执行目录为基准去找lib/的如果手动把 jar 移动走了、没带上lib目录或者换了个目录执行入口类能加载、业务类加载不到报错样式千奇百怪。这种“轻包 外置依赖目录”的交付结构适合需要控制单个文件体积的场景比如公司内部传输有限制时但分发时少带一个文件就全崩。3.3 参数边界jar-plugin 不是万能的maven-jar-plugin 的适用边界很清晰它能解决“入口可找到”不解决“依赖被包含”。如果你只有这一个插件java -jar能不能跑取决于lib目录是否完整、Class-Path是否相对关系正确。还有两个容易忽略的参数细节archiveindex设为true时会生成META-INF/INDEX.LIST目录索引略微加快类加载但 jar 被移动后更容易出现ClassNotFoundaddClasspath在某些旧配置中被误用它只能把Class-Path生成为绝对路径或基于目录的相对路径面对复杂依赖树时经常写出错误条目。所以我一般明确写出classpathPrefix而不是依赖插件默认行为。提示判断瘦 jar 是否配置正确的最快方式是构建后执行unzip -p target/demo.jar META-INF/MANIFEST.MF直接看 manifest 内容一切玄学在明文前都会现形。4. 打胖 jar 的默认答案maven-shade-plugin 与三处关键配置4.1 为什么胖 jar 选 shade合并与重定位一条龙瘦 jar 要带着一整包lib目录到处走总有人嫌麻烦。胖 jar 的思路是把所有依赖的 class 解压后重新合并进同一个 jar让产物只有一个文件。maven-shade-plugin 是这个领域最常用的工具它能处理同名资源覆盖、SPI 配置合并、依赖排除等瘦 jar 方案压根碰不到的问题。核心配置如下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.3/version executions execution phasepackage/phase goals goalshade/goal /goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.Main/mainClass /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 /pluginManifestResourceTransformer负责重写Main-Class。注意这里不是简单地在已有 manifest 上追加而是合并所有依赖 jar 的 manifest 后重新生成所以mainClass必须在这里配置写在 maven-jar-plugin 里会被吞掉。filters用来忽略签名文件是另一个关键点依赖 jar 里常见的META-INF/*.SF、*.DSA、*.RSA是 JAR 签名文件直接合并进胖 jar 会在运行时触发SecurityException多数人第一次打胖 jar 都会在这里翻车。4.2 依赖冲突与服务文件同名资源覆盖是玄学重灾区多个 jar 合并之后会产生一个天然矛盾不同 jar 里可能存在同名路径的 class 或配置文件。shade 的默认策略是“后声明的依赖覆盖先声明的”这个顺序由 pom 中的依赖声明顺序决定肉眼很难看出最终生效的是哪一个。遇到类冲突时先跑一次依赖树定位重复项mvn dependency:tree -Dverbose -Dincludescom.example:company-sdkdependency:tree会把每个依赖的版本冲突明细列出来但由于依赖顺序本身很难从 pom 表面判断所以更可靠的做法是不猜直接排除不需要的重复项或者用artifactSet中的excludes把确定不用的依赖从打包范围剔除。剔除后构建产物里就只剩一个版本的 class问题随之消失。SPI 配置文件是另一类易被覆盖的资源。比如多个依赖 jar 都带有META-INF/services/java.sql.Driver合并时后者会把前者的内容整个覆盖导致 JDBC 驱动注册不全。这个时候需要追加一个 transformertransformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/ServicesResourceTransformer会读取所有依赖 jar 中META-INF/services目录下的同名文件把内容逐行合并后统一写进最终产物。如果项目里用了 SPI 机制加载服务实现配置里没有这一行就等于给运行期埋雷。类似地Spring 项目的spring.handlers、spring.schemas文件还要用到AppendingTransformer具体取决于你的技术栈但道理相同同名资源在合并时必须显式声明处理策略。4.3 不再需要 original jar输出产物与瘦身思路shade 插件执行后target目录下会出现两个文件demo.jar是合并后的胖 jaroriginal-demo.jar是未合并之前的原产物。如果确定只交付胖 jar可以在配置里加configuration shadedArtifactAttachedfalse/shadedArtifactAttached promoteTransitiveDependenciestrue/promoteTransitiveDependencies /configurationshadedArtifactAttached控制是否把胖 jar 附加为独立产物类型promoteTransitiveDependencies则会影响依赖范围的处理方式多数场景不需要动。胖 jar 体积大是常态想瘦身可以在filters里配置exclude排除某个依赖下的无用包路径比如所有依赖里的module-info.class、META-INF/versions/9/兼容目录等。排除前先确认没有反射或按包名扫描的逻辑否则瘦身变瘦死。5. 可执行 jar 排查手册五个启动失败的典型原因5.1 打包成功但启动即报 ClassNotFound现象java -jar demo.jar执行后立刻抛NoClassDefFoundError堆栈里的类来自某个依赖 jar。原因要么是瘦 jar 没把依赖带进去要么是Class-Path指错了相对路径。解决先用unzip -l demo.jar | grep 类名确认这个类到底在不在 jar 里。如果不在说明打的还是瘦 jar 或依赖没被 shade 合并如果在再看META-INF/MANIFEST.MF里的Class-Path路径与当前 jar 所在目录是否匹配。最省事的做法是直接换成 shade 打胖 jar从根上消灭路径问题。5.2 no main manifest attribute入口配置被覆盖现象启动报no main manifest attribute, in demo.jar。原因Main-Class没有写进 manifest。可能是 maven-jar-plugin 配置里漏了mainClass也可能是多个插件先后执行后执行的打包插件把 manifest 重写了。解决unzip -p demo.jar META-INF/MANIFEST.MF检查有没有Main-Class行。如果 manifest 里确实没有确认 maven-jar-plugin 或 shade 的 transformer 是否正确绑定到了package阶段。如果项目同时配了 maven-jar-plugin 和 shadeMain-Class会由后执行的插件决定原则上是 shade 的 transformer 最后掉笔你只需要把 mainClass 配到最终生效的那个插件里。5.3 SecurityException签名文件被合进了胖 jar现象启动报java.lang.SecurityException: Invalid signature file digest for Manifest main attributes。原因依赖 jar 里有签名文件META-INF/*.SF等合并到胖 jar 后 JVM 校验 manifest 和类时发现内容对不上判定为“包被改过”。解决在 shade 的filters中排除签名文件后缀标准三件套就是.SF、.DSA、.RSA。过滤之后重新打包即可。这个现象几乎只出现在胖 jar 场景瘦 jar 因为依赖独立存放很少触发。5.4 配置文件找不到资源没进包或被过滤改写现象启动后报FileNotFound或拿到的是空的配置对象常见于application.yml、log4j2.xml等资源。原因默认打包只包含src/main/resources下的文件。放在项目根目录或lib目录下的配置文件不会自动进 jar另外pom.xml里开启filtering时${...}占位符可能被替换成空内容。解决先把必要资源放进src/main/resources或者用resources显式声明额外资源目录。如果排查发现配置文件内容被污染把 filtering 关掉只对真正需要动态替换的文件单独开启。5.5 换机器就起不来本地仓库的“这里能跑”幻觉现象开发机运行正常发送到测试机或 CI 服务器立刻NoSuchMethodError或ClassNotFoundException。原因本地包通过install-file装只进入了你自己的~/.m2/repository测试机仓库里没有这个坐标构建时拉取失败或者两个机器上的依赖版本不一致旧版本的类缺新方法。解决本地包要么上传到私服统一管理要么在项目里附带安装脚本换机器先执行 install-file。正式项目里所有依赖版本尽量在dependencyManagement中集中声明锁死版本。提交代码前清掉本地仓库里手动的坐标产物重新全量构建一次能复现问题就说明有机器相关依赖未声明。6. 验证可执行 jar 的边界一套脚本盯住四种坑每次构建完都要靠手动java -jar验一次效率太低而且常驻进程会把终端卡住。我习惯写一个小脚本构造后自动检查四件事jar 是否存在、manifest 里有没有入口、入口类是否真在 jar 内、程序能不能在超时时间内正常启动。#!/usr/bin/env bash # verify-jar.sh构建后验证可执行 jar 的关键属性 set -euo pipefail JAR_PATH${1:-target/demo.jar} echo 1. 检查 jar 是否存在 test -f $JAR_PATH || { echo 找不到 $JAR_PATH; exit 1; } echo 2. 读取 MANIFEST.MF 中的入口与依赖路径 unzip -p $JAR_PATH META-INF/MANIFEST.MF \ | grep -E ^Main-Class|^Class-Path || echo 注意未找到 Main-Class echo 3. 确认入口类真实存在 MAIN_CLASS$(unzip -p $JAR_PATH META-INF/MANIFEST.MF \ | awk /^Main-Class:/{print $2} | tr -d \r) if [ -z $MAIN_CLASS ]; then echo Main-Class 为空结束检查; exit 1 fi CLASS_PATH$(echo $MAIN_CLASS | tr . /).class unzip -l $JAR_PATH | grep -q $CLASS_PATH \ || { echo 入口类缺失: $CLASS_PATH; exit 1; } echo 4. 试运行 2 秒判断能否正常启动 timeout 2 java -jar $JAR_PATH /tmp/jar-run.log 21 code$? if [ $code -eq 124 ]; then echo 程序运行超过 2 秒被 timeout 截停说明启动流程正常 elif [ $code -eq 0 ]; then echo 程序在 2 秒内自行退出退出码 0同样视为通过 else echo 启动失败退出码 $code最近日志如下 tail -20 /tmp/jar-run.log exit 1 fi脚本的关键点在第 3 步把Main-Class的点号全部替换成斜杠拼出.class的相对路径再用unzip -l查 jar 列表确认入口类真的被打进去了。第 4 步用timeout 2包住java -jar常驻服务会因超时被截停并返回 124这反而证明启动成功如果进程快速退出要看退出码和日志。这个脚本分辨不了业务逻辑对错但能挡住大部分“入口找不到、依赖没跟上、资源缺失”的低级问题。从那以后我每次要把 jar 交给别人之前都会强制走一遍这个验证流程不再拿“在我机器上能跑”当交付理由。希望帮到你。本文还有配套的精品资源点击获取