资讯动态

Maven仓库与可执行Jar包:本地依赖引入与打包避坑实战

发布时间:2026/10/18 9:09:01 来源:尧图企业网站定制
简介面向Java开发者的Maven学习示例包聚焦仓库理解、本地依赖引入与可执行jar包打包三大主题适合初中级开发者快速建立Maven构建认知也适合在开发中遇到打包难题时对照检索。压缩包内含一个完整Eclipse Maven工程共6个文件以pom.xml、Java主类、classpath/project工程配置与Eclipse预置偏好文件为主整体仅5KB属于轻量源码配置型资料便于直接查看配置与运行验证。内容从本地仓库目录~/.m2/repository、远程仓库到中央仓库的职责逐一说明并演示如何将本地jar包按groupId/artifactId/version路径放入仓库再通过mvn install完成引用随后详细给出maven-jar-plugin、maven-assembly-plugin、maven-shade-plugin三种打包可执行jar的配置方法覆盖普通可执行jar、含依赖的fat jar及避免类冲突的shade jar等场景对Main-Class设置、classpathPrefix与资源转换器均有明确示例可复制到项目中调整使用。已有2770人学习下载配合自身项目动手执行可逐步掌握Maven依赖管理与打包发布的完整链路。1. Maven仓库与可执行Jar包你可能用错了引入本地包的方式在用 Maven 做 Java 项目时很多初次接触的人都会遇到一个尴尬场景把人家提供的 SDK jar 往项目 lib 目录一丢IDE 里编译、运行一切正常但执行mvn package得到 jar 之后java -jar一启动就报ClassNotFoundException。这个问题的根源不在代码而在你对 Maven 仓库的理解——Maven 根本不知道你放在 lib 目录下的那个 jar 的存在。本文不兜售复杂的理论直接梳理 Maven 的仓库体系、引入本地包的三种主流姿势以及打可执行 jar 包的多种实现方式并附上我实际踩过的坑。适合正在做工程交付、需要把项目从 IDE 跑通到命令行交付的 Java 开发者。2. Maven仓库体系本地仓库、中央仓库与私服的三级结构2.1 仓库的本质本地仓库不是项目的库是机器的依赖缓存Maven 的仓库和你项目根目录下的lib/是两个完全不同的概念。本地仓库默认在用户主目录下的.m2/repository它属于操作系统级别的依赖缓存属于用户机器上的这个 Maven 实例不属于某个具体项目。整个 Maven 依赖管理的流程可以理解为三个字符坐标groupId:artifactId:version作为唯一寻址方式本地仓库作为首查缓存查不到时从中央仓库或私服远程拉取拉取后再落盘到本地仓库。举个例子说明。你在pom.xml里声明依赖org.springframework.boot:spring-boot-starter-web:2.7.18Maven 的处理顺序是先在本地仓库看有没有org/springframework/boot/spring-boot-starter-web/2.7.18/目录如果没有就去你在settings.xml或 pom 里配置的远程仓库地址下载默认是 Maven Central下载完成之后会连同.pom文件一起存到本地仓库中。这个过程表面上是依赖管理本质上是文件缓存与坐标寻址。至于私服可以把它理解成你内网自己搭的中央仓库代理。团队协作场景下私服帮你解决中央仓库访问慢、私有包不能外发、依赖版本统一治理这三大问题。它的角色类似于一个中间缓存开发者在pom.xml里照常写坐标实际下载流量先走进内网私服这样不会让每个开发者的机器直接面对外网和慢速连接。2.2 坐标定位为什么 lib 目录下的 jar 编译不认Maven 完全不理会你放在项目根目录lib/下的 jar 文件。你在 IDE 里能跑是因为 IDE 默认帮你把lib/目录加入了 classpath但在 Maven 的世界里它只按坐标寻址。没有把 jar 注册到本地仓库或者 pom 依赖体系里Maven 打包的时候根本不会把它算进依赖图。很多新手会误以为scopesystem就能解决一切问题后面会细讲这个坑。这里先说明一点Maven 对依赖的解析完全依赖坐标模型你不是少了一个功能开关而是少了一次坐标注册。你需要在pom.xml中给出依赖的groupId、artifactId、versionMaven 才能找到它。这也是为什么直接把 jar 文件扔进 lib 目录后mvn clean package时编译能过但打出来的 jar 里并没有那个 SDK 的原因——编译期 IDE 认识Maven 不认识。排查依赖问题时先跑一遍依赖树是很有用的习惯。命令如下mvn dependency:tree执行后会输出你当前项目的完整依赖树。如果是编译报错、缺类、打出来的 jar 启动报错先用这个命令看漏了哪个坐标。参数方面没什么需要调的它就是 Maven 内置插件maven-dependency-plugin的默认执行输出到控制台。我一般还会配合-Dverbosetrue查看依赖被谁引入的路径排查冲突时很有用。2.3 settings.xml 关键配置localRepository、mirror 与 server很多操作都需要先确认settings.xml配置对了否则后续所有命令都可能拉取失败或者走错仓库。settings.xml存放在 Maven 安装目录的conf/下全局配置或用户主目录的.m2/下用户配置用户配置优先。常用配置块如下settings localRepository/data/maven_repository/localRepository mirrors mirror idaliyun/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors /settings这里localRepository指本地仓库路径。默认是~/.m2/repository很多公司要求统一把本地仓库指到 D 盘或大硬盘路径避免 C 盘膨胀。mirrorOf的值有别的地方踩坑central表示只对中央仓库生效*表示所有远程请求都走这个镜像一般内网私服会配*。注意mirror 和仓库repository是两个概念mirror 拦截并转发请求repository 是真正的远程仓库源。配置 mirror 可以不改 pom全局都走镜像。server 配置用于认证私服场景包含id、username、password其中id必须和 pom 中repositoryid保持一致才会生效。3. 引入本地包install-file、system scope 与本地文件目录方案3.1 方法一mvn install:install-file给本地包上户口把本地 jar 注册到本地仓库是正规军做法。常见场景是某供应商给你一个sdk-2.0.jar没有发布到中央仓库也没发到公司私服你要在自己项目里引入。执行命令如下mvn install:install-file -Dfile./lib/sdk-2.0.jar -DgroupIdcom.example -DartifactIdsdk -Dversion2.0.0 -Dpackagingjar执行完成之后本地仓库会出现com/example/sdk/2.0.0/sdk-2.0.0.jar同时生成对应的.pom文件。此时你的项目里就能正常写坐标引用了dependency groupIdcom.example/groupId artifactIdsdk/artifactId version2.0.0/version /dependency这能正常工作本质上是把文件变成了坐标。注意事项有几个。-Dfile路径可以是相对路径建议用绝对路径避免在不同目录下执行命令时产生不一致。-DgroupId、-DartifactId、-Dversion这三个参数就是坐标的组成你得自己定一套规范别随便起名不然就是给以后的自己埋雷。版本号命名也建议遵循语义化版本规范x.y.z 三段式不要出现v2-final-最终版这种名字。此外如果 jar 还依赖别的本地包安装时可以用-DgeneratePomtrue让它自动生成 pom但更稳妥的做法是在 pom 里手写依赖声明。3.2 方法二scopesystem省事但部署时会翻车这种方式在很多项目的 pom 里能看到但它需要特别警惕。写法如下dependency groupIdcom.example/groupId artifactIdsdk/artifactId version2.0.0/version scopesystem/scope systemPath${project.basedir}/lib/sdk-2.0.0.jar/systemPath /dependencyscopesystem表示这个依赖不在仓库中查找直接使用文件系统的指定路径。缺点是显而易见的systemPath是硬编码路径项目拷贝到别人的机器上只要目录结构不变还能工作一旦换一个构建环境或者用 CI 服务器打包很可能就找不到这个 jar 导致构建失败。更严重的是打可执行 jar 包时scopesystem的依赖默认不会被包含进最终的 jar 包里这就是很多开发者在本地能跑、交付后运行就报ClassNotFoundException的根本原因。怎么规避如果你只是临时在本机试一下本地 jar用 system scope 没问题。但代码要提交、构建要交付我一般不会用它。另一种思路是用 profile 做动态切换本地开发用 system scope正式构建用 install-file 方式但这样 pom 里会写两套依赖维护成本不小。综合来看没必要为了省一条命令引入这种不一致的构建语义。3.3 方法三把 lib 目录变成一个小型本地文件仓库不需要注册到全局本地仓库也不需要用 system scope可以用 pom 里的file://协议直接指向一个本地路径repositories repository idlocal-lib/id urlfile://${project.basedir}/lib/url /repository /repositories这样 Maven 会把lib/目录当作一个本地文件仓库来解析路径层级必须遵循坐标目录结构比如lib/com/example/sdk/2.0.0/sdk-2.0.0.jar。优点是不污染全局本地仓库项目带着 lib 目录走别人 clone 下来也能构建。缺点也很明显你必须在 lib 下手动维护坐标目录层级每更新一个版本都要手动摆目录、放 jar特别容易出错。我一般只会在离线场景或者给别人做一个演示 demo 时用这个方案真正的项目不建议这么干。3.4 三种方案选型从使用场景判断三种方案的取舍最终取决于你的使用场景。用表格来对比会直观一些。方案使用场景主要缺点install-file私有 SDK、需要跨项目复用、要进 CI 构建每个开发机器都要执行一次或者依赖私服system scope本地临时试用、demo 快速验证不参与打包、不可移植、同一坐标在不同机器上可能不一致file:// 本地仓库离线环境、demo 分发手动维护目录结构多人协作容易出错记住一个判断标准如果你要把它变成可以在任何构建机器上复现的项目依赖项install-file是底线方案。如果你只是要在本机跑通一件事另外两种也不是不能用。我自己的习惯是能上私服就上私服不能上私服就写一个 README 说明让每个人执行 install-file 命令绝不把 system scope 带进主线代码。4. 多种打可执行 Jar 包方式maven-jar-plugin、shade 与 assembly 对比4.1 可执行 jar 的前提主类找到、依赖放对、本地包不丢给 Maven 项目打可执行 jar 包拆开来看其实是三个问题。第一个是主类声明java -jar启动时需要通过MANIFEST.MF文件里的Main-Class属性去找到入口默认 Maven 打包不会帮你写这个值。第二个是依赖路径JVM 执行 jar 时不会自动解析 Maven 的依赖关系要么把所有依赖 jar 解压合并到同一个 fat jar 中要么把依赖 jar 拷贝到单独目录并在Class-Path里声明。第三个是资源文件配置文件、spi 文件、META-INF/services等如果被排除或覆盖运行时会出现各种稀奇古怪的错误。这三种目标分别对应三类主流插件maven-jar-plugin解决主类和 Class-Path 声明maven-shade-plugin解决依赖合并和主类声明maven-assembly-plugin解决自定义包结构。下面逐个说明配置和适用场景。4.2 方式一maven-jar-plugin 外部依赖目录这种方式适合不想打 fat jar希望交付物中包含一个可执行 jar 外加一个lib/依赖目录的场景。核心思路是jar-plugin 负责生成 MANIFEST 里的Main-Class和Class-Path依赖目录用maven-dependency-plugin的copy-dependencies目标来拷贝。配置示例如下build plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest mainClasscom.example.Main/mainClass addClasspathtrue/addClasspath classpathPrefixlib//classpathPrefix /manifest /archive /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-dependency-plugin/artifactId version3.5.0/version executions execution idcopy-dependencies/id phasepackage/phase goals goalcopy-dependencies/goal /goals configuration outputDirectory${project.build.directory}/lib/outputDirectory includeScoperuntime/includeScope /configuration /execution /executions /plugin /plugins /build这段配置的核心逻辑是addClasspath自动生成Class-Path字段把所有 Maven 依赖按照相对路径lib/xxx.jar写入 MANIFESTclasspathPrefix指定前缀和copy-dependencies的输出目录保持一致includeScope用runtime可以避免打包时把provided依赖带进来比如 servlet-api 这种本来由容器提供的 jar。这个方案的优点是你随时能看到哪些依赖被带走了交付物结构清晰缺点是目录结构比单 jar 要多一层部署时要保证lib/和主 jar 的相对位置不变。4.3 方式二maven-shade-plugin最常用的 fat jar 打法当你要交付一个单个 jar 拿来就能运行的产物shade 是覆盖面最广的做法。它把项目自身的代码和所有依赖的 jar 解压后合并到一个 uber jar 中同时通过ManifestResourceTransformer写入Main-Class。核心配置如下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/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 /plugin这个配置中ManifestResourceTransformer负责把主类写进 MANIFESTfilters里的excludes段是排除签名文件这一步不能省略。依赖 jar 里经常带有META-INF/MANIFEST.MF、.SF、.RSA之类的签名文件合并到同一个 jar 时 JVM 会因为签名校验失败而报SecurityException排除这三个后缀可以避免。artifact*:*表示对所有依赖生效。需要注意shade 插件默认会覆盖maven-jar-plugin生成的原始 jar——它的执行目标是shade:shade绑定 package 阶段后会取代原 jar所以你最终拿到的是以-shaded.jar结尾的产物。使用 shade 时还要考虑 SPI 文件的合并问题。多个依赖 jar 中存在相同的META-INF/services/xxx文件时默认行为是后者覆盖前者这会导致某些基于 ServiceLoader 扩展的功能静默失效比如数据库驱动、日志框架的 SPI 实现。解决方案是额外加一个ServicesResourceTransformertransformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/在 transformers 标签里和 ManifestResourceTransformer 并列配置即可。这个坑比较隐蔽下面章节会单独展开。4.4 方式三maven-assembly-plugin定制化程度更高的选择assembly 插件的定位比较特殊它以descriptor为描述文件构建一个任意结构的发布包不只是 jar可以是 zip、tar.gz。与 shade 的核心差别在于shade 默认是纯 fat jarassembly 完全由你定义。做一个包含可执行 jar、配置文件、启动脚本的发布包assembly 非常合适。一个最小配置可以写成这样plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId version3.6.0/version configuration descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs archive manifest mainClasscom.example.Main/mainClass /manifest /archive /configuration executions execution idmake-assembly/id phasepackage/phase goals goalsingle/goal /goals /execution /executions /pluginjar-with-dependencies是内置描述符效果和 shade 类似是把所有依赖统一打入一个 jar 中同时写入 Main-Class 配置。进阶用法是自定义assembly.xml描述符控制依赖放置目录、配置文件拷贝、启动脚本生成。比如你这样描述assembly xmlnshttp://maven.apache.org/ASSEMBLY/2.1.0 idbin/id formats formatzip/format /formats dependencySets dependencySet outputDirectorylib/outputDirectory scoperuntime/scope /dependencySet /dependencySets fileSets fileSet directory${project.basedir}/scripts/directory outputDirectorybin/outputDirectory fileMode0755/fileMode /fileSet /fileSets /assembly这种做法的好处是当你的交付物是一个完整服务或者工具集而不是仅一个 jar 时assembly 可以把依赖、配置、脚本都统一编排好。代价是它的 descriptor 语法比 pom 内联配置复杂上手成本略高。4.5 三种插件怎么选不必追求单 jar 唯一论选择的关键要看你的产物如何被使用。单 jar 可以被 java -jar 直跑适合内部工具、命令行程序jar lib 目录适合 Web 服务部署更新依赖时只需替换 libzip 发布包适合还要带外部配置和启动脚本的场景。我通常的建议是哪怕最终交付只需要一个 jar 文件也建议用 shade并且把relocation这个功能利用起来。relocation可以重定位依赖包名用于解决不同模块依赖同一库但版本不一致导致的冲突。用法如下relocations relocation patterncom.google.gson/pattern shadedPatterncom.example.shaded.gson/shadedPattern /relocation /relocations常见坑点是重定位后如果代码里有反射、spi 字符串常量很可能找不到类。这一点后面会在排查部分讲到。5. 避坑与常见问题排查五个高概率翻车场景与解法5.1 坑一install-file 之后重新构建还是报依赖找不到现象执行 install-file 后项目里依赖坐标一直标红重新 import Maven 也没有用。原因这种情况多半是因为你的项目用了 SNAPSHOT 快照版本而 Maven 对于-SNAPSHOT依赖有定期检查更新的机制默认更新策略是 daily也就是一天之内不会重新检查。你把 SNAPSHOT 版本 install 到本地仓库后Maven 缓存了旧的反查结果导致新安装的版本没被识别。解决在 pom 或 settings 中强制刷新快照。命令行可以加-U参数mvn clean install -U-U的含义是将所有 SNAPSHOT 依赖强制更新到最新版本。如果还是不生效可以检查一下本地仓库目录下有没有.lastUpdated文件那个文件的存在说明之前下载失败过把它删掉再重试。还有一种可能是你的 coordinates 写错了install-file 时 groupId 和 artifactId 与你 pom 中声明的不完全一致大小写差一个字符都会失败。我一般执行完 install-file 会直接去本地仓库目录ls确认 jar 确实落盘了再回 IDE 写坐标。5.2 坑二打出的 jar 启动报 no main manifest attribute现象执行java -jar xxx.jar时控制台提示no main manifest attribute, in xxx.jar原因jar 包本身没有MANIFEST.MF里的Main-Class字段。常见于 jar 包是 shade 插件配置覆盖前生成的原 jar或 jar-plugin 没有配置mainClass。解决第一步先看 MANIFEST 里的实际情况解包查看unzip -p target/xxx.jar META-INF/MANIFEST.MF如果Main-Class为空检查 pom 里有没有为maven-jar-plugin配置archivemanifestmainClass。如果配置了还不生效多半是finalName配置冲突或者 shade 的替换逻辑没跑。最直接的办法是给 shade 插件配ManifestResourceTransformer它会强制重新生成 MANIFEST。另外注意Main-Class必须写类的全限定名不带.class后缀并且该类必须包含标准的public static void main(String[] args)方法。5.3 坑三本地包system scope导致运行时 ClassNotFoundException现象代码里直接引用本地 SDK 的类IDE 编译通过打包成 jar 后运行时调用相关方法就报ClassNotFoundException: com.xxx.sdk.SdkClient。原因scopesystem依赖不会进入最终打出的 jar 包。无论用 shade 还是 jar-pluginMaven 都不会把 system scope 的依赖合并到产物里。IDE 不报错是因为 IDE 自己把lib/目录加到了编译 classpath 中和 Maven 打包逻辑是两回事。解决如果要打 fat jar把本地 jar 先转成仓库坐标也就是走 install-file或者干脆用 profile 区分环境。如果临时处理一条 system 依赖可以在 shade 插件配置里手动补一个额外的 artifactItemartifactSet includes includecom.example:local-sdk/include /includes /artifactSet再用 install-file 先注册这个坐标shade 才能拿到它。直接让 shade 绑定一个目录文件是不支持的操作所以这个问题的终极解法还是要回到坐标注册那一步不要在 system scope 上死磕。5.4 坑四打包后通过 ServiceLoader 加载的组件静默失效现象打包后的 jar 运行时不报错但某些依赖的功能没有初始化比如自定义数据库驱动、自定义日志拦截器没有生效而 IDE 里跑一切正常。原因META-INF/services 目录下的 SPI 配置文件在多个 jar 合并时发生了覆盖。shade 默认的合并模式是替换也就是后出现的同名文件把先出现的覆盖了导致最终只保留了其中一个实现类。解决在 shade 插件 transformers 中增加ServicesResourceTransformer做 SPI 文件的合并。配置如下transformers transformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/ transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.Main/mainClass /transformer /transformers注意顺序没有强制要求但一般把 Services 放在 Manifest 前面。我遇到过一次比较隐蔽两个依赖 jar 中各有META-INF/services/org.slf4j.spi.SLF4JServiceProviderSPI 合并后 SLF4J 绑定变随机日志全部打到控制台不输出文件。做业务代码时 SLF4J 这种常见绑定问题已经很难找更别提自己封装的服务发现机制了。所以只要打了 fat jarSPI 合并的 awareness 要有凡是看到多层依赖都顺手加上这个 transformer。5.5 坑五包的签名文件导致 SecurityException现象fat jar 启动时报java.lang.SecurityException: Invalid signature file digest for Manifest main attributes。原因多个 jar 合并时META-INF/*.SF和*.RSA签名文件混入最终 jar。JVM 在处理带签名信息的 jar 时会验证签名文件是否和 MANIFEST 一致合并后签名文件和条目对应关系错乱了就会直接拒绝运行。解决在 shade 插件的 filters 中排除这些文件。之前第 4 章的配置里已经写了那段 excludes那是必需品不是可选优化filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters注意要同时列出.SF、.DSA、.RSA只排除一个后缀不完整。出现这个异常时也可以先用 unzip 打开 jar 看一眼META-INF目录下还有没有这些文件。这个坑在 Spring Boot 项目的 fat jar 里相对少见因为 Spring Boot 自己的打包插件已经处理过如果用原生 shade 做非 Spring Boot 项目就必须自己加这一层。6. 交付前验证一个固定要走的检查流程shaded 插件配置完后很多人习惯直接执行mvn package然后把 jar 扔给别人就完事了。我现在的习惯是打包之后必须走一整套固定验证可以有效避免回到现场才发现问题。第一步是看 MANIFEST。对任何打出来的 jar先用解包命令查看主类和 Class-Pathunzip -p target/xxx-all.jar META-INF/MANIFEST.MFExpect 的是Main-Class和Class-Path字段都符合预期不存在字段为空或路径写错的情况。如果是 fat jarMain-Class正常即可是胖包。第二次是用jdeps检查依赖确认没有非预期的类不见或依赖缺失的问题。jdeps --print-module-deps target/xxx-all.jarjdeps 还会打印 jar 中是否引用了 JDK 以外但没有被列入模块的类库如果输出里有not found那就是某个本地包没有合并进去。第三步是直接运行java -jar target/xxx-all.jar --help这一步可以在部署前发现启动路径相关的类加载问题。如果项目使用了外部配置文件我还会把 jar 放到一个全新目录不带任何 classpath 地执行一次看看有没有隐式的相对路径依赖。第四步是检查本地仓库缓存确认安装的坐标版本没有遗留问题ls ~/.m2/repository/com/example/sdk/目录结构合理才继续下一步。对于多模块项目我还会顺手用mvn dependency:analyze检查未使用依赖和声明未使用、使用未声明的依赖这一步能提前发现一些潜在的问题。那次在交付某跨平台系统时就是因为新加的工具包没有执行最后一步导致 spu 文件没合并现场启动后功能缺失排查了一整个下午。从那以后我每次打包完都会强制走一遍这个流程解包看清单jdeps 查依赖java -jar 实测调用再确认本地仓库坐标完整。这套流程不挑项目凡是 Maven 构建的 Java 工程都适用。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑