Maven可执行JAR与仓库机制深度解析 简介本资源是一份面向Java开发者与Maven初学者的实战型学习资料聚焦Maven核心机制中的仓库管理、本地依赖引入及可执行JAR包构建三大痛点问题。内容覆盖本地/远程/中央仓库的定位与协同逻辑手把手演示本地JAR包手动安装与POM声明方法并对比详解maven-jar-plugin、maven-assembly-plugin和maven-shade-plugin三种打包方案的适用场景与配置要点。压缩包共6个文件5KB含2个Eclipse偏好设置文件.prefs用于IDE环境适配、1个关键pom.xml配置模板、1个示例Java主类、1个.classpath工程路径配置及1个.project项目元信息文件结构精简开箱即用。目前已有2772人学习下载适合需要快速理解Maven底层依赖流转、解决本地包引用异常或生成独立运行jar的中初级Java工程师。1. Maven 可执行 JAR为什么mvn package出来的 jar 双击就报“no main manifest attribute”你写完一个 Spring Boot 之外的纯 Java CLI 工具mvn clean package一跑target 目录下赫然躺着my-tool-1.0.0.jar——双击闪退。命令行敲java -jar my-tool-1.0.0.jar控制台冷冰冰甩出一句no main manifest attribute, in my-tool-1.0.0.jar。这不是配置漏了也不是 IDE 抽风而是 Maven 默认压根不往 MANIFEST.MF 里写 Main-Class。它把「可执行」这件事交给了你——但不是靠手改 MANIFEST而是靠理解 Maven 的坐标体系、依赖解析机制、以及三种打包策略背后的设计契约。这份资源不是教你怎么点几下鼠标生成 jar而是带你钻进maven-jar-plugin的archive配置、maven-shade-plugin的类重定位逻辑、甚至spring-boot-maven-plugin的LaunchScript注入时机。搞懂它你才能在 CI 流水线里稳稳输出带完整依赖入口点的 fat jar而不是每次上线前临时jar -uf手动补 MANIFEST——那才是真正的玄学运维。2. Maven 仓库的本质不是文件夹而是坐标寻址协议Maven 仓库常被误认为是“一堆 jar 包堆成的本地文件夹”但这种理解会直接导致mvn dependency:copy-dependencies失败时抓瞎。真实情况是Maven 仓库是一套基于 GAV 坐标GroupId:ArtifactId:Version的 HTTP 可寻址命名空间。它不关心物理路径只认坐标而本地.m2/repository只是这套协议的一个缓存实现。2.1 坐标如何映射到文件路径——从org.springframework:spring-core:5.3.31到~/.m2/repository/org/springframework/spring-core/5.3.31/spring-core-5.3.31.jarMaven 将 GAV 坐标按规则转换为路径这是所有插件行为的基础GroupId: org.springframework → 路径分段org/springframework ArtifactId: spring-core → 路径分段spring-core Version: 5.3.31 → 路径分段5.3.31 最终 jar 名spring-core-5.3.31.jar这个映射不是约定俗成而是硬编码在org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout类中。你可以验证删掉本地~/.m2/repository/org/springframework/spring-core/5.3.31/执行mvn compileMaven 会自动从中央仓库或你配置的镜像下载并严格按此路径结构存放。关键点在于Maven 从不扫描文件夹找 jar它永远先拼路径再检查该路径下是否存在*.jar和*.pom。2.2 本地包引入的三种合法姿势为什么system范围是反模式当你有一个公司内部未发布到 Nexus 的legacy-util-2.1.jar常见错误是写dependency groupIdcom.example/groupId artifactIdlegacy-util/artifactId version2.1/version scopesystem/scope systemPath${project.basedir}/lib/legacy-util-2.1.jar/systemPath /dependency注意systemscope 是 Maven 设计上明确标记为「不推荐用于生产」的机制。它绕过整个仓库解析链不参与依赖传递且mvn deploy时完全忽略——CI 构建必然失败。真正合规的引入方式只有三种✅ 方式一安装到本地仓库适合单机开发验证mvn install:install-file \ -Dfilelegacy-util-2.1.jar \ -DgroupIdcom.example \ -DartifactIdlegacy-util \ -Dversion2.1 \ -Dpackagingjar执行后jar 被按 GAV 规则写入~/.m2/repository/com/example/legacy-util/2.1/后续mvn compile会像引用任何远程依赖一样解析它。✅ 方式二部署到私有仓库Nexus/Artifactory团队标准流程mvn deploy:deploy-file \ -Dfilelegacy-util-2.1.jar \ -DgroupIdcom.example \ -DartifactIdlegacy-util \ -Dversion2.1 \ -Dpackagingjar \ -DrepositoryIdnexus-releases \ -Durlhttp://nexus.internal/repository/maven-releases/这要求settings.xml中已配置nexus-releases对应的认证信息。部署后所有团队成员只需声明坐标即可无需共享 jar 文件。✅ 方式三使用maven-install-plugin在构建生命周期内安装适合多模块项目中跨模块依赖在父 POM 的buildplugins中添加plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-install-plugin/artifactId version3.1.1/version executions execution idinstall-legacy-jar/id phaseinitialize/phase goalsgoalinstall-file/goal/goals configuration file${project.basedir}/lib/legacy-util-2.1.jar/file groupIdcom.example/groupId artifactIdlegacy-util/artifactId version2.1/version packagingjar/packaging /configuration /execution /executions /plugin这样每次mvn clean compile都会先将本地 jar 安装到本地仓库再进入编译阶段。它比手动install:install-file更可靠且可被 CI 复现。2.3 仓库镜像与settings.xml的优先级链为什么改了pom.xml的repositories还连不上私有源Maven 解析依赖时遵循严格的仓库优先级顺序项目 POM 中repositories声明的仓库仅对当前项目生效settings.xml中profiles激活的repositories用户级影响所有项目settings.xml中mirrors配置的镜像覆盖所有仓库请求最高优先级Maven 内置中央仓库Central兜底最典型的翻车场景你在pom.xml里加了公司 Nexus 地址repositories repository idcompany-nexus/id urlhttp://nexus.internal/repository/maven-public//url /repository /repositories但settings.xml里同时存在mirrors mirror idnexus-mirror/id mirrorOf*/mirrorOf urlhttps://repo1.maven.org/maven2//url /mirror /mirrors此时mirrorOf*/mirrorOf表示「所有仓库请求都走这个镜像」你的company-nexus请求会被强制重定向到repo1.maven.org自然 404。解决方法只有两个要么删掉mirrorOf*改为精确匹配如mirrorOfcompany-nexus/mirrorOf要么在settings.xml的 profile 中用repositories显式声明公司仓库并通过activeProfiles激活它——镜像不会覆盖 profile 中声明的仓库。3. 打可执行 JAR 的三种正统方案从 MANIFEST 到 ClassLoader 重写Maven 默认jar插件只打「原始 jar」仅含编译后的 class 文件和MANIFEST.MF无 Main-Class。要让它可执行必须注入启动元数据或合并依赖。主流方案有且仅有三个各自解决不同场景混用必踩坑。3.1maven-jar-pluginmaven-dependency-plugin分离式可执行适合依赖稳定、环境可控这是最轻量、最符合 Unix 哲学的方案主 jar 不含依赖依赖单独解压到lib/目录通过Class-Path指向它们。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration archive manifest addClasspathtrue/addClasspath classpathPrefixlib//classpathPrefix mainClasscom.example.Main/mainClass /manifest /archive /configuration /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-dependency-plugin/artifactId version3.6.0/version executions execution idcopy-dependencies/id phasepackage/phase goalsgoalcopy-dependencies/goal/goals configuration outputDirectory${project.build.directory}/lib/outputDirectory overWriteReleasesfalse/overWriteReleases overWriteSnapshotsfalse/overWriteSnapshots /configuration /execution /executions /plugin执行mvn package后target/下生成my-app-1.0.0.jar含MANIFEST.MF其中Class-Path: lib/guava-32.1.3-jre.jar lib/logback-classic-1.4.14.jar ...target/lib/目录含所有 runtime 依赖 jar机制本质JVM 启动时读取MANIFEST.MF的Class-Path将指定路径下的 jar 全部加入 classpath。maven-jar-plugin的addClasspathtrue/addClasspath会自动扫描target/lib/下的文件名并写入Class-Path行注意它不校验文件是否存在只拼字符串。提示classpathPrefix必须与maven-dependency-plugin的outputDirectory相对路径一致。若你把依赖拷到target/deps/这里就必须写deps/否则运行时java -jar找不到依赖。3.2maven-shade-pluginFat Jar全量合并适合分发给终端用户当用户环境无法保证依赖 jar 存在比如发给客户一个独立工具必须把所有 class 打进一个 jar。shade插件不是简单复制而是重写字节码中的类引用路径解决类冲突。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/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关键机制在transformersManifestResourceTransformer会在最终 jar 的META-INF/MANIFEST.MF中写入Main-Class而filters排除签名文件避免SecurityException。但更深层的是shade的类重定位Relocation能力。假设你依赖的guava和netty都用了com.google.common.base.Preconditions但版本不兼容。你可以将其中一个 relocaterelocations relocation patterncom.google.common/pattern shadedPatternshaded.com.google.common/shadedPattern /relocation /relocations此时shade会将guava-*.jar中所有com.google.common.*类复制到shaded/com/google/common/路径下重写所有引用这些类的字节码包括你自己的 class 和其他依赖的 class把new com.google.common.base.Preconditions()改成new shaded.com.google.common.base.Preconditions()。这就是为什么shade比assembly更安全——它不是文件级拼接而是 JVM 字节码级的语义重写。3.3spring-boot-maven-pluginSpring Boot 专用 Launcher适合 Spring 生态如果你的项目是 Spring Boot绝不要用shade或assembly打可执行 jar。Boot 的Launcher是一套完整的类加载隔离机制plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId version3.2.0/version executions execution goalsgoalrepackage/goal/goals /execution /executions /plugin它生成的 jar 结构特殊my-boot-app-1.0.0.jar ├── BOOT-INF/ │ ├── classes/ ← 你的代码 │ └── lib/ ← 所有依赖 jar不展开 ├── META-INF/ │ └── MANIFEST.MF ← Main-Class: org.springframework.boot.loader.JarLauncher └── org/ └── springframework/ └── boot/ └── loader/ ← Boot 自己的类加载器实现JarLauncher的核心逻辑是不使用 JVM 默认的AppClassLoader而是用LaunchedURLClassLoader加载BOOT-INF/classes和BOOT-INF/lib/*.jar。它通过MANIFEST.MF中的Start-Class你自己的主类触发启动并确保 Spring 的ApplicationContext能正确扫描BOOT-INF/classes下的Component。血泪经验若你强行用shade打 Spring Boot 项目SpringBootApplication会找不到spring.factories因为shade默认不合并同名资源文件。必须显式配置ServicesResourceTransformer否则启动直接ClassNotFoundException。4. 避坑五条真实踩过的 Maven 打包雷区这些不是文档里的警告而是某开发者在凌晨三点排查 CI 失败时记下的日志。4.1 现象mvn package成功但java -jar target/app.jar报ClassNotFoundException: com.google.gson.Gson原因你用了maven-jar-plugin的addClasspath但maven-dependency-plugin拷贝依赖的 phase 是package而maven-jar-plugin的jargoal 默认绑定在packagephase 的更早时刻Maven 生命周期中jar在package的早期执行dependency:copy-dependencies在晚期。结果MANIFEST.MF生成时target/lib/还是空的Class-Path里什么都没写。解决显式将maven-jar-plugin绑定到prepare-packagephase确保它在dependency:copy-dependencies之后执行plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version executions execution phaseprepare-package/phase !-- 关键 -- goalsgoaljar/goal/goals configuration.../configuration /execution /executions /plugin4.2 现象shade打出的 jar 运行时报NoSuchMethodError: org.slf4j.LoggerFactory.getLogger原因slf4j-api和slf4j-simple版本不匹配且shade默认不处理META-INF/services/org.slf4j.spi.SLF4JServiceProvider这类服务发现文件。多个依赖 jar 中的同名services文件被覆盖导致 SLF4J 找不到实现类。解决添加ServicesResourceTransformer它会合并所有META-INF/services/下的同名文件transformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/4.3 现象本地mvn clean package正常Jenkins 上构建失败报Could not find artifact com.example:legacy-util:jar:2.1原因你用了systemscope 引入本地 jar而 Jenkins slave 机器上根本不存在${project.basedir}/lib/legacy-util-2.1.jar。system依赖不参与远程仓库解析也不上传到 Nexus。解决立即废除systemscope改用maven-install-plugin在构建时安装见 2.2 方式三或推动该 jar 发布到私有仓库。4.4 现象spring-boot-maven-plugin打出的 jar用java -cp target/app.jar com.example.Main启动失败原因-cp强制使用 JVM 默认AppClassLoader它无法识别BOOT-INF/目录结构。Spring Boot 的 jar只能用java -jar启动因为-jar会触发java.util.jar.JarFile的特殊解析逻辑找到MANIFEST.MF中的Launcher类。解决永远用java -jar target/app.jar不要试图用-cp绕过 Launcher。4.5 现象maven-shade-plugin重定位后反射调用Class.forName(com.google.common.base.Preconditions)仍失败原因shade只重写编译期已知的字节码引用而Class.forName()是运行时动态加载字符串com.google.common.base.Preconditions不会被重写。shade无法修改字符串字面量。解决避免在重定位范围内使用硬编码类名的反射或改用ClassUtils.resolveClassName()Spring 提供它支持别名映射最彻底的是——不要重定位 JDK 或主流库只 relocate 冲突的第三方小众库。5. 设计取舍为什么 Maven 不默认打可执行 jar——从单一职责到构建可重现性Maven 的设计哲学第一条就是每个插件只做一件事并做到极致。maven-jar-plugin的唯一职责是「将编译输出打包成标准 jar 格式」而「可执行」涉及三个正交问题入口点声明MANIFEST、依赖管理classpath、类加载策略fat vs thin。把这些塞进一个插件只会让jargoal 变成黑匣子失去可组合性。我们来对比三种方案的取舍表维度maven-jar-plugindependencymaven-shade-pluginspring-boot-maven-plugin产物大小小主 jar 1MB依赖独立大所有 class 合并常 50MB大含 Boot Loader2MB启动速度快JVM 原生 classpath 加载慢需解压/内存映射所有 class中Launcher 需解析 BOOT-INF调试友好性高可单独替换target/lib/中某个 jar低所有 class 混在一起无法 hotswap中可 debugBOOT-INF/classes依赖冲突解决能力无靠 Maven 依赖调解失败则报错强relocation resource 合并强Boot 自定义 ClassLoader适用场景企业内网服务依赖由运维统一部署独立 CLI 工具发给客户一个 zipSpring Web 应用云原生部署我曾经负责一个跨部门的 SDK 项目初期用shade打包结果下游团队反馈“你们的 jar 里log4j-core版本把我们的log4j-api覆盖了日志全没了”。我们花了两天才定位到是shade的 relocation 规则没写全。从那以后我每次设计 SDK 发布流程都强制走三步用maven-jar-plugin打标准 jar确保符合 Java SE 规范用maven-source-plugin和maven-javadoc-plugin生成 source/javadoc jar让下游能 attach 源码用maven-deploy-plugin发布到 Nexus所有坐标可追溯mvn dependency:tree能精准定位冲突。可执行 jar那是下游应用的事。SDK 只提供原子能力不越界承担执行环境责任。Maven 的克制恰恰是它能在二十年后依然支撑百万级 Java 项目的底层原因——它不帮你做决定只给你清晰的契约和可验证的机制。希望帮到你。本文还有配套的精品资源点击获取