简介:这是一份围绕 Maven 仓库机制、本地依赖引入与可执行 jar 打包的实战型示例工程,面向已具备 Java 基础、希望理清 Maven 依赖查找流程与打包配置的开发者。压缩包共 6 个文件,约 5KB,以 Eclipse 工程配置、首选项文件、XML 项目描述和 Java 源码为主,体量轻巧,便于直接导入 IDE 对照理解项目结构与构建配置。已有 2770 人学习,说明其在 Maven 入门与打包实践场景中具有一定参考价值。读者可结合包内工程骨架,梳理本地仓库、远程仓库与中央仓库的依赖来源,理解将本地 jar 安装到本地仓库并写入 pom 依赖的做法,同时对比 maven-jar-plugin、assembly 与 shade 等打包思路的配置差异,适合作为课堂示例或项目脚手架参考。
1. 从一次“本地包能编译却跑不起来”说起:Maven 仓库和可执行 jar 的安装边界
第一次把合作方给的本地 jar 塞进项目,mvn package一路绿灯,java -jar却只回一句no main manifest attribute,这种翻车在 Maven 项目里太常见。Maven 仓库、本地包引入和可执行 jar 三件事看起来分散,实际是一条链:仓库路径没统一,本地包坐标对不上;打包插件选错,主类清单又丢了。装 Maven 3.x 本身不复杂,难的是让依赖解析、install-file和 assembly/shade 的输出对得上。下面按我平时装环境的顺序走:先配 JDK 和settings.xml,再引本地包,最后分别用 assembly、shade、jar 插件打可执行包,并给出排查和验证命令。适合刚接手多模块项目、需要离线引入 SDK 的开发者。
2. 装 Maven 3.x 与配 settings.xml:本地仓库、中央仓库和私服的路径关系
Maven 装不上通常不是下载包的问题,而是JAVA_HOME和localRepository两处路径没对齐。我一般在干净环境里先确认 JDK,再解压 Maven 二进制包,最后改用户级settings.xml;IDE 自带的 Maven 只当补全工具,命令行构建统一走自己装的这一套。这样做的结果是 CI 和本机行为一致,本地包装到哪个仓库、打包时读哪个仓库,都有明确路径可查。
2.1 安装顺序与环境变量:JAVA_HOME 先于 MAVEN_HOME
先装 JDK,再解压 Maven。Maven 启动脚本会读JAVA_HOME,这个变量指到 JDK 根目录,不要指到bin,也不要指到 JRE 目录。Maven 解压到固定目录,例如 Linux 下的/opt/maven-3.x,Windows 下的D:\tools\maven,路径里不要带中文和空格。配置完执行mvn -v,输出里要同时看到 Java version 和 Maven home。
# Linux/macOS:JDK 与 Maven 解压到固定目录,避免桌面路径和中文路径 export JAVA_HOME=/opt/jdk-11 export MAVEN_HOME=/opt/maven-3.x export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH" # 写入 ~/.bashrc 或 ~/.zshrc 后重新加载 source ~/.bashrc # 验证:Java version、Maven home、Maven version 三行都要正常 mvn -vJAVA_HOME指向 JDK 根目录,MAVEN_HOME指向 Maven 解压根目录,PATH里把$JAVA_HOME/bin放在前面,避免系统里旧 JDK 抢先。Windows 下用setx JAVA_HOME "D:\tools\jdk-11"和setx MAVEN_HOME "D:\tools\maven",再把%JAVA_HOME%\bin、%MAVEN_HOME%\bin加进Path,改完重开终端。mvn -v报JAVA_HOME not found时,先echo $JAVA_HOME看变量是否为空,再看路径下有没有bin/java。
提示:同一台机器上装多个 JDK 时,不要让 IDE 的 JDK 和命令行 JDK 各走一套;打包插件读的是命令行
JAVA_HOME,版本不一致时容易出现 class 文件版本不匹配。
2.2 settings.xml 里本地仓库与镜像的写法
Maven 的settings.xml有两个常见位置:Maven 安装目录下的conf/settings.xml是全局配置,用户目录下的~/.m2/settings.xml是用户配置。我一般改用户级,避免升级 Maven 时被覆盖。localRepository建议指到独立盘或固定目录,不要留在默认的~/.m2/repository里,尤其是 Windows 用户目录容易遇到权限和磁盘空间问题。镜像配置要谨慎,mirrorOf写*会挡住所有仓库请求,私服没代理的仓库就会直接失败。
<!-- ~/.m2/settings.xml:本地仓库与私服镜像示例 --> <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd"> <localRepository>/data/maven-repo</localRepository> <mirrors> <mirror> <id>internal-nexus</id> <name>internal repository</name> <url>https://repo.internal/repository/maven-public/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors> </settings>localRepository是本地仓库绝对路径,install-file安装的包会落在这里,dependency:tree也按这个路径解析。mirrorOf写central只代理中央仓库;如果私服已经聚合了代理仓库,才考虑写*,否则不要一上来就全拦。url换成你自己的仓库地址,不要照抄示例。改完执行mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout,输出路径要和settings.xml里写的一致。
Maven 仓库类型可以先按下面这张表对齐:
| 仓库类型 | 作用 | 常见位置 | 排查入口 |
|---|---|---|---|
| 本地仓库 | 缓存下载的依赖和install-file安装的包 | settings.xml的localRepository | 看目录下是否有对应groupId/artifactId/version |
| 中央仓库 | 默认远程仓库 | Maven 内置 | 被镜像覆盖时看mirrorOf |
| 私服/远程仓库 | 团队内部依赖和代理缓存 | settings.xml的mirror或pom.xml的repositories | 看mvn -U是否仍报 404/401 |
| 多模块本地模块 | 同一反应堆内模块互相依赖 | 执行mvn install后进本地仓库 | 看版本号和SNAPSHOT时间戳 |
2.3 用 dependency:tree 看清依赖从哪来
依赖冲突和“本地包明明装了却找不到”,多半是坐标或仓库路径不一致。dependency:tree能把传递依赖展开,-Dverbose会显示被省略的冲突项。先跑树,再去本地仓库对应目录看 jar 和 pom 是否成对存在,比在 IDE 里反复刷新索引可靠。
# 查看依赖树,重点看本地包是否出现在预期层级 mvn dependency:tree -Dverbose # 只看某个 groupId 的依赖路径 mvn dependency:tree -Dincludes=com.demo.local:demo-sdk # 清理失败下载留下的 lastUpdated 标记后再拉取 find /data/maven-repo -name "*.lastUpdated" -delete mvn -U clean package-Dincludes的参数格式是groupId:artifactId,用来快速定位本地包被哪个模块引入。-U强制更新快照和失败缓存,适合私服刚上传新包但本地仍用旧索引的情况。lastUpdated文件是下载失败留下的标记,删掉后再跑,避免 Maven 认为“刚试过,不再重试”。这些命令不解决所有问题,但能先把“依赖到底从哪来”这个黑匣子打开。
3. 把本地 jar 引入 Maven 项目:install-file、system scope 与多模块 install 的取舍
本地包引入最容易犯的错,是把 jar 往lib目录一放,就在pom.xml里写systemPath。这样做 IDE 能编译,命令行打包却经常不带依赖,换一台机器路径又失效。更稳的做法是用install-file把 jar 装进本地仓库,让 Maven 按普通坐标解析;如果是团队共享包,再走deploy:deploy-file上传私服。多模块项目则优先用mvn install暴露本地模块,不要用外部文件路径模拟依赖。
3.1 用 install:install-file 安装到本地仓库
install-file做三件事:读取 jar、生成或读取 pom、复制到localRepository对应目录。groupId、artifactId、version三件套必须和后面pom.xml里的依赖完全一致,大小写和连字符都算数。-DgeneratePom=true会生成一个最小 pom,适合没有随包提供 pom 的第三方 SDK。
# 把 lib 下的本地 jar 安装到 settings.xml 指定的本地仓库 mvn install:install-file \ -Dfile=lib/demo-sdk-1.0.0.jar \ -DgroupId=com.demo.local \ -DartifactId=demo-sdk \ -Dversion=1.0.0 \ -Dpackaging=jar \ -DgeneratePom=true-Dfile是 jar 路径,相对路径按执行命令的目录算,建议在项目根目录执行。-DgroupId用倒置域名风格,团队内部包统一前缀,避免和中央仓库坐标撞车。-DartifactId和-Dversion要和依赖声明一致。-Dpackaging=jar告诉插件文件类型。安装完成后,去/data/maven-repo/com/demo/local/demo-sdk/1.0.0/下确认有demo-sdk-1.0.0.jar和demo-sdk-1.0.0.pom,再在pom.xml里写普通依赖:
<!-- 安装完成后按普通依赖引入,不要写 systemPath --> <dependency> <groupId>com.demo.local</groupId> <artifactId>demo-sdk</artifactId> <version>1.0.0</version> </dependency>这样依赖会参与编译、打包和传递,assembly、shade也能按 scope 把它打进可执行 jar。如果 jar 自带 pom,可以去掉-DgeneratePom=true,用-DpomFile=lib/demo-sdk-1.0.0.pom指定,避免生成的 pom 缺少传递依赖。装完后跑一次mvn dependency:tree -Dincludes=com.demo.local:demo-sdk,能看到它挂在哪个模块下,才算真正接进项目。
3.2 system scope 的边界与翻车点
systemscope 不是不能用,而是只能当临时验证。它把依赖指向本地文件系统路径,Maven 不负责下载和传递,打包插件默认也不会把它复制进可执行 jar。项目换目录、换机器、进 CI,systemPath很容易变成失效路径。
<!-- 仅用于临时验证:不要提交到主干,也不要用于 CI --> <dependency> <groupId>com.demo.local</groupId> <artifactId>demo-sdk</artifactId> <version>1.0.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/demo-sdk-1.0.0.jar</systemPath> </dependency>${project.basedir}是当前模块根目录,systemPath必须指向真实存在的 jar。问题在于:assembly的jar-with-dependencies默认不包含systemscope,shade也可能跳过;多模块里每个子模块都要能解析到这个相对路径,否则构建失败。正式做法是回到install-file,或者把包上传私服:
# 团队共享包:上传到私服,让所有构建机都能按坐标拉取 mvn deploy:deploy-file \ -Dfile=lib/demo-sdk-1.0.0.jar \ -DgroupId=com.demo.local \ -DartifactId=demo-sdk \ -Dversion=1.0.0 \ -Dpackaging=jar \ -DrepositoryId=internal-nexus \ -Durl=https://repo.internal/repository/maven-releases/-DrepositoryId要和settings.xml里server的id对上,认证信息放在settings.xml,不要写进命令历史。-Durl指向发布仓库,快照包用快照仓库地址。上传成功后,把pom.xml里的systemscope 和systemPath删掉,换成普通依赖,才算把临时方案收干净。
3.3 多模块项目用 mvn install 暴露本地模块
多模块项目里,模块 A 被模块 B 依赖,常见错误是 B 直接引 A 的target/classes或本地 jar。正确做法是让 Maven 反应堆处理:父pom.xml里声明modules,在根目录执行mvn install,A 的构件会进本地仓库,B 按坐标解析。只构建 B 时用-pl module-b -am,-am会连同依赖模块一起构建。
# 在父工程根目录执行:先安装依赖模块,再构建目标模块 mvn -pl module-b -am clean install -DskipTests # 只重新安装某个本地模块,再验证依赖树 mvn -pl module-a install -DskipTests mvn -pl module-b dependency:tree -Dincludes=com.demo.local:module-a-pl指定模块目录,-am表示同时构建依赖的上游模块,-DskipTests跳过测试但保留编译和打包。模块 A 的版本在pom.xml里写清楚,SNAPSHOT版本要确认本地仓库里是最新时间戳,必要时加-U。不要用systemPath指向兄弟模块的target目录,那是构建目录,clean一下就没了,血泪经验往往来自这种“本机能跑、CI 必挂”的写法。
4. 打可执行 jar 的三条路线:assembly、shade 与 manifest 配置
可执行 jar 不是只有一种打法。依赖少、结构简单的工具,用maven-assembly-plugin的jar-with-dependencies最快;依赖多、容易包冲突的项目,用maven-shade-plugin做重定位和资源合并;依赖体积大、希望外部放lib的,用maven-jar-plugin配Main-Class和Class-Path,再用maven-dependency-plugin复制依赖。三条路线不要同时开,否则target下会多出几个名字相近的 jar,运行错文件就会报主清单缺失。
4.1 maven-assembly-plugin 打 jar-with-dependencies
assembly 插件用预置描述符jar-with-dependencies把所有 runtime 依赖解压后合并进一个 jar,并在清单里写主类。它适合命令行小工具、内部脚本,配置短,执行mvn clean package就能在target下看到带-jar-with-dependencies后缀的文件。
<!-- pom.xml:assembly 插件配置,主类按你的启动类替换 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.6.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.demo.local.DemoApp</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>descriptorRef选jar-with-dependencies,插件会把依赖 jar 的内容合并进来。mainClass写全限定类名,package阶段触发single目标,产物在target/下。插件版本按你仓库里可用的版本替换,不要死抄。assembly 的合并规则是“先到先得”,同名资源文件可能只保留一个,META-INF/services下的服务声明容易丢;依赖里有多个META-INF/spring.factories或类似资源时,运行期可能缺 bean 或缺少 SPI 实现,这种问题用jar tf看资源,再决定换 shade。
# 构建并确认产物名称,不要运行成普通 jar mvn clean package ls -lh target/*jar-with-dependencies.jar java -jar target/demo-app-1.0.0-jar-with-dependencies.jarls先确认文件名,避免target下同时有普通 jar 和 assembly jar 时运行错。java -jar启动后看日志第一行是否有主类加载异常;报ClassNotFoundException时,先用jar tf检查依赖类是否在包里,再回看dependency:tree里对应依赖的 scope 是不是provided或test。
4.2 maven-shade-plugin 处理依赖冲突与资源合并
shade 插件同样做“胖 jar”,但它支持重定位包名、合并META-INF/services、过滤签名文件。依赖里出现同名类、不同版本 Jackson、不同日志实现时,shade 的比重定位能力比 assembly 稳。配置里ManifestResourceTransformer写主类,ServicesResourceTransformer合并服务文件,filters排除META-INF/*.SF等签名文件,避免运行期SecurityException。
<!-- pom.xml:shade 插件配置,适合依赖冲突较多的项目 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.0</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.demo.local.DemoApp</mainClass> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin>createDependencyReducedPom设为false可以避免 shade 改写pom.xml后影响其他插件。ManifestResourceTransformer负责主类清单,ServicesResourceTransformer把多个依赖的META-INF/services合并成一个,不配这个在 JDBC 驱动、日志门面场景容易出问题。filters排除签名文件,否则合并后的 jar 可能因为签名校验失败启动不了。执行mvn clean package后,shade 默认会替换主 artifact,产物通常是target/demo-app-1.0.0.jar,同时保留original-前缀的原始包。
# shade 后检查清单和服务文件 mvn clean package unzip -p target/demo-app-1.0.0.jar META-INF/MANIFEST.MF jar tf target/demo-app-1.0.0.jar | grep META-INF/services java -jar target/demo-app-1.0.0.jarunzip -p直接打印清单,看Main-Class是否存在。grep META-INF/services看服务文件是否被合并,而不是只剩一个。运行时报NoSuchMethodError时,多半是依赖冲突没有重定位,回看dependency:tree -Dverbose里同一 artifact 的多个版本,再决定是否加relocation。
4.3 maven-jar-plugin 配 Main-Class 和 Class-Path
依赖很多、jar 太大时,不必把所有依赖塞进一个文件。用maven-jar-plugin在清单里写Main-Class和Class-Path,用maven-dependency-plugin把 runtime 依赖复制到target/lib,运行时java -jar会按清单里的相对路径找依赖。这种方式适合需要外置配置、频繁替换依赖的场景。
<!-- pom.xml:薄 jar + lib 目录,Class-Path 指向 lib/ 下的依赖 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> <mainClass>com.demo.local.DemoApp</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>3.6.0</version> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> <includeScope>runtime</includeScope> </configuration> </execution> </executions> </plugin>addClasspath让 jar 插件把依赖写进Class-Path,classpathPrefix是依赖相对主 jar 的目录。copy-dependencies在package阶段把 runtime 依赖复制到target/lib,includeScope只收运行期需要的包。构建后target/demo-app-1.0.0.jar和target/lib/要一起分发,不能只拷主 jar。
# 构建薄 jar 与 lib 目录,运行时保持相对位置 mvn clean package java -jar target/demo-app-1.0.0.jar ls target/lib | headjava -jar在target目录下执行时,Class-Path里的lib/相对主 jar 所在目录解析;把 jar 拷到别的目录时,lib也要一起拷。ls target/lib看依赖是否复制成功,缺包时运行会报NoClassDefFoundError。这种打法的好处是依赖可替换、包体积小,代价是分发时多一个目录,脚本里要写清启动路径。
5. 避坑与排查:本地包、插件和可执行 jar 的翻车现场
Maven 的问题很少是“命令不会写”,更多是坐标、路径、scope 和插件阶段四件事没有对齐。下面这些坑我都在不同环境里遇到过,按“现象、原因、解决”拆开记,排查时按顺序对照,比盲目删本地仓库快。
5.1 本地包引入后的依赖解析问题
现象:install-file执行成功,pom.xml里也不报红,但mvn package编译时仍提示找不到本地包里的类。原因:安装时的groupId、artifactId、version和依赖声明不完全一致,或者install-file和构建用了两个不同的settings.xml,包被装到了另一个localRepository。解决:先跑mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout确认当前仓库路径,再去该路径下按groupId/artifactId/version找 jar 和 pom;坐标不一致就按依赖声明重新执行install-file,不要靠改目录名硬修。装完用mvn dependency:tree -Dincludes=com.demo.local:demo-sdk验证依赖是否进入当前模块。
现象:多模块项目里子模块升级了本地包版本,命令行构建仍用旧类,IDE 里却看起来正常。原因:本地仓库里同版本SNAPSHOT没有更新,或者子模块依赖的是旧版本号,IDE 用模块 classpath 绕过了仓库。解决:执行mvn -U clean install强制更新快照,确认pom.xml里的版本号一致;必要时删除本地仓库中对应版本的目录再重装,但删之前先确认没有其他项目依赖这个版本。排查时以命令行dependency:tree为准,不要只看 IDE 的索引状态。
现象:本地包在compile阶段可用,打包后运行却报ClassNotFoundException。原因:依赖被写成了systemscope,或者providedscope,assembly/shade 默认不把它打进可执行 jar。解决:把systemscope 改成普通依赖并用install-file安装;如果是容器提供的包,确认运行时容器里确实有,否则改成runtime并重新打包。用jar tf检查产物里是否包含本地包的类,没有就回到 scope 配置。
5.2 打包插件的配置冲突
现象:java -jar报no main manifest attribute。原因:运行的是普通 jar,不是 assembly/shade 产物;或者maven-jar-plugin没有配mainClass,清单里没有Main-Class。解决:先ls target/*.jar确认文件名,运行带-jar-with-dependencies或 shade 替换后的主包;再用unzip -p target/xxx.jar META-INF/MANIFEST.MF看清单。缺主类就在对应插件的archive/manifest/mainClass里补全限定类名,重新mvn clean package。
现象:assembly 打出的包启动时报ClassNotFoundException,但dependency:tree里明明有该依赖。原因:依赖 scope 是provided或test,assembly 的jar-with-dependencies只合并 runtime 范围;也可能是依赖被optional标记,没有传递进来。解决:检查pom.xml中该依赖的 scope,改成compile或runtime;optional依赖需要显式声明。改完重新打包,用jar tf搜类名确认进入产物。
现象:shade 后的 jar 启动报签名校验异常,堆栈里出现SecurityException或Invalid signature file digest。原因:依赖 jar 自带META-INF/*.SF、*.DSA、*.RSA签名文件,合并后签名和内容不一致。解决:在 shade 的filters里排除这些文件,配置如第 4 章所示;排除后重新打包。不要手动解压删文件再压回去,那样容易破坏清单和目录结构。
现象:assembly 和 shade 同时配置后,target下出现多个 jar,运行其中一个能启动,另一个报错。原因:两个插件都在package阶段改写主 artifact,产物名称和内容互相覆盖,执行顺序不同结果不同。解决:一个项目只保留一种主打包方式,需要对比时用 profile 隔离,不要在同一套build/plugins里同时开。构建后先看target文件列表,再决定运行哪个。
5.3 运行期与仓库环境问题
现象:依赖下载卡住,最后报Could not resolve dependencies,本地仓库里出现大量.lastUpdated文件。原因:镜像地址不可达、私服没有代理该仓库,或者之前失败后 Maven 缓存了失败标记。解决:先mvn -U clean package强制更新;仍失败就检查settings.xml的mirrorOf是否把需要的仓库也拦了,临时把mirrorOf收窄到central再试。删.lastUpdated只作为清理手段,根因还是仓库地址或网络权限。
现象:本地包在命令行能跑,换到另一台机器或 CI 就找不到。原因:包只装在当前用户的localRepository,没有上传私服;CI 用了另一份settings.xml,本地仓库路径不同。解决:团队共享包走deploy:deploy-file上传私服,项目里写普通依赖;CI 的settings.xml指向同一私服,构建前不要依赖“本机已经装过”。临时离线环境可以把包和安装脚本一起放进项目tools/目录,构建前执行install-file,但要把坐标写死并纳入版本管理。
现象:可执行 jar 启动后读不到src/main/resources下的配置文件。原因:资源没有被打进 jar,或者 assembly/shade 合并时同名资源被覆盖;也可能是代码用绝对文件路径读取,而不是 classpath。解决:用jar tf看BOOT-INF或根目录下是否有配置文件;代码改用getResourceAsStream从 classpath 读。shade 场景加ServicesResourceTransformer只解决服务文件,普通同名资源仍要避免重复,必要时给资源加前缀目录。
6. 装完怎么验证:从 mvn -v 到 java -jar 的完整走查
装完 Maven、引完本地包、打完可执行 jar,不要急着交付。我一般按固定顺序走一遍:先确认 Maven 和本地仓库路径,再看依赖树里本地包是否在正确模块下,最后在干净目录用java -jar启动。下面这组命令可以直接抄,输出对得上再继续。
# 1. 确认 Maven、JDK 和本地仓库路径 mvn -v mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout # 2. 确认本地包进入依赖树 mvn dependency:tree -Dincludes=com.demo.local:demo-sdk -Dverbose # 3. 构建可执行 jar,按你选的插件确认产物名 mvn clean package ls -lh target/*.jar # 4. 看清单主类和服务文件 unzip -p target/demo-app-1.0.0-jar-with-dependencies.jar META-INF/MANIFEST.MF jar tf target/demo-app-1.0.0-jar-with-dependencies.jar | grep -E "DemoApp|META-INF/services" # 5. 干净目录启动,避免当前目录的 lib 或配置文件干扰 mkdir -p /tmp/demo-run && cp target/demo-app-1.0.0-jar-with-dependencies.jar /tmp/demo-run/ cd /tmp/demo-run && java -jar demo-app-1.0.0-jar-with-dependencies.jarmvn -v看 Java version 和 Maven home 是否与预期一致;help:evaluate的输出要和settings.xml的localRepository相同。dependency:tree -Dincludes能确认本地包没有被systemscope 截断。unzip -p看Main-Class,jar tf看启动类和META-INF/services是否在包里。最后复制到/tmp/demo-run启动,是为了排除“在target目录下能跑、换目录就找不到依赖”的假通过。
| 检查点 | 命令 | 通过标准 |
|---|---|---|
| Maven 可用 | mvn -v | 显示 Maven home 和 Java version |
| 本地仓库一致 | mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout | 路径与settings.xml一致 |
| 本地包已解析 | mvn dependency:tree -Dincludes=com.demo.local:demo-sdk | 出现在预期模块,scope 为 compile/runtime |
| 主类清单存在 | unzip -p target/xxx.jar META-INF/MANIFEST.MF | 有Main-Class且类名正确 |
| 独立运行 | cd /tmp/demo-run && java -jar xxx.jar | 能启动,不报NoClassDefFoundError |
如果第 5 步在干净目录失败、在项目目录成功,优先查Class-Path相对路径和外部配置文件,不要先怀疑代码。薄 jar 方案要把lib/一起复制;assembly/shade 方案要确认依赖 scope 不是provided。我现在的习惯是:每次引入本地包,先把groupId、artifactId、version写在一张便签上,跑完install-file立刻用dependency:tree对一遍,打包后必须到空目录java -jar验一次,不给自己留“本机能跑”的后悔药。上面这些命令和 pom 片段我收在一个安装包里,取用后先按你的 JDK、Maven 路径和启动类改三处,再跑完整走查。希望帮到你。
本文还有配套的精品资源,点击获取