IDEA+Maven 打 Jar 包:两种方式与常见报错排查
2026/9/18 15:34:43 网站建设 项目流程

上周同事老张卡在一个再普通不过的需求上——把手里那个写了三个月的 Maven 项目打成 jar 包发给运维部署。结果他折腾了一下午,先是mvn package之后双击跑不起来,提示no main manifest attribute;换了个方式重新打,又报ClassNotFoundException,一路怀疑人生。其实这事说穿了就那么点门道,IDEA 配合 Maven 打 jar 包,往大了说有一堆插件组合,往小了说日常真正好用的就两种路子。这篇文章我想把这两种方式掰开揉碎讲清楚,包括它们各自的适用场景、坑在哪、验证怎么做,以及我这些年踩出来的实操经验。不管你是刚接触 Maven 打包的新手,还是平时只用 IDE 一键打包、没深究过原理的老手,看完都能对着自己的项目直接抄作业。

1. 先搞清楚 Maven 打包到底在打什么

很多人用 IDEA 用了很久,Maven 面板里的cleanpackageinstall点了无数次,但问起来「打出来的 jar 里到底有啥」,往往答不上来。这个问题不搞明白,后面选哪种打包方式就是瞎蒙。

1.1 一个 jar 包里通常装着哪几样东西

jar 本质上就是个 zip 压缩包,只不过换了个扩展名,并且约定了一个META-INF/MANIFEST.MF文件来记录元信息。一个普通 Maven 项目执行mvn package之后,产物大致长这样:

myapp-1.0.0.jar ├── META-INF/ │ └── MANIFEST.MF # 元信息,记录主类、版本、Class-Path 等 ├── com/example/ # 你项目自己的编译产物(.class) ├── application.yml # src/main/resources 下的资源文件 └── ...

注意这里的关键点:默认打包只会把你的src/main/java编译出的 class 和src/main/resources里的资源塞进去,不会把任何第三方依赖打进去。也就是说,如果你的项目用了 Spring、Jackson、MySQL 驱动,这些依赖的 class 全都不在 jar 里,它们只是躺在本地仓库里被记录在依赖清单上而已。

这就是为什么很多人第一次打包后运行会报ClassNotFoundException: org.springframework.boot.SpringApplication之类的错误——jar 里根本没有那个类。理解了这一点,你就明白所谓「两种打包方式」的分野在哪了:一种是不带依赖的精简包,一种是连依赖一起塞进去的可执行包。

1.2 两种打包方式的真正区别

日常在 IDEA 里给 Maven 项目打 jar,我用得最多的两条路子:

第一种,走 Maven 生命周期 + IDEA 图形界面(或命令行)直接 package。它依赖的是项目里配置好的打包插件,产物形态完全由pom.xml里的<build>决定。这是最通用、最基础的方式,学习成本最低。

第二种,配置专门的打包插件(spring-boot-maven-plugin/maven-shade-plugin/maven-assembly-plugin)来产出可执行 fat jar。这种方式会把依赖一并打进去,产物体积大,但可以做到java -jar直接跑,适合 Spring Boot 项目和需要独立部署的场景。

说它们是「两种方式」,其实更准确的说法是:第一种是「调用打包这件事」,第二种是「决定打包这件事的产物长什么样」。两者并不冲突,第一种方式里你完全可以用第二种插件。之所以还是一分为二来讲,是因为新手最容易混淆的就是这两层概念,把它俩捋清楚,后面所有操作都是顺水推舟。

1.3 场景对照:你该选哪条路

项目类型运行环境推荐方式产物特征
Spring Boot 应用独立服务器、Docker方式二,spring-boot 插件 repackage一个可执行 jar,内含依赖
传统 Java SE 工具类命令行调用方式二,shade/assembly 插件fat jar,java -jar可跑
被其他项目引用的公共库作为依赖被引入方式一,默认 package精简 jar,不含依赖
部署到已有容器(Tomcat)的 Web 应用外部容器方式一,打成 war 或瘦 jar依赖由容器提供

这张表建议存一下。很多人打包出错,根源不是命令敲错了,而是项目类型和打包方式根本不匹配——拿一个要被别的项目当依赖引用的公共库去打成 fat jar,结果依赖冲突满天飞;又或者拿一个 Spring Boot 应用按默认方式打包,运行时直接找不到主类。

1.4 打之前先确认动手的三件事

在点任何按钮之前,我习惯先做三个检查,能省掉一大半回滚重来的时间:

  • 确认 JDK 版本和项目编译版本一致。IDEA 里Project Structure的 SDK、pom.xmlmaven-compiler-pluginsource/target、以及java -version,三者必须对得上。用 JDK 17 编译、用 JDK 8 跑,报UnsupportedClassVersionError几乎是必然的。
  • 确认 pom 里有没有打包插件。翻到<build>节点看一眼,如果没有spring-boot-maven-plugin或类似插件,默认打出来的就是精简包,别指望java -jar能跑。
  • 确认 resources 目录下的配置有没有被过滤掉application.ymllogback.xml这些如果写在了src/main/java里,是打不进去的,必须放在src/main/resources

注意:IDEA 的图形界面打包和命令行mvn package走的是完全一样的流程,都读同一份pom.xml。所以命令行报的错,图形界面一样会报;别指望换个入口能绕过问题。

2. 方式一:IDEA 图形界面配合 Maven 生命周期打包

这是最省事的一条路,也是大多数人日常用的。核心动作就三下:打开 Maven 面板、双击clean、双击package。但要把这条路走顺,前置配置得先补齐。

2.1 前置配置:pom.xml 里必须交代清楚的几块

先看一份最基础的pom.xml骨架,重点看<build>部分:

<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>myapp</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- 你的依赖 --> </dependencies> <build> <finalName>myapp</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>

几个点值得单独拎出来说:

<packaging>必须是jar。如果你是从老项目复制过来的,可能残留着war,那打出来的就是 war 包,package阶段行为完全不一样。

<finalName>决定产物的名字。不写的话默认是artifactId-version.jar,例如myapp-1.0.0.jar。我一般会显式写成myapp,因为后面写部署脚本、Dockerfile 的时候,固定名字省事得多。

project.build.sourceEncoding一定要设成 UTF-8。这个属性不设置,Maven 会拿系统默认编码去处理资源文件,Windows 上默认 GBK,稍不注意打出来的application.yml里中文就是乱码。这是我在项目里被坑过好几次的地方。

2.2 在 IDEA 右侧 Maven 面板里一步步点下去

配置好了之后,操作本身很简单。打开 IDEA 右侧边栏的 Maven 面板(没有的话,菜单View → Tool Windows → Maven),展开你的项目节点,找到Lifecycle这一栏,里面按顺序列着validatecompiletestpackageverifyinstalldeploy等若干条目。

第一步,双击clean。它的作用是删掉target目录,把上一次的编译产物清干净。别小看这一步,我见过太多「明明改了代码,打出来的 jar 还是旧的」的案例,十有八九是没 clean,target目录里残留着上次的 class。

第二步,双击package。它会按顺序执行compile → test → package,也就是先编译主代码,再跑单元测试,最后打包。如果你项目里的单元测试比较重或者恰好有关系数据库、外部服务的依赖,这一步可能会卡很久。

第三步,去target目录看产物。打包成功的话,控制台会输出类似:

[INFO] Building jar: /path/to/project/target/myapp.jar [INFO] BUILD SUCCESS

这时候target目录下应该能看到myapp.jar,以及一个classes目录(编译后的 class)。classes是你项目自己的 class 存放处,也是被打进 jar 的原始材料。

2.3 命令行等价操作与几个省时间的参数

上面的双击操作,等价于在项目根目录执行:

mvn clean package

在 IDEA 底部的 Terminal 里直接敲这条命令,效果完全一样。命令行有个好处是可以带参数,这对日常提速很有用:

# 跳过测试执行,但编译测试代码 mvn clean package -DskipTests # 完全跳过测试的编译和执行,速度最快 mvn clean package -Dmaven.test.skip=true

-DskipTests-Dmaven.test.skip=true的区别值得说一下。前者只是不运行测试方法,但测试代码照样编译,如果测试代码本身有编译错误,还是会失败;后者连测试代码都不编译,彻底跳过。日常本地打包图快,我用后者;但在 CI 上我会保留测试,用前者。

还有个常用的参数是-o,代表离线模式。本地仓库里依赖都全的情况下,加上它能让构建不去远程仓库检查更新,速度肉眼可见地快,尤其是网络不太稳定的时候。缺点是如果本地缺依赖,会直接报错而不是去下载。

mvn clean package -o -Dmaven.test.skip=true

最后加一个-U的说明,它的作用相反,是强制去远程仓库检查快照版本更新。团队协作时你的项目依赖了别人正在开发的 SNAPSHOT 包,拉不到最新代码时加上它往往能解决。

2.4 产物的验证:别打完就发走

jar 打出来别急着交给运维,先自己验一遍。我一般会用三条命令查:

# 看 jar 里的目录结构 jar tf target/myapp.jar # 看 MANIFEST.MF 内容,重点看有没有 Main-Class unzip -p target/myapp.jar META-INF/MANIFEST.MF # 直接尝试运行 java -jar target/myapp.jar

第一条命令会列出 jar 内所有条目。如果里面只有META-INF/com/yourpackage/加上少量配置文件,说明这是精简包,依赖没进去,那java -jar基本跑不起来——除非你之前特意在 MANIFEST 里配了Class-Path并保证依赖都在正确位置。

第二条命令查看清单文件,正常应该能看到Manifest-VersionCreated-ByBuild-Jdk这些。如果是个可以独立运行的应用,还要有Main-Class: com.example.MyApp

第三条是最直接的验证。报no main manifest attribute,说明没配主类;报ClassNotFoundException,说明依赖没进去或者主类名写错了。这两种错误后面会专门讲怎么排查。

实操心得:我习惯在项目根目录放一个verify.sh,把上面这三条命令串起来,每次打包完跑一遍。看起来是小事,但能挡住很多「本地能跑、服务器上起不来」的低级问题。

3. 方式二:用打包插件产出可以直接运行的 jar

如果你的目标就是打出一个java -jar能跑的应用包,那前面那种默认打包是不够的,必须靠插件把依赖一起塞进去。这里按项目类型分三条线来说。

3.1 Spring Boot 项目:spring-boot-maven-plugin 的标准姿势

Spring Boot 项目打可执行 jar 是最常见的需求,官方插件spring-boot-maven-plugin就是为这个场景设计的。在pom.xml里加上:

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>3.2.0</version> <configuration> <mainClass>com.example.MyApplication</mainClass> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

这个插件默认绑定到package生命周期阶段,执行repackage目标。它的工作方式是先把项目打成普通 jar,再把它重新打包成可执行 jar。所以你会看到一个有意思的现象:target目录下会有两个 jar,一个是myapp.jar(原始精简包,也可能是.original后缀),一个是被 repackage 后覆盖的最终产物。

<mainClass>一般可以不写,插件会自动去找带@SpringBootApplication注解的类。但如果项目里有多个主类(比如还写了命令行工具),自动识别就会犯迷糊,这时候显式指定最保险。

<excludes>里的lombok是个典型操作。Lombok 只在编译期起作用,运行时不需要,打进去纯属占空间。这种编译期依赖都建议排除。

打包命令和前面一样:

mvn clean package -Dmaven.test.skip=true

打完之后用jar tf看一眼结构,会看到明显的不同:

BOOT-INF/ ├── classes/ # 你的项目 class ├── lib/ # 所有第三方依赖 jar META-INF/ ├── MANIFEST.MF org/springframework/boot/loader/ # Spring Boot 自己的类加载器

BOOT-INF/lib里躺着所有依赖,MANIFEST.MF里的Main-Class指向org.springframework.boot.loader.JarLauncher,真正的业务主类则记录在Start-Class属性里。这是 Spring Boot 特有的结构,JarLauncher负责用自定义类加载器把BOOT-INF/classesBOOT-INF/lib加载起来,本质上是个「jar 中 jar」的启动器。

3.2 普通 Java 项目:shade 和 assembly 插件怎么选

不是 Spring Boot 的老项目,想打 fat jar,可以用maven-shade-pluginmaven-assembly-plugin。两者能力有重叠,但侧重点不同。

maven-assembly-pluginjar-with-dependencies描述符最省事,配置简单:

<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.example.Main</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>

它的工作方式是暴力解压所有依赖的 jar,把 class 全展开再重新压成一个 jar。弊端也在这:不同依赖里如果有同名的文件,会直接覆盖,且不一定会警告META-INF/services下的 SPI 配置、spring.factories这类文件被覆盖,就是各种「诡异失效」的根源。

maven-shade-plugin在这点上升级了,它支持ServicesResourceTransformer来合并 SPI 文件,还支持类重定位(relocation)解决包冲突:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.Main</mainClass> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> </configuration> </execution> </executions> </plugin>

我的选择习惯是:新项目、依赖复杂、有 SPI 机制的,用 shade;老项目、依赖简单、快速出包,assembly 的jar-with-dependencies够用。两者打出来的都是平铺结构的 fat jar,用java -jar直接跑。

3.3 指定 Main-Class 的几种写法与区别

主类配置是打包环节最容易出错的地方,不同插件的写法不一样,这里统一对照一下:

插件配置位置写法
spring-boot-maven-plugin<configuration><mainClass>com.example.Main</mainClass>
maven-assembly-plugin<archive><manifest><mainClass>com.example.Main</mainClass>
maven-shade-pluginManifestResourceTransformer<mainClass>com.example.Main</mainClass>
maven-jar-plugin<archive><manifest><mainClass>com.example.Main</mainClass>

看起来都是mainClass,但位置差之毫厘谬以千里。放进<configuration>顶层是 Spring Boot 插件专属;放进<archive><manifest>是标准 maven-archiver 的写法,绝大多数插件通用。

还有一个细节:主类名必须是带完整包名的全限定名,不能只写Main。写错的后果是运行时Could not find or load main class。我见过的错误写法里,这个排第一。

3.4 依赖外置:让产物瘦下来

fat jar 用起来爽,但体积是个大问题。一个中等规模的 Spring Boot 应用,打包后动辄七八十兆,光依赖就占了九成。如果你的部署环境需要频繁更新代码,每次上传这么大的包很痛苦。

spring-boot-maven-plugin支持把依赖外置。做法是配置layoutZIP并配合maven-dependency-plugin把依赖复制到外部目录:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin>

打完之后,target/lib里是所有依赖,主 jar 只有几兆。运行时:

java -cp "myapp.jar:lib/*" com.example.Main

Windows 下分隔符是分号,"myapp.jar;lib/*"。这种瘦包方案在容器化部署里很常见,因为 Docker 分层缓存能让依赖层复用,代码变一次只推几兆。

4. 打包报错排查实录与常见坑

打包这事,成功的路径只有一条,报错的方式五花八门。下面这些是我这些年遇到频率最高的几个,整理成速查表放在前面,后面逐条展开。

报错信息大概率原因处理方向
no main manifest attributeMANIFEST 里没配 Main-Class补配置,或确认插件执行了
ClassNotFoundException依赖没打进去改用 fat jar 插件
UnsupportedClassVersionError编译和运行 JDK 版本不一致统一 JDK 版本
Invalid or corrupt jarfilejar 传输损坏或用了 oss 后缀重传,核对 MD5
打包时中文乱码资源编码未指定 UTF-8配 sourceEncoding
BUILD FAILURE 找不到依赖私服/镜像仓库配置问题检查 settings.xml

4.1 no main manifest attribute 到底怎么回事

这个报错几乎是新手第一坑。原因就一个:jar 的 MANIFEST.MF 里没有Main-Class这一行。默认的maven-jar-plugin不会自动写主类,除非你显式配置:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.1</version> <configuration> <archive> <manifest> <mainClass>com.example.Main</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin>

addClasspathclasspathPrefix这两个参数配合起来,会在 MANIFEST 里生成一行Class-Path: lib/xxx.jar lib/yyy.jar,告诉 JVM 去lib/目录找依赖。这是一种「主 jar + 平级 lib 目录」的经典布局,和前面讲的依赖外置思路类似,只是实现路径不同。

排查这条错误的具体步骤:先用unzip -p target/myapp.jar META-INF/MANIFEST.MF看内容,确认有没有Main-Class;如果没有,看你的pom.xml里有没有配。注意 IDEA 里有时会缓存 MANIFEST 配置,改了 pom 之后建议mvn clean再 package,不然改动可能不生效。

4.2 ClassNotFoundException 与 NoClassDefFoundError 的排查

这两个错误长得很像,但含义不同。ClassNotFoundException是主动加载某个类时找不到,比如Class.forName()main方法入口类缺失;NoClassDefFoundError是编译时存在、运行时找不到,通常发生在某个类的静态初始化之后。

在打包场景下,这两个错误的共同根源基本都是依赖缺失。排查方法:

# 看 jar 里到底有没有那些依赖的 class jar tf target/myapp.jar | grep org/springframework/boot/SpringApplication

如果 grep 不出来,那依赖确实没进去。这时候就要回头看你的打包插件配置。常见疏漏有三个:插件没绑定到package阶段(<executions>没写)、<scope>provided</scope>用多了(provided 的依赖不会进包)、<excludes>把不该排除的排除了。

特别注意provided这个 scope。它的语义是「编译和测试时需要,打包和运行时由容器提供」。本地跑得好好的,一打成 fat jar 就少类,八成是某个依赖被标了 provided。开发阶段想省事直接用它,部署时就要小心。

4.3 编码、时区与配置文件被漏打

这几类问题不会报错,但会在运行时以奇怪的方式暴露出来,排查起来最费劲。

中文乱码。前面提过,源头是资源编码未指定。除了设project.build.sourceEncoding,还要注意 IDEA 里Settings → Editor → File Encodings的全局编码,三个地方:Global Encoding、Project Encoding、Default encoding for properties files,全部设 UTF-8。properties 文件历史上默认按 ISO-8859-1 处理,虽然现代 Maven 能覆盖,但设成 UTF-8 更保险。

时区问题。容器里通常默认 UTC,容器外可能是东八区,写日志时间对不上会影响排查。打运行时包的时候,我习惯加一个 JVM 参数启动参数-Duser.timezone=Asia/Shanghai,或者在 Dockerfile 里设TZ环境变量。

配置文件漏打application.yml放在src/main/java下,是打不进去的。Maven 只认识src/main/resources。这个坑我中过一次,本地 IDEA 里跑得好好的(IDE 把 java 目录下的资源也算进 classpath 了),打包后配置全丢,找了好久才发现文件位置不对。

4.4 仓库配置引发的构建失败

热词里频繁出现「maven 配置阿里云仓库」,说明这是很多人踩过的地方。默认的中央仓库在国内访问速度感人,构建经常超时。在~/.m2/settings.xml里的<mirrors>节点加一段:

<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>

<mirrorOf>central</mirrorOf>表示接管中央仓库。如果公司还有私服,写成<mirrorOf>*,!my-private</mirrorOf>这种形式,把私服 id 排除出去。

配置完之后,如果某个依赖还是拉不下来,先清掉本地仓库对应目录再重试:

mvn clean package -U

-U强制更新 SNAPSHOT,对发布版一般无效,但对 SNAPSHOT 依赖有效。另外注意本地仓库里经常会有xxx.jar.lastUpdated这种文件,它们是上次下载失败的标记,网络恢复后如果不带-U,Maven 可能直接跳过重试。稳妥做法是手动删掉这些.lastUpdated文件:

find ~/.m2/repository -name "*.lastUpdated" -delete

这条命令在 Linux/macOS 上直接可用,Windows 下用 PowerShell 或者干脆手动删。清理完再打包,成功率会高不少。

4.5 外部 jar 包引入的正确姿势

老项目里经常有「手动引入本地 jar」的需求。热词里提到springboot+maven 导入外部 jar 包,这个场景确实麻烦。

不推荐的方式是用systemscope:

<dependency> <groupId>com.vendor</groupId> <artifactId>legacy-sdk</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/legacy-sdk.jar</systemPath> </dependency>

它能编译通过,但systemscope 在较新版本的 Maven 里已经被标记为不推荐,而且这个依赖不会被打进 fat jar,部署时还是得手动带上。

稳妥的做法有两条。一是把 jar 手动安装到本地仓库:

mvn install:install-file -Dfile=lib/legacy-sdk.jar \ -DgroupId=com.vendor \ -DartifactId=legacy-sdk \ -Dversion=1.0 \ -Dpackaging=jar

安装之后再按正常依赖引用,本地能编译,但团队其他人拉代码时仍需各自执行一遍。更彻底的方式是搭建内网私服,把 jar 传上去,所有成员统一从私服拉。公司项目里我一般推后者,个人项目前者够用。

5. 一些能提效的实操技巧

前面讲的都是主线流程,这一节聊点散装的、但日常很好用的东西。

5.1 多环境 profile 与打包的配合

项目通常有 dev、test、prod 三套配置。经典做法是按环境拆application-dev.ymlapplication-test.ymlapplication-prod.yml,然后通过spring.profiles.active激活。打包时用 Maven 的 profile 把对应环境的配置复制进产物:

<profiles> <profile> <id>prod</id> <properties> <env>prod</env> </properties> <build> <resources> <resource> <directory>src/main/resources</directory> <filtering>true</filtering> <includes> <include>application.yml</include> <include>application-${env}.yml</include> </includes> </resource> </resources> </build> </profile> </profiles>

打包命令带上 profile:

mvn clean package -Pprod -Dmaven.test.skip=true

<filtering>true</filtering>会开启占位符替换,application.yml里写${env}的会被替换成 profile 里的值。这里有个注意点:开启 filtering 后,所有@xxx@${xxx}形式的字符串都会被尝试替换,如果你的配置文件里有@Value注解或者路径中带${}的表达式,可能被误伤。规避方法是在文件顶部用#set($x=...)#[[...]]#之类的转义语法,或者干脆只对指定文件开启 filtering。

5.2 反编译验证:确认代码真的打进去了

打包这件事最怕的不是报错,是「看起来成功了但内容不对」。有一种办法可以直接验证 jar 里的 class 是不是最新代码:反编译。

javap是 JDK 自带的,最简单:

javap -c -p -classpath target/myapp.jar com.example.Main

-c输出字节码,-p显示私有成员。不加-c就只看类签名。想看得更舒服,可以用图形化反编译工具,直接把 jar 拖进去就能浏览源码结构的工具不少,社区里比较常见。

什么时候用得上这招?两个场景:一是怀疑打进去的是旧代码,反编译看看方法体里有没有你新加的逻辑;二是接手别人维护的老项目,jar 是唯一产物没有源码,想弄清楚它到底做了什么。这算是打包的一个副产品技能,但确实实用。

5.3 用 Docker 打包时容易忽略的细节

打包和 Docker 结合的场景现在太常见了,热词里也有idea 打包 docker 镜像。这里容易出问题的地方主要是镜像构建阶段的maven缓存和产物拷贝。

一个我常用的Dockerfile骨架:

FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -Dmaven.test.skip=true FROM eclipse-temurin:17-jre WORKDIR /app COPY --from=builder /build/target/myapp.jar app.jar ENV TZ=Asia/Shanghai ENTRYPOINT ["java", "-jar", "app.jar"]

关键点在于COPY pom.xmlmvn dependency:go-offline这一步单独做。Docker 的分层机制会让这一层被缓存,只要 pom 不变,后续构建就不用重新下载依赖,构建时间能从几分钟降到十几秒。这是实打实的效率提升,值得每个做容器化的项目都用上。

另外FROM maven:xxx AS builder这种多阶段构建,最终镜像里只保留jre基础镜像和 jar,不会有 Maven、源码这些,镜像体积小很多。生产环境强烈建议这么干。

提醒一下时区。基础镜像默认几乎都是 UTC,日志时间会差八个钟头。前面ENV TZ=Asia/Shanghai那行别省,省了之后排查问题会怀疑人生。

5.4 版本号管理与产物归档

最后说个容易被忽略但很重要的实践:别让target目录成为唯一产物来源

本地打出来的 jar 分散在各个项目的 target 目录,时间一长就找不到「上周发给运维那个版本是哪次构建的」。我的做法是在构建脚本里带上时间戳和 git 短哈希:

VERSION=$(git rev-parse --short HEAD) TIMESTAMP=$(date +%Y%m%d%H%M) mvn clean package -Dmaven.test.skip=true cp target/myapp.jar dist/myapp-${TIMESTAMP}-${VERSION}.jar

这样每次构建都留档,出问题能快速回滚到已知可用的版本。团队里如果要正式一点,就是上 CI 工具,让每次合入自动构建、自动上传到制品库,人工只负责触发部署。

这一步看起来和「打包」关系不大,但你踩过几次「旧包覆盖新包、版本对不上」的坑之后,就会知道留档这事有多值。

写到这里基本把 IDEA 配合 Maven 打 jar 的两种主线和周边都过了一遍。我个人的体会是,打包这件事真正的门槛不在操作,而在「想清楚产物要长什么样」——是给别的项目当依赖,还是自己独立运行;是本地调试用,还是上生产。答案不同,选型就不同。把这一点想明白,剩下的配置和命令都是照着填的事。

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

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

立即咨询