☰
从本地jar包到可执行jar:Maven仓库与打包插件避坑
2026/10/31 6:22:39 网站建设 项目流程

简介:面向 Java 开发者与 Maven 初学者的轻量实践资料,围绕仓库分类、本地 JAR 包引入以及可执行 JAR 打包三类常见需求,提供一个可直接查看的示例工程。压缩包共 6 个文件,约 5KB,主要由 pom.xml 构建描述、Java 源文件、Eclipse 工程配置(.project、.classpath)及 prefs 偏好设置组成,体量小巧但保留了项目结构与依赖配置的关键信息,便于在 IDE 中对照理解 Maven 工程的组成方式。已有 2770 人学习,适合用作课堂演示、个人练习或配置排查的参考样本。读者可从中观察 POM 组织、依赖声明和打包插件配置之间的关系,并据此梳理本地包接入与可执行包生成时的配置要点,减少在仓库定位、工程导入和构建参数设置上的试错成本。

1. 项目库里的 jar 拖进 lib 目录后,IDE 里能运行,为什么打包失败

A 同学接了个老项目,需要引入一个第三方的短信 SDK。他把 jar 拖进项目根目录的 lib 文件夹,在 IDE 里点绿三角跑得好好的,接口调通、日志也正常,提交前习惯性敲了一遍mvn clean package,java -jar起来直接NoClassDefFoundError。这不是什么玄学,是 Maven 仓库机制在提醒他:IDE 认的是 classpath,Maven 认的是仓库坐标,两套东西没对齐。Maven 仓库理解、本地包引入、可执行 jar 打包这三件事其实是一条链:仓库没弄清,本地包就引不明白;本地包引不明白,打出来的 jar 一定缺类。这套环境几乎每个 Java 项目都会碰到,尤其是要交付可执行 jar 给运维部署的场景。下面按我实际装过、翻过车的顺序,从 JDK 和 Maven 版本对齐开始,一路写到打出来的 jar 怎么验。

2. 环境准备:JDK 17 与 Maven 3.9.6 的版本对齐

装 Maven 最容易犯的错,是先装 Maven 再随便找个 JDK 配上。Maven 本身是个 Java 程序,它启动要用一个 JDK;它编译你的代码,用的又是maven-compiler-plugin指定的目标版本。这两个版本不一致时,报错往往出现在很靠后的打包阶段,前面编译一路绿灯,排查时容易被带偏。我的习惯是先确定项目要跑在哪个 JDK 上,再倒推 Maven 和插件版本,最后才动手装。

2.1 先定 JDK,再定 Maven 版本

把版本对应关系列清楚,比装完再试错省事得多。下面这张表是我在几个不同 JDK 项目里实际用过的组合,中间那列是 Maven 运行所需的最低 JDK,右边两列才是编译期的关键。

项目 JDKMaven 版本maven-compiler-plugin关键参数写法
83.8.8 / 3.9.63.8.1<source>1.8</source>
113.9.63.10.1<release>11</release>
173.9.63.11.0 及以上<release>17</release>
213.9.6 及以上3.13.0<release>21</release>

Maven 3.9.x 自身要求运行环境至少 JDK 8,这一条基本不会卡住人。真正会卡住人的是<source>/<target>和<release>的差别:前两个只改字节码版本,不改 bootclasspath,用 JDK 17 编译出target=1.8的包,仍然可能引用到高版本 API,在低版本 JVM 上运行时报NoSuchMethodError。<release>会连同 API 一起校验,所以 JDK 9 以后我一律用它。

2.2 装 Maven:解压后只改三个地方

Maven 没有安装程序,二进制包解压即用,麻烦的是环境变量和 Windows、Linux 两套写法。我一般统一放在一个固定目录,再做一个不带版本号的软链接,升级时只换链接,不动 PATH。

# 取二进制包,地址换成你手头能访问的下载源 export MAVEN_DIST="https://<你的下载源>/apache-maven-3.9.6-bin.tar.gz" curl -O "$MAVEN_DIST" # 解压到统一目录,软链接固定为 /opt/maven sudo tar -xzf apache-maven-3.9.6-bin.tar.gz -C /opt sudo ln -sfn /opt/apache-maven-3.9.6 /opt/maven # 三个环境变量:MAVEN_HOME、PATH、可选的 MAVEN_OPTS echo 'export MAVEN_HOME=/opt/maven' >> ~/.bashrc echo 'export PATH=$MAVEN_HOME/bin:$PATH' >> ~/.bashrc echo 'export MAVEN_OPTS="-Xms256m -Xmx1024m"' >> ~/.bashrc source ~/.bashrc mvn -v

MAVEN_HOME是给脚本和 IDE 找根目录用的,有些老脚本只认M2_HOME,两个都写上不冲突。PATH放在最前面,避免机器上预装的旧版 Maven 抢先命中,mvn -v输出的第一行必须是 3.9.6,第二行是Java version: 17.x,第三行Java home指向的路径要和JAVA_HOME一致。MAVEN_OPTS不是必须,但机器内存小于 8G 时建议给上,否则依赖多的大项目解析 POM 时容易卡死在内存回收上。Windows 下把解压目录写到D:\tools\apache-maven-3.9.6,MAVEN_HOME指过去,Path里追加%MAVEN_HOME%\bin,其余逻辑一样。

2.3 settings.xml:本地仓库路径和镜像先行

conf/settings.xml是全剧最容易改坏的文件,改完不生效往往是因为放错了位置。Maven 读取顺序是:命令行-s指定 > 用户目录~/.m2/settings.xml> 安装目录conf/settings.xml。我一般只改用户目录那份,安装目录保持原样,方便出问题时对照。

<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"> <!-- 本地仓库换到大盘,默认在 ~/.m2/repository,容易把系统盘撑爆 --> <localRepository>D:/maven-repo</localRepository> <mirrors> <!-- 镜像只拦 central,不要用 * 把私服也拦了 --> <mirror> <id>internal-mirror</id> <mirrorOf>central</mirrorOf> <url>https://maven.example.com/repository/public</url> </mirror> </mirrors> <profiles> <!-- 固定编译 JDK,避免机器上多个 JDK 时结果漂移 --> <profile> <id>jdk17</id> <activation> <activeByDefault>true</activeByDefault> <jdk>17</jdk> </activation> <properties> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </profile> </profiles> </settings>

<mirrorOf>central</mirrorOf>只代理中央仓库,公司私服的依赖仍然走后配置的<repositories>;写成*是最常见的翻车点,会让所有请求都打到镜像上,私服里的内部包瞬间全部找不到。<localRepository>建议放在空间充足的盘,一个中型项目一年下来攒几个 G 很正常,清缓存时直接删目录比逐个找包快得多。改完用mvn help:effective-settings确认localRepository和mirrors已经按预期生效,这一步只要几秒,比后面找不到包再回头查省事。

3. 仓库体系:local、私服、central 的查找顺序

很多「明明配了还是拉不到包」的问题,根源是没搞清 Maven 到底去哪儿找。它找依赖的顺序是固定的,但中间会插入缓存、镜像和私服几个变量,理解这条链路,后面引入本地包时才知道自己该往哪一层塞东西。

3.1 三类仓库和它们的查找顺序

一次mvn compile触发依赖解析时,Maven 走的是这么一条线:

  1. 先看本地仓库<localRepository>下有没有对应坐标目录,有就直接用,不联网;
  2. 没有就按settings.xml的镜像规则,请求镜像或私服;
  3. 镜像也没有,才会回到 POM 里<repositories>声明的地址;
  4. 全部落空,报Could not find artifact。

本地仓库就是磁盘上的一个目录结构,坐标直接映射成路径,比如com.example.sdk:sms-sdk:2.1.0对应com/example/sdk/sms-sdk/2.1.0/sms-sdk-2.1.0.jar。这个映射关系是后面手动塞包的基础,很多人install-file之后找不到文件,其实是去找了错误的目录。

私服是团队场景的中间层,Nexus 这类工具本质上就是一个带权限和缓存的仓库服务。它对外代理中央仓库,对内接收团队自己发布的包。判断要不要上私服很简单:有两个以上的人需要同一份内部 jar,就该传上去,而不是每人往本地仓库 install 一遍。

3.2 坐标、SNAPSHOT 与 updatePolicy

Maven 的坐标由groupId:artifactId:version[:packaging][:classifier]组成,前三个必填。SNAPSHOT后缀代表开发中的版本,本地仓库里它的目录名后面会带时间戳,比如sms-sdk-2.1.0-20240115.083012-3.jar,同时有一个maven-metadata-*.xml记录哪个是「最新」。

这就引出一个高频坑:同事在私服上推了新 SNAPSHOT,你这边mvn compile用的还是旧时间戳。原因是updatePolicy默认是daily,一天只检查一次。开发期我会在 POM 里显式声明:

<repositories> <repository> <id>internal-snapshots</id> <url>https://maven.example.com/repository/snapshots</url> <releases><enabled>false</enabled></releases> <snapshots> <enabled>true</enabled> <!-- 每次构建都问一次,开发期用;CI 上改成 never,让缓存生效 --> <updatePolicy>always</updatePolicy> </snapshots> </repository> </repositories>

release版本则相反,一个版本号一旦发布就不该被覆盖,本地缓存是永久的,想强制重拉只能删目录或加-U。带-U跑一次mvn -U clean package是排查「私服明明有、本地就是没有」的第一手段。

3.3 依赖调解三条规则

多引入一个 starter,传递依赖就可能撞版本。Maven 的调解规则按优先级是:

  • 路径最短优先:直接声明的一定赢过传递进来的;
  • 路径一样长,先声明优先:POM 里写在前面的<dependency>生效;
  • 想强行统一,用<dependencyManagement>覆盖,它的优先级高于前两条。
<dependencyManagement> <dependencies> <!-- 统一版本,子模块就不用各自写 version --> <dependency> <groupId>com.example</groupId> <artifactId>common-core</artifactId> <version>1.4.2</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>com.example</groupId> <artifactId>common-core</artifactId> <!-- 排除它带进来的旧日志实现,避免和现有绑定冲突 --> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> </exclusion> </exclusions> </dependency> </dependencies>

<exclusions>只排除依赖树上的一个节点,不影响其它路径引入的同名包,所以排除完一定要用mvn dependency:tree -Dverbose复查,-Dverbose会把被忽略的版本也打出来,能看清到底是谁把它带进来的。

3.4 用 dependency:tree 把冲突揪出来

排查依赖问题不要靠猜,直接把树打出来。

# 输出依赖树,重定向到文件方便搜索 mvn dependency:tree -Dverbose > tree.txt # 定位某个包是从哪条路径带进来的 mvn dependency:tree -Dincludes=org.slf4j:slf4j-api # 本地仓库出现诡异问题时的后悔药:清掉重拉 mvn dependency:purge-local-repository -DreResolve=true

第一条命令里,树中带omitted for conflict的就是被调解掉的版本,omitted for duplicate是重复声明。第二条的-Dincludes支持通配,格式是groupId:artifactId:version,只写前两段就能匹配全部版本。第三条慎用,它会把你本地仓库里对应项目的依赖全删一遍再重下,网络差的时候非常耗时,一般只在怀疑包损坏时用。

4. 引入本地 jar 包:install-file、systemPath 与私服三条路

现在回到开头那个场景。第三方给的 jar 没有坐标,Maven 不认识它,你有三条路可以走,但它们的可维护性差别很大。

4.1 推荐路线:install:install-file 装进本地仓库

这是最省事、也最不容易在打包时出问题的做法。本质是把裸 jar 加上坐标,塞进本地仓库对应的目录里,之后就像普通依赖一样声明。

mvn install:install-file \ -Dfile=lib/sms-sdk-2.1.0.jar \ -DgroupId=com.example.sdk \ -DartifactId=sms-sdk \ -Dversion=2.1.0 \ -Dpackaging=jar \ -DgeneratePom=true

-Dfile指向 jar 的实际路径,-DgeneratePom=true会在仓库里同时生成一个最小 POM,没有它某些插件解析依赖时会报缺少 POM。执行完去本地仓库核对一下目录:com/example/sdk/sms-sdk/2.1.0/下应该同时有 jar 和 pom 两个文件。装完在项目 POM 里正常写<dependency>就行,不需要额外声明<scope>system</scope>。

这条路的缺点是可复现性差:换台机器、换个 CI 环境,本地仓库是空的,构建立刻失败。所以它适合单机自测,不适合团队协作,除非在 README 里写清楚这条 install 命令。

4.2 项目内 lib 目录加 install-file 绑定生命周期

要让「克隆下来就能构建」,我一般把 jar 放进项目里的lib目录,然后把install-file绑到initialize阶段,让每次构建自动装一遍。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-install-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>install-sms-sdk</id> <!-- 绑到 initialize,比 package 早,依赖解析时包已经在仓库里 --> <phase>initialize</phase> <goals> <goal>install-file</goal> </goals> <configuration> <file>${project.basedir}/lib/sms-sdk-2.1.0.jar</file> <groupId>com.example.sdk</groupId> <artifactId>sms-sdk</artifactId> <version>2.1.0</version> <packaging>jar</packaging> <generatePom>true</generatePom> </configuration> </execution> </executions> </plugin>

绑到initialize而不是<dependencies>之前,是因为依赖解析发生在validate之后、compile之前,实际构建时install-file会先执行,顺序不会乱。如果项目是多模块,这条插件要放在引入该依赖的那个模块里,放在父 POM 会导致每个子模块都执行一遍,构建日志里刷一串相同输出,看着就烦。另一种更规范的写法是把这段放进一个local-libprofile,需要时用-Plocal-lib激活,CI 上不激活,走私服。

4.3 system scope 为什么看着能用、实际不能用

很多人第一次引入本地包就是抄的这段:

<dependency> <groupId>com.example.sdk</groupId> <artifactId>sms-sdk</artifactId> <version>2.1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/sms-sdk-2.1.0.jar</systemPath> </dependency>

在 IDE 里确实能跑,因为 IDE 直接把systemPath加进了 classpath,编译也不报错。但system作用域的依赖不参与传递,任何依赖了当前模块的下游项目都拿不到这个包;更致命的是打包时它不会被打进最终产物,mvn package出来的 jar 里根本没有这个类,运行就报NoClassDefFoundError。这也是开头 A 同学那个问题的直接原因。我的建议是把它当只读知识:看得懂别人代码里的写法,自己别用。

4.4 团队路线:deploy:deploy-file 推到私服

只要团队超过两个人,正确做法就是传私服,让所有人从同一个坐标拉取。命令和 install-file 几乎一样,只是目标是远端。

mvn deploy:deploy-file \ -Dfile=lib/sms-sdk-2.1.0.jar \ -DgroupId=com.example.sdk \ -DartifactId=sms-sdk \ -Dversion=2.1.0 \ -Dpackaging=jar \ -DgeneratePom=true \ -DrepositoryId=internal-releases \ -Durl=https://maven.example.com/repository/releases

-DrepositoryId要和settings.xml里<server>的<id>对上,账号密码写在<server>节点里,不要写在命令行的 URL 里,否则会进 shell history。推上去之后,项目 POM 里加上对应的<repository>,依赖声明恢复正常写法,本地仓库和 CI 都能拉到。这条路前期多花十分钟配权限,后面省下的是无数次「我这儿能跑啊」。

5. 打可执行 jar:四种插件路线怎么选

包引进来之后,剩下的是把它打进产物。Maven 本身不提供「可执行 jar」的概念,得靠插件拼,四种常见路线各有各的适用面,选错了排查起来很难受。

5.1 瘦 jar 加 lib 目录:jar-plugin 配 dependency-plugin

这是最接近运维部署习惯的一种:主 jar 里只有自己的类,依赖全部复制到lib目录,启动时用-cp或者Class-Path清单项指定。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.1</version> <configuration> <archive> <manifest> <!-- 声明入口类,否则 java -jar 报 no main manifest attribute --> <mainClass>com.example.app.Bootstrap</mainClass> <addClasspath>true</addClasspath> <!-- 清单里的 Class-Path 前缀,要和 copy-dependencies 的输出目录一致 --> <classpathPrefix>lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>3.6.1</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>

<classpathPrefix>lib/</classpathPrefix>决定 MANIFEST 里每一行长什么样,写错一个斜杠就找不到类。<includeScope>runtime</includeScope>只复制运行时需要的包,把test和provided排除掉,产物能小一圈。这种结构的好处是升级某个依赖只要换lib下对应 jar,不用重新打主包,适合交付给运维、由他们控制依赖版本的场景。

5.2 assembly:jar-with-dependencies 一把梭

想要一个文件扔过去就能跑,assembly 插件最直观,预置的jar-with-dependencies描述符直接可用。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.example.app.Bootstrap</mainClass> </manifest> </archive> <!-- 重命名去掉描述符后缀,方便脚本固定文件名 --> <finalName>${project.artifactId}-${project.version}</finalName> <appendAssemblyId>false</appendAssemblyId> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals><goal>single</goal></goals> </execution> </executions> </plugin>

它是把所有依赖的 class 直接解压合并进一个 jar。问题也在这里:多个依赖里同名路径的文件会互相覆盖,比如META-INF/spring.handlers、META-INF/services/*,后解压的赢。业务简单、依赖少的时候没问题,一旦引入框架,启动就会报找不到某个 handler。

5.3 shade:合并加重定位

shade 同样做合并,但给了 Transformer 来控制合并策略,还能重定位包名避免冲突。

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.3</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.example.app.Bootstrap</mainClass> </transformer> <!-- 把多个依赖里的 spring.handlers 追加合并,而不是覆盖 --> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <!-- 去掉依赖自带的签名文件,否则运行报 SecurityException --> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin>

ManifestResourceTransformer负责写Main-Class;AppendingTransformer处理的是那些需要「合并而不是覆盖」的索引文件,框架类项目基本都要加。签名文件必须排除,因为合并后的 jar 内容变了,原签名校验不过,启动直接抛SecurityException: Invalid signature file digest,这个报错信息不明显,第一次遇到能查半天。

5.4 微服务常用:repackage 打成可执行 jar

用 Spring Boot 的项目一般直接用它的spring-boot-maven-plugin,repackage目标会把原始 jar 和依赖一起重新包装,BOOT-INF/lib下放依赖,BOOT-INF/classes放自己的类,入口是框架的JarLauncher。

<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>3.2.5</version> <configuration> <!-- 排除只用于编译的依赖,别打进产物 --> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> <executions> <execution> <goals><goal>repackage</goal></goals> </execution> </executions> </plugin>

repackage会把maven-jar-plugin产出的原始 jar 改名成xxx.jar.original,正式的xxx.jar是重打包后的。排查启动问题时如果发现类路径对不上,先确认自己看的是哪个文件。另外<excludes>里的 lombok 只是编译期用,打进去纯属浪费体积,还会在某些环境下和注解处理器冲突。

四条路线的取舍可以简单记:单文件交付、依赖少,用 assembly;依赖里有框架资源需要合并,用 shade;要控制依赖版本、方便热替换,用瘦 jar 加 lib;本身是微服务,用 repackage。同一份 POM 里不要同时开多个打包插件,产物互相覆盖,谁最后执行谁生效,这种事故排查成本极高。

6. 避坑与排查:五条踩过的记录

下面这几条都是我在不同项目里真实遇到过的,按「现象、原因、解决」记,遇到了可以对着翻。

6.1 本地仓库明明有 jar,编译还是报 missing artifact

现象:install-file执行成功,目录里 jar 和 pom 都在,mvn compile依然报Could not resolve dependencies ... was not found。原因多半是仓库目录里残留了_remote.repositories或*.lastUpdated文件,Maven 会记录「这个包是从哪个仓库来的」,当来源和当前配置的仓库 ID 对不上时,它宁愿报错也不认缓存。

解决:进到对应版本目录,删掉_remote.repositories和*.lastUpdated,再跑一次mvn -U compile。命令行带上-llr(legacy local repository)也能绕过这套校验,但不建议长期用,会掩盖真正的仓库配置问题。

6.2 java -jar 报 no main manifest attribute

现象:包打出来了,java -jar直接报no main manifest attribute, in xxx.jar。原因是MANIFEST.MF里没有Main-Class,通常是打包插件没配<mainClass>,或者插件绑在了错误的阶段,package时压根没执行。

解决:先unzip -p xxx.jar META-INF/MANIFEST.MF把清单打出来看,确认有没有Main-Class。没有就回到 POM 补配置;如果有但依然报错,检查Main-Class的类名是不是全限定名、有没有写错包路径。多模块项目还要确认插件是配在产出 jar 的那个模块里,配在父 POM 的<pluginManagement>下不会自动执行。

6.3 打出来的 jar 只有几百 KB,启动缺类

现象:依赖一大堆,产物却小得离谱,启动时报ClassNotFoundException。原因基本锁定在system作用域,或者用了<scope>provided</scope>却没意识到容器不提供这些包。

解决:unzip -l xxx.jar | grep -c "\.class"数一下类数量,再和依赖树对照。把<scope>system</scope>全部改成正常坐标加install-file或私服;provided只留给真正由运行容器提供的依赖,比如 servlet-api。改完重新打包,jar 体积应该和依赖总量大致对得上。

6.4 加了框架依赖后启动报 handlers 找不到

现象:原来能跑的包,加了一个框架依赖后启动报某个命名空间解析失败。原因是 assembly 或 shade 合并时,META-INF/spring.handlers被后一个 jar 的同名文件覆盖,前一个框架的映射丢了。

解决:换 shade,并在<transformers>里给spring.handlers、spring.schemas各加一个AppendingTransformer,让它们追加而不是覆盖。加完重新打包,用unzip -p xxx.jar META-INF/spring.handlers确认内容是多段拼接的,而不是只有一段。

6.5 私服推了新 SNAPSHOT,本地始终拿旧的

现象:同事说修好了,你这边mvn clean package用的还是旧代码。原因是本地仓库里 SNAPSHOT 的时间戳没更新,updatePolicy是daily,当天已经检查过一次就不再问了。

解决:临时用mvn -U clean package强制更新;长期方案是在开发环境的 POM 里把该仓库的<updatePolicy>改成always,CI 上保持never或daily避免每次都请求远端。如果-U也不行,就去本地仓库把该 SNAPSHOT 目录整个删掉再拉,同时确认私服上的maven-metadata.xml里的时间戳确实是新的。

7. 验证:可执行 jar 交出去之前的三步自检

包打出来不算完,我现在的习惯是交付前固定跑三步,缺一步都可能在上线时翻车。这套检查不依赖任何额外工具,unzip和java就够了。

第一步看清单,确认入口和类路径写对了。

# 打印 MANIFEST,重点看 Main-Class 和 Class-Path 两行 unzip -p target/app-1.0.0.jar META-INF/MANIFEST.MF # 用框架插件打的包,入口是 JarLauncher,属于正常现象 # Main-Class: org.springframework.boot.loader.launch.JarLauncher # Start-Class: com.example.app.Bootstrap

第二步看内容,确认依赖真的进去了。

# 统计 class 数量,瘦 jar 和 fat jar 的数量级差很多 unzip -l target/app-1.0.0.jar | grep -c "\.class$" # 找某个关键依赖是否在包里(fat jar 看 BOOT-INF/lib 或根目录) unzip -l target/app-1.0.0.jar | grep "sms-sdk" # 瘦 jar 结构要单独确认 lib 目录被一起交付了 ls -lh target/lib | head

第三步换环境跑。本地能跑不代表别人机器能跑,我在本地验证时至少做两件事:把 jar 复制到一个和项目无关的临时目录再执行,排除「因为当前目录有配置文件所以能跑」的假象;用一个干净的基础镜像跑一次,确认没有隐式依赖系统里预装的东西。

检查项命令通过标准
入口类unzip -p xxx.jar META-INF/MANIFEST.MF有Main-Class,类名拼写正确
依赖完整unzip -l xxx.jar | grep -c "\.class$"数量与依赖规模相符,不异常偏小
本地包已入包unzip -l xxx.jar | grep sms-sdk能命中对应 jar 或 class
独立运行复制到空目录后java -jar xxx.jar启动日志正常,无NoClassDefFoundError
外部配置检查application.yml是否在包外配置走外部挂载,不打进 jar

最后一步跑起来之后,别急着关窗口,看几秒日志里有没有ClassNotFoundException的 warning,有些框架会吞掉异常降级启动,等真正调用到那个功能才炸。这套自检我一般写成脚本挂在package之后,多花二十秒,能省掉一次回滚。

从那以后我每次引入本地包,都强制走一遍「install-file、确认仓库目录、打包后 unzip 查一遍」这三步,再也没在交付现场遇到过NoClassDefFoundError。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询