Java可执行JAR包打包大揭秘:三种方式全解析

4 阅读12分钟

引言: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 文件中,需要进行如下配置:


<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 包解压后,目录结构大致如下:


├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.example
│           └── java-demo
│               ├── pom.properties
│               └── pom.xml
└── org
    └── example
        └── App.class

MANIFEST.MF 文件内容类似:


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                         启动类

执行命令为:


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 文件中,配置如下:


<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 阶段执行该插件,goalsingle 表示生成一个独立的聚合 JAR 包。

打包后的成果呈现

执行 mvn clean package 命令后,在 target 目录下会生成一个包含所有依赖的可执行 JAR 包,例如 java-demo-1.0-SNAPSHOT-jar-with-dependencies.jar,同时还会有一个原始的不包含依赖的 JAR 包 java-demo-1.0-SNAPSHOT.jar。 可执行 JAR 包解压后,目录结构大致如下:


├── 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 文件内容类似:


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 文件中,配置如下:


<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 进行配置:


<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 - pluginmaven - assembly - pluginmaven - shade - plugin
打包大小较小,不包含依赖较大,包含所有依赖较大,包含所有依赖
依赖处理依赖外置,在MANIFEST.MF指定路径依赖内置,全部打包进 JAR依赖内置,全部打包进 JAR
冲突解决无特殊冲突解决机制可能出现依赖类合并冲突通过类重定位解决依赖冲突
部署便捷性需保证依赖目录与 JAR 包在同一级,部署相对复杂只需一个 JAR 文件,部署便捷只需一个 JAR 文件,部署便捷
适用场景依赖管理简单,对 JAR 包大小敏感,且部署环境能保证依赖路径一致性的项目依赖管理不太复杂,追求部署便捷性,对 JAR 包大小不太敏感的项目依赖复杂,存在依赖冲突风险,需要确保不同版本依赖共存的项目

根据场景选择

针对不同项目场景,选择合适的打包方式可以事半功倍:

  • 小型项目:如果项目依赖较少,且对 JAR 包大小比较敏感,同时部署环境能保证依赖路径的一致性,那么maven - jar - plugin是一个不错的选择。它可以让 JAR 包保持较小的体积,同时依赖管理也相对简单。例如一个简单的命令行工具项目,依赖的第三方库很少,使用maven - jar - plugin打包后,方便在不同环境中快速部署和运行。

  • 大型项目:对于大型项目,尤其是微服务架构中的各个服务,maven - assembly - pluginmaven - 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 包打包问题,让项目的交付更加顺利!如果在实践过程中遇到任何问题,欢迎随时在评论区留言,我们一起探讨解决 。