引言:JAR 包打包的困扰与重要性
写完代码只是万里长征的第一步,如何将代码打包成一个 "开箱即用" 的 JAR 文件,才是交付的关键一步。不少 Java 开发者都有过这样的经历:本地运行得好好的程序,兴高采烈地打包后,满心欢喜地准备部署,结果一运行,报错ClassNotFoundException!这时候先别慌,大概率不是代码出了岔子,而是 JAR 包没打好。
在 Java 开发中,JAR(Java Archive)包是一种非常常见的文件格式,它可以将多个 Java 类文件及其相关元数据和资源(如图像和库)打包成单一文件,方便分发和部署。而对于 Maven 项目来说,打可执行 JAR 包有多种方式。今天,我们就来深入对比三种主流方案:maven-jar-plugin(轻量外置依赖)、maven-assembly-plugin(全家桶打包)和 maven-shade-plugin(高级防冲突版)。每种方式都会附上真实的 pom.xml 配置、执行命令以及输出结构,让大家看完就能轻松上手。
方式一:maven - jar - plugin,轻量但依赖外置
方式一:maven-jar-plugin,轻量但依赖外置
原理剖析
maven-jar-plugin 是 Maven 的一个内置插件,主要用于将项目编译后的 class 文件及相关资源打包成 JAR 文件。不过,它打包时仅包含项目自身的代码和资源,第三方依赖则不会被打包进去。那运行时如何找到这些依赖呢?它通过在 MANIFEST.MF 文件中指定依赖的路径,让 JVM 在运行时能从指定位置加载依赖。就好比你搬家时,只带走了自己的行李,而家具等大件物品则在新家附近的仓库放着,然后在入住清单上写清楚了仓库的位置,这样入住时就能顺利取到家具。
pom.xml 配置详解
在 pom.xml 文件中,需要进行如下配置:
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<mainClass>org.example.App</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>dependencies/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/dependencies/</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
配置解释如下:
-
mainClass:指定程序的入口类,也就是包含main方法的类。 -
addClasspath:设置为true表示将依赖的路径添加到MANIFEST.MF文件的Class-Path中。 -
classpathPrefix:指定依赖路径的前缀,这里表示依赖包存放在dependencies/目录下。 -
maven-dependency-plugin:这个插件用于将项目的依赖复制到指定目录,outputDirectory指定了依赖包的输出目录,includeScope设置为runtime表示只复制运行时依赖的包。
打包后结构与执行命令
执行 mvn clean package 命令后,在 target 目录下会生成项目的 JAR 包以及 dependencies 目录,dependencies 目录中存放着项目的所有第三方依赖包。 JAR 包解压后,目录结构大致如下:
Plain
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── org.example
│ └── java-demo
│ ├── pom.properties
│ └── pom.xml
└── org
└── example
└── App.class
MANIFEST.MF 文件内容类似:
Plain
Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 17
Class-Path: dependencies/fastjson2-2.0.60.jar 第三方依赖包在这里
Main-Class: org.example.App 启动类
执行命令为:
bash
java -jar java-demo-1.0-SNAPSHOT.jar
运行时,JVM 会根据 MANIFEST.MF 文件中 Class-Path 指定的路径去加载依赖包。
优缺点分析
-
优点:
-
JAR 包体积小:由于不包含第三方依赖,JAR 包本身的大小相对较小,便于传输和存储。
-
依赖清晰 :依赖包单独存放在
dependencies目录中,依赖关系一目了然,方便管理和维护。
-
-
缺点:
- 依赖位置限制 :运行时必须保证
dependencies目录与 JAR 包在同一级目录下,否则会因为找不到依赖而运行失败,这在部署时可能会带来一些不便。例如在不同环境下部署,若目录结构发生变化,就需要手动调整依赖目录的位置。
- 依赖位置限制 :运行时必须保证
方式二:maven - assembly - plugin,全家桶打包
独特的打包特点
maven-assembly-plugin 主打一个 "全家桶" 概念,它生成的是所谓的 "fat jar",也就是将项目代码以及所有依赖的 class 文件全部打包到一个 JAR 文件中。就好比你搬家时,把所有的东西,包括家具、行李等一股脑都塞进了一个超大的集装箱里,这样到了新家,只要这个集装箱在,所有东西都在,完全不用担心依赖丢失的问题。在微服务架构中,这种打包方式就非常实用,它可以确保各个服务独立运行,不用担心依赖的传递和管理。
关键的 pom.xml 配置
在 pom.xml 文件中,配置如下:
xml
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<archive>
<manifest>
<mainClass>org.example.App</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
配置解释如下:
-
mainClass:指定程序的入口类,这是程序运行的起点。 -
descriptorRefs中的jar-with-dependencies:这是一个预定义的描述符,表示生成包含所有依赖的 JAR 包,使用这个描述符可以快速实现全家桶打包。 -
execution部分:id是执行的唯一标识符,phase表示在 Maven 的package阶段执行该插件,goal为single表示生成一个独立的聚合 JAR 包。
打包后的成果呈现
执行 mvn clean package 命令后,在 target 目录下会生成一个包含所有依赖的可执行 JAR 包,例如 java-demo-1.0-SNAPSHOT-jar-with-dependencies.jar,同时还会有一个原始的不包含依赖的 JAR 包 java-demo-1.0-SNAPSHOT.jar。 可执行 JAR 包解压后,目录结构大致如下:
Plain
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── org.example
│ └── java-demo
│ ├── pom.properties
│ └── pom.xml
├── org
│ └── example
│ └── App.class
├── com
│ └── fasterxml
│ └── json2
│ └── ... fastjson2相关类文件
├── org
│ └── slf4j
│ └── ... slf4j相关类文件
└── ... 其他依赖的类文件
MANIFEST.MF 文件内容类似:
Plain
Manifest-Version: 1.0
Created-By: Maven Assembly Plugin 3.4.2
Build-Jdk-Spec: 17
Main-Class: org.example.App
深入权衡利弊
-
优点:
-
部署便捷:只需一个 JAR 文件,无需额外管理依赖包,部署时直接上传运行即可,大大简化了部署流程,降低了出错的概率。在生产环境中,运维人员可以更方便地进行部署和维护,不用担心依赖包的丢失或版本不一致的问题。
-
依赖统一管理:所有依赖都在一个 JAR 包内,不会出现依赖冲突(只要打包时不冲突),而且在不同环境中运行时,依赖的一致性有保障,避免了因环境差异导致的依赖问题。
-
-
缺点:
-
JAR 包体积大:由于包含了所有依赖,JAR 包的体积通常会比较大,这在网络传输和存储时可能会带来一些不便,比如部署到远程服务器时,上传速度会受到影响。
-
依赖类合并冲突:在打包过程中,如果不同依赖中有同名的类,可能会导致冲突。虽然可以通过一些配置来解决部分冲突,但仍可能出现运行时错误,排查和解决这类问题往往比较困难。
-
方式三:maven - shade - plugin,高级防冲突版
核心技术:类重定位
maven-shade-plugin 可谓是一个 "全能选手",它不仅能像 maven-assembly-plugin 一样将项目和依赖打包成一个可执行的 "超级 JAR"(也就是 "fat jar"),还自带了一个高级技能 ------ 类重定位(Relocating Classes)。在复杂的项目中,不同依赖可能会引入相同库的不同版本,或者不同依赖中有同名的类,这就好比两个搬家的人都带了同样名字的家具,放在同一个屋子里肯定会乱套,程序运行时就会报错。而 maven-shade-plugin 的类重定位功能就像是给其中一个人的家具都贴上了特殊标签,改变了它们的 "名字"(类名、包名),这样就避免了冲突。它使用 ASM 字节码操作库,在打包过程中动态地修改类的字节码,将指定的类或包移动到新的命名空间 ,从而实现不同版本依赖的共存。
详细配置步骤
在 pom.xml 文件中,配置如下:
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.example.App</mainClass>
</transformer>
</transformers>
<relocations>
<relocation>
<pattern>org.old.package</pattern>
<shadedPattern>org.new.shaded.package</shadedPattern>
</relocation>
</relocations>
<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>
</plugins>
</build>
配置解释如下:
-
transformers中的ManifestResourceTransformer:用于修改MANIFEST.MF文件,设置mainClass为程序的入口类,这样生成的 JAR 包就可以直接通过java -jar命令运行。 -
relocations:这是类重定位的核心配置部分,pattern指定需要重定位的原始包名,shadedPattern指定重定位后的目标包名。比如项目中org.old.package下的类可能与其他依赖冲突,就可以将其重定位到org.new.shaded.package。 -
filters:用于过滤资源,这里配置排除META-INF目录下的签名文件(.SF、.DSA、.RSA),因为这些文件在合并 JAR 包时可能会引起冲突。
实际应用案例分析
假设有一个电商项目,使用了一个老版本的支付 SDK(old-payment-sdk),它依赖 guava 18.0 版本。而项目中其他功能需要使用 guava 32.0 版本来获得新特性和性能优化。在没有使用 maven-shade-plugin 之前,项目启动时就会报错,因为不同版本的 guava 冲突了,导致一些方法找不到或者类加载错误。
使用 maven-shade-plugin 进行配置:
xml
<configuration>
<relocations>
<relocation>
<pattern>com.google.common</pattern>
<shadedPattern>com.shaded.guava18.com.google.common</shadedPattern>
<includes>
<include>com.old.payment:old-payment-sdk</include>
</includes>
</relocation>
</relocations>
</configuration>
配置解释:这里将 old-payment-sdk 及其依赖中的 com.google.common 包下的所有类重定位到 com.shaded.guava18.com.google.common 包下。includes 标签指定了只对 old-payment-sdk 进行重定位操作,这样项目中其他地方使用的 guava 32.0 版本就不会受到影响。
执行 mvn clean package 命令后,生成的 JAR 包中就包含了两个版本的 guava,并且不会冲突。在代码中,访问 old-payment-sdk 中的 guava 相关类时,需要使用重定位后的包名,比如原来 com.google.common.collect.Lists 现在要写成 com.shaded.guava18.com.google.common.collect.Lists 。通过这种方式,成功解决了依赖冲突问题,项目能够正常启动和运行 。
对比总结:选择最合适的打包方式
特性全面对比
为了更直观地对比这三种插件,我们将它们在打包大小、依赖处理、冲突解决、部署便捷性等方面的特性整理成如下表格:
| 特性 | maven - jar - plugin | maven - assembly - plugin | maven - shade - plugin |
|---|---|---|---|
| 打包大小 | 较小,不包含依赖 | 较大,包含所有依赖 | 较大,包含所有依赖 |
| 依赖处理 | 依赖外置,在MANIFEST.MF指定路径 |
依赖内置,全部打包进 JAR | 依赖内置,全部打包进 JAR |
| 冲突解决 | 无特殊冲突解决机制 | 可能出现依赖类合并冲突 | 通过类重定位解决依赖冲突 |
| 部署便捷性 | 需保证依赖目录与 JAR 包在同一级,部署相对复杂 | 只需一个 JAR 文件,部署便捷 | 只需一个 JAR 文件,部署便捷 |
| 适用场景 | 依赖管理简单,对 JAR 包大小敏感,且部署环境能保证依赖路径一致性的项目 | 依赖管理不太复杂,追求部署便捷性,对 JAR 包大小不太敏感的项目 | 依赖复杂,存在依赖冲突风险,需要确保不同版本依赖共存的项目 |
根据场景选择
针对不同项目场景,选择合适的打包方式可以事半功倍:
-
小型项目 :如果项目依赖较少,且对 JAR 包大小比较敏感,同时部署环境能保证依赖路径的一致性,那么
maven - jar - plugin是一个不错的选择。它可以让 JAR 包保持较小的体积,同时依赖管理也相对简单。例如一个简单的命令行工具项目,依赖的第三方库很少,使用maven - jar - plugin打包后,方便在不同环境中快速部署和运行。 -
大型项目 :对于大型项目,尤其是微服务架构中的各个服务,
maven - assembly - plugin或maven - shade - plugin更为合适。如果依赖管理相对简单,没有复杂的依赖冲突问题,maven - assembly - plugin的 "全家桶" 打包方式可以简化部署流程,提高部署效率。比如一个电商微服务项目,各个服务之间依赖明确,使用maven - assembly - plugin打包后,每个服务都可以独立部署,互不干扰。 -
依赖复杂项目 :当项目依赖复杂,存在不同版本依赖冲突的风险时,
maven - shade - plugin无疑是最佳选择。它通过类重定位功能,能够有效地解决依赖冲突问题,确保项目的稳定运行。例如一个大型的企业级应用,集成了多个不同团队开发的模块,每个模块可能依赖不同版本的相同库,使用maven - shade - plugin可以让这些依赖和谐共处,避免因冲突导致的运行时错误 。
在实际项目中,选择合适的打包方式至关重要。希望通过本文的介绍,大家能根据项目的具体需求,灵活运用这三种打包方式,顺利完成项目的交付。如果在实践过程中有任何疑问或心得,欢迎在评论区留言分享,让我们一起进步!
结语:打包之路,从此畅通
JAR 包的打包方式各有千秋,maven-jar-plugin 以小巧轻便见长,适用于依赖管理简单的项目;maven-assembly-plugin "全家桶" 式的打包风格,让部署变得轻而易举,是追求便捷部署项目的首选;而 maven-shade-plugin 凭借强大的类重定位技术,在复杂依赖的项目中发挥着关键作用,解决了令人头疼的依赖冲突问题。
在实际的项目开发中,大家要根据项目的具体情况,如依赖的复杂程度、JAR 包大小的限制、部署环境的要求等,灵活选择合适的打包方式。希望大家通过这篇文章,对这三种主流的打包方式有了更深入的理解,在今后的 Java 开发中,能够轻松应对 JAR 包打包问题,让项目的交付更加顺利!如果在实践过程中遇到任何问题,欢迎随时在评论区留言,我们一起探讨解决 。