1. 项目概述:为什么我们需要“打包大全”?
在Java开发的世界里,Maven几乎是项目构建和依赖管理的代名词。而将我们的心血——一个完整的Java应用——打包成一个可执行的JAR文件,是交付给用户或部署到生产环境前的最后一步,也是最关键的一步。这听起来简单,不就是打个包吗?但真正踩过坑的开发者都知道,这里面的门道可太多了。
你可能遇到过这些问题:打出来的JAR包在本地
java -jar
运行得好好的,一到服务器就报
ClassNotFoundException
;或者依赖了上百个第三方库,打出来的“胖JAR”体积巨大,每次上传部署都慢得让人心焦;又或者,你的应用需要读取
src/main/resources
下的配置文件,打包后却怎么也找不到路径了。这些问题,根源都在于打包方式的选择和配置上。
网上关于Maven打包的文章很多,但往往只讲一两种方法,或者只给配置代码不说原理,遇到稍微复杂点的场景就抓瞎。这就是我写这篇“大全”的初衷。我将结合自己十多年在大小项目中摸爬滚打的经验,为你系统梳理从最基础的
maven-jar-plugin
,到最流行的
maven-shade-plugin
,再到追求极致体验的
spring-boot-maven-plugin
,以及面向未来的分层打包等所有主流方法。我会详细拆解每种方法的原理、适用场景、详细配置以及那些官方文档里不会写的“坑”。目标只有一个:让你看完之后,面对任何打包需求,都能游刃有余地选出最合适的那把“瑞士军刀”。
2. 核心打包策略深度解析
打包一个可执行JAR,核心要解决三个问题: 依赖管理 、 主类设定 和 资源处理 。不同的插件以不同的哲学来处理这些问题,从而衍生出不同的打包策略。
2.1 基础策略:标准JAR与依赖分离
这是最“Maven”的方式,使用自带的
maven-jar-plugin
。它的哲学是“职责分离”:生成的
projectname-version.jar
只包含你项目自己编译的类文件和资源,所有第三方依赖都通过
maven-dependency-plugin
复制到独立的
lib/
目录下。
为什么选择它? 这种方式的优势在于清晰和高效。在持续集成/持续部署(CI/CD)流水线中,如果你的依赖不常变化,那么只需要重复构建和传输很小的应用JAR包,依赖库可以被缓存或共享,极大地加快了构建和部署速度。它也便于依赖的单独管理和更新。
它的致命短板是什么?
可执行性差。你无法直接通过
java -jar app.jar
来运行它,因为
ClassLoader
在标准JAR包中找不到依赖的类。你必须手动指定类路径(classpath),例如:
java -cp “app.jar:lib/*” com.yourcompany.MainClass
这在生产环境的启动脚本中显得笨拙,且容易出错。因此,这种策略更适合作为库(Library)发布,或者需要与其他工具(如容器)深度集成的场景。
2.2 经典策略:构建“胖JAR”或“超级JAR”
为了解决依赖分离带来的启动不便,“胖JAR”(Fat JAR)或“超级JAR”(Uber JAR)的概念应运而生。其核心思想是: 将所有依赖的类文件、资源都解压后,重新打包进同一个JAR文件中 。这样,一个JAR包就包含了运行所需的一切。
实现这一思想的两位主力干将是:
-
maven-assembly-plugin: 这是一个非常通用和强大的打包插件,不仅能打JAR,还能打ZIP、TAR等。通过预定义的或自定义的assembly descriptor(装配描述符),它可以精确控制将哪些文件(项目代码、依赖、资源、脚本等)以何种结构打包到最终产物中。功能强大,但配置相对繁琐。 -
maven-shade-plugin: 可以看作是assembly插件在打“胖JAR”领域的专业化、优化版本。它专为打包可执行JAR设计,不仅合并依赖,还提供了两个杀手级功能: 重命名依赖包中的类 (解决不同依赖库中同名类文件的冲突),以及 处理资源文件中的特定内容 (比如合并所有依赖中的META-INF/services/文件)。对于需要解决依赖冲突的复杂项目,shade插件几乎是首选。
注意 :“胖JAR”并非银弹。它会导致JAR包体积庞大,每次更新哪怕只改一行代码,也需要上传整个巨大的JAR。在微服务架构和容器化部署中,这会影响部署效率。此外,如果多个服务使用大量相同的依赖(如Spring框架),每个服务的“胖JAR”都会包含这些依赖的副本,造成存储和内存的浪费。
2.3 现代策略:Spring Boot的“可执行JAR”
spring-boot-maven-plugin
重新定义了可执行JAR。它打的包不是一个标准的JAR,而是一个“JAR中的JAR”,或者说是一种特殊的归档格式。
它的工作原理很巧妙:
- 它创建一个“胖JAR”,但内部结构是分层的。
-
外层是应用的加载器(
org.springframework.boot.loader.JarLauncher)和你的应用类。 -
内层(在
BOOT-INF/lib/目录下)以JAR文件的形式原封不动地包含了所有依赖。 -
内层(在
BOOT-INF/classes/目录下)是你的应用资源。
这样做的好处是什么?
-
直接可执行
: 因为有了自定义的
JarLauncher,它可以加载BOOT-INF/lib/下的嵌套JAR,所以直接java -jar就能运行。 - 支持依赖分层,优化Docker镜像 : 这是它的王牌功能。通过配置,可以将依赖分为多个层(如:Spring Boot依赖层、快照依赖层、应用层)。在构建Docker镜像时,不常变化的依赖层可以被缓存,只有经常变动的应用层需要重建和传输,这使得Docker镜像的构建和推送速度极快。
- 与Spring Boot生态无缝集成 : 自动处理配置文件加载、Actuator端点、Profile激活等Spring Boot特性。
如果你的项目是基于Spring Boot的,那么
spring-boot-maven-plugin
就是你的不二之选。即使不是Spring Boot项目,你也可以利用它的
repackage
目标来获得一个结构良好的可执行JAR。
2.4 前沿策略:Docker时代的分层打包与JLink
在云原生和容器化成为主流的今天,打包策略也在进化。
-
利用Docker多阶段构建和分层 : 如前所述,
spring-boot-maven-plugin的分层特性可以与Dockerfile的多阶段构建完美结合。首先在“构建阶段”用Maven打包,并将分层信息(layers.idx)和每层文件导出;然后在“运行阶段”的Docker镜像中,按层拷贝文件。依赖层只要没有变化,就会命中Docker构建缓存,大大提升效率。 -
使用
jlink创建定制化运行时 : 对于Java 9及以上版本的项目,如果你的依赖模块化清晰,可以考虑使用jlink工具。它允许你基于JDK模块,只打包你的应用及其 真正需要 的JDK模块,生成一个极小的、定制化的Java运行时镜像。这个镜像可以直接包含你的应用,生成一个完全自包含的、无需安装系统级JDK的可执行文件。这能极大减少容器镜像的体积(从几百MB的完整JDK缩减到几十MB),是追求极致效率和安全的场景下的终极方案,但前提是你的项目和依赖必须支持JPMS(Java Platform Module System)。
3. 五大打包方法实战详解
理论说再多,不如一行配置。下面我将逐一演示每种方法的详细配置和操作,并附上我踩过的坑和总结的技巧。
3.1 方法一:使用 maven-jar-plugin + maven-dependency-plugin(依赖外置)
这是最基础、最标准的Maven方式。
pom.xml
配置如下:
<build>
<plugins>
<!-- 1. 配置主类 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath> <!-- 在MANIFEST.MF中生成Class-Path -->
<classpathPrefix>lib/</classpathPrefix> <!-- 指定依赖库的相对路径 -->
<mainClass>com.example.myapp.Main</mainClass> <!-- 指定主类 -->
</manifest>
</archive>
</configuration>
</plugin>
<!-- 2. 将依赖复制到指定目录 -->
<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> <!-- 绑定到package阶段 -->
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory> <!-- 复制到target/lib -->
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
执行与运行:
-
运行
mvn clean package。 -
查看
target/目录,会生成myapp-1.0.jar和lib/文件夹(里面是所有依赖的JAR)。 -
运行应用有两种方式:
-
指定类路径运行
:
java -cp “target/myapp-1.0.jar:target/lib/*” com.example.myapp.Main -
利用MANIFEST中的Class-Path
(更优雅):确保JAR包和
lib/目录的相对位置不变,然后java -jar target/myapp-1.0.jar。这是因为我们在maven-jar-plugin里配置了<addClasspath>true</addClasspath>,生成的MANIFEST.MF文件里会有一行Class-Path: lib/dependency1.jar lib/dependency2.jar ...,JVM会自动去这些路径下寻找类。
-
指定类路径运行
:
实操心得:
-
classpathPrefix的值lib/是相对于生成的JAR包所在目录的。如果你打算把JAR包和lib文件夹一起移动到别处,必须保持它们在同一目录下,且lib文件夹名称不变。 -
这种方式非常适合与服务器上的共享类库结合。比如,你可以把常用的、版本稳定的依赖(如Log4j、Guava)放在服务器的某个公共路径(如
/usr/share/java/lib/),然后在classpathPrefix或启动脚本中指向这个绝对路径,这样多个应用可以共享同一份依赖,节省空间。
3.2 方法二:使用 maven-assembly-plugin(定制化打包)
当你需要将应用、依赖、配置文件、启动脚本甚至文档打包成一个用于分发的ZIP或TAR包时,
assembly
插件是神器。
首先,定义一个装配描述符文件
src/assembly/distribution.xml
:
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
<id>distribution</id>
<formats>
<format>zip</format> <!-- 也可以同时生成tar.gz -->
<format>tar.gz</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory> <!-- 包含一个以artifactId-version为名的根目录 -->
<dependencySets>
<dependencySet>
<outputDirectory>/lib</outputDirectory> <!-- 依赖放到根目录下的lib里 -->
<scope>runtime</scope> <!-- 只包含runtime范围的依赖 -->
</dependencySet>
</dependencySets>
<fileSets>
<fileSet>
<directory>${project.basedir}/src/main/resources</directory>
<outputDirectory>/conf</outputDirectory> <!-- 配置文件放到conf目录 -->
<includes>
<include>*.yml</include>
<include>*.properties</include>
</includes>
</fileSet>
<fileSet>
<directory>${project.basedir}/src/main/scripts</directory>
<outputDirectory>/bin</outputDirectory> <!-- 启动脚本放到bin目录 -->
<fileMode>0755</fileMode> <!-- 赋予脚本可执行权限 -->
</fileSet>
<fileSet>
<directory>${project.build.directory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>*.jar</include> <!-- 主JAR包放到根目录 -->
</includes>
</fileSet>
</fileSets>
</assembly>
然后在
pom.xml
中配置插件:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptors>
<descriptor>src/assembly/distribution.xml</descriptor>
</descriptors>
<archive>
<manifest>
<mainClass>com.example.myapp.Main</mainClass>
</manifest>
</archive>
<!-- 附加后缀,避免与默认的jar包重名 -->
<appendAssemblyId>false</appendAssemblyId>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
执行与结果:
运行
mvn clean package
后,在
target/
目录下会生成类似
myapp-1.0-distribution.zip
的文件。解压后,你会看到一个结构清晰的分发包:
myapp-1.0/
├── bin/
│ └── start.sh
├── conf/
│ ├── application.yml
│ └── logback.xml
├── lib/
│ ├── dependency1.jar
│ └── dependency2.jar
└── myapp-1.0.jar
用户拿到这个包,只需要运行
bin/start.sh
(里面包含了正确的
java -cp
命令)即可启动,非常友好。
注意事项:
-
assembly插件功能强大但配置复杂,容易出错。务必仔细检查fileSet的路径和包含/排除规则。 - 对于简单的“胖JAR”需求,用它有点杀鸡用牛刀。但对于需要生成包含多种文件、有特定目录结构的 发布包 ,它是无可替代的。
3.3 方法三:使用 maven-shade-plugin(解决冲突的胖JAR)
shade
插件是构建“胖JAR”的行业标准,尤其擅长处理依赖冲突。它的核心配置如下:
<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>
<createDependencyReducedPom>false</createDependencyReducedPom> <!-- 通常不需要生成简化pom -->
<transformers>
<!-- 合并多个依赖中的META-INF/services/下的文件,对使用ServiceLoader机制的关键 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<!-- 指定主类 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.myapp.Main</mainClass>
</transformer>
<!-- 合并Apache许可证等文件,避免重复 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
</transformers>
<!-- 可选:重命名冲突的包 -->
<relocations>
<relocation>
<pattern>com.google.guava</pattern>
<shadedPattern>com.example.shaded.guava</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons.lang3</pattern>
<shadedPattern>com.example.shaded.commons.lang3</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>
执行与结果:
运行
mvn clean package
后,
target/
目录下会生成两个JAR:一个是原始的
myapp-1.0.jar
,另一个是带有
-shaded
分类器(或根据配置替换了原始文件)的“胖JAR”。这个胖JAR包含了所有依赖,可以直接用
java -jar myapp-1.0-shaded.jar
运行。
重定位(Relocation)的妙用:
这是
shade
插件解决类冲突的核武器。假设你的项目依赖了库A(内嵌了Guava 18.0)和库B(内嵌了Guava 30.0),直接打包会导致类冲突。通过上述
<relocations>
配置,你可以将其中一个库(或两者)中的Guava类全部重命名到新的包路径下(如
com.example.shaded.guava
)。这样,两个版本的Guava在运行时就被隔离了,互不影响。代价是,如果你在代码中直接使用了Guava的类,也需要相应修改导入语句,或者只重定位那些你确信不会直接使用的、纯内部依赖的库。
踩坑实录:资源文件合并
ServicesResourceTransformer
至关重要。很多库(如JDBC驱动、日志实现、序列化框架)通过
META-INF/services/
下的文件声明其SPI(Service Provider Interface)实现。如果不合并这些文件,打包后可能只有最后一个被处理的依赖的SPI配置生效,导致功能异常(比如找不到数据库驱动)。务必在
<transformers>
中加上它。
3.4 方法四:使用 spring-boot-maven-plugin(Spring Boot之道)
对于Spring Boot项目,这是最简单、最强大的方式。基本配置极其简洁:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.5</version> <!-- 使用与你Spring Boot版本对应的插件版本 -->
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 重新打包,将依赖打入 -->
</goals>
<configuration>
<!-- 主类通常会自动从spring-boot-starter中推断,也可显式指定 -->
<mainClass>com.example.myapp.MyApplication</mainClass>
<!-- 启用分层支持,为Docker优化做准备 -->
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</execution>
</executions>
</plugin>
执行与结果:
运行
mvn clean package
后,在
target/
目录下会生成两个JAR:一个是原始的
myapp-1.0.jar
(内容很少),另一个是重新打包后的
myapp-1.0.jar.original
(原始的瘦JAR),而默认的
myapp-1.0.jar
则变成了Spring Boot的可执行“胖JAR”。直接运行
java -jar target/myapp-1.0.jar
即可。
高级特性:分层打包优化Docker构建
启用
<layers>true</layers>
后,打包时会额外生成一个
layers.idx
文件,定义了JAR包内部的分层结构(如:dependencies, spring-boot-loader, snapshot-dependencies, application)。你可以使用
jarmode
工具来提取这些层:
# 查看分层信息
java -Djarmode=layertools -jar myapp-1.0.jar list
# 提取各层到指定目录
java -Djarmode=layertools -jar myapp-1.0.jar extract --destination /path/to/extract
结合以下Dockerfile,可以实现高效的镜像构建:
# 第一阶段:构建
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# 第二阶段:运行
FROM openjdk:17-jdk-slim
WORKDIR /app
# 从构建阶段复制分层JAR
COPY --from=builder /app/target/myapp-*.jar app.jar
# 使用jarmode提取层(实际中更常用的是在构建阶段提取好再复制)
# 为了清晰,这里展示在运行阶段提取的简单方式,生产环境建议优化
RUN java -Djarmode=layertools -jar app.jar extract
# 按层拷贝,依赖层变化少,可以利用Docker缓存
COPY --from=builder /app/target/dependencies/ ./
COPY --from=builder /app/target/spring-boot-loader/ ./
COPY --from=builder /app/target/snapshot-dependencies/ ./
COPY --from=builder /app/target/application/ ./
ENTRYPOINT [“java”, “org.springframework.boot.loader.JarLauncher”]
这样,当你的应用代码变更而依赖不变时,Docker在构建到
COPY --from=builder /app/target/dependencies/ ./
这一层时就会直接使用缓存,无需重新下载和安装依赖,极大加速构建过程。
3.5 方法五:结合Docker多阶段构建与JLink(追求极致)
对于非Spring Boot项目或追求极致镜像体积和启动速度的场景,可以结合Docker多阶段构建和JLink。
思路:
-
使用
maven-dependency-plugin和maven-jar-plugin打出标准的“依赖外置”包。 -
在第一阶段(构建阶段)使用
jlink基于你的模块描述(module-info.java)创建一个只包含必要模块的定制JRE。 - 在第二阶段(运行阶段)只拷贝这个定制JRE、你的应用JAR和依赖库。
简化示例Dockerfile:
# 第一阶段:用Maven构建应用,并用JLink创建定制JRE
FROM maven:3.8-openjdk-17 AS build
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
# 假设项目是模块化的,模块名为 com.example.myapp
# 找出所有依赖的模块(这里需要更复杂的脚本分析,此处简化)
RUN jlink --add-modules java.base,java.sql,java.desktop,com.example.myapp \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /custom-jre
# 第二阶段:最小化运行镜像
FROM debian:bullseye-slim
WORKDIR /app
# 拷贝定制JRE
COPY --from=build /custom-jre /opt/jre
ENV JAVA_HOME=/opt/jre
ENV PATH=“${JAVA_HOME}/bin:${PATH}”
# 拷贝应用jar和依赖lib
COPY --from=build /app/target/myapp-1.0.jar .
COPY --from=build /app/target/lib ./lib
ENTRYPOINT [“java”, “-cp”, “myapp-1.0.jar:lib/*”, “com.example.myapp.Main”]
通过这种方式,最终的运行镜像可以非常小(可能只有50MB左右),因为它不包含完整的JDK,只包含了应用运行必需的模块。安全补丁更新也只需要重建和替换这个定制JRE即可。
4. 常见问题排查与实战技巧
即使配置正确,打包和运行过程中也可能遇到各种“妖魔鬼怪”。下面是我总结的一些高频问题和解决思路。
4.1 类找不到(ClassNotFoundException/NoClassDefFoundError)
这是最常见的问题,根本原因都是类加载器在运行时找不到指定的类。
-
问题表现
: 运行
java -jar时,报错ClassNotFoundException: com/example/SomeClass或NoClassDefFoundError。 -
排查思路
:
-
检查依赖是否真的被打包
:对于“胖JAR”,用解压工具(如
jar tf your.jar或unzip -l your.jar)查看JAR包内部,在BOOT-INF/lib/(Spring Boot)或根目录下是否存在包含缺失类的依赖JAR。对于“依赖外置”方式,检查lib/目录下是否有对应JAR。 -
检查依赖作用域(Scope)
:在
pom.xml中,<scope>provided</scope>的依赖(如Servlet API,在运行时由容器提供)和<scope>test</scope>的依赖不会被包含到打包的依赖中。确保运行时需要的依赖是<scope>compile</scope>(默认)或<scope>runtime</scope>。 -
检查MANIFEST.MF文件
:对于依赖外置的JAR,检查
MANIFEST.MF中的Class-Path属性是否完整、路径是否正确。路径分隔符在Unix系统是冒号:,在Windows是分号;。 -
依赖冲突导致类被错误覆盖
:如果使用了
shade插件但没有正确配置重定位,或者多个依赖包含了不同版本的同名类,可能会加载到错误的版本。使用mvn dependency:tree查看依赖树,排查冲突。对于shade插件,考虑启用重定位。
-
检查依赖是否真的被打包
:对于“胖JAR”,用解压工具(如
4.2 资源文件找不到
-
问题表现
: 代码中使用
getClass().getResource(“/config.properties”)或Thread.currentThread().getContextClassLoader().getResourceAsStream(“template.html”)在IDE中运行正常,但打包后返回null。 -
根本原因
: 资源文件的路径在打包后发生了变化。标准Maven项目,
src/main/resources下的文件在打包后会位于JAR包的根目录。但在“胖JAR”中,你的资源文件可能被“淹没”在无数个依赖JAR里,或者被插件处理时移动了位置。 -
解决方案
:
-
统一使用ClassLoader获取资源
:优先使用
Thread.currentThread().getContextClassLoader().getResourceAsStream(),它的搜索范围更广。 -
检查插件配置
:对于
assembly或shade插件,检查<fileSet>或资源转换器的配置,确保资源文件被包含并放在了预期的路径。 -
Spring Boot的特殊处理
:Spring Boot有自己的一套资源加载逻辑(
ResourceLoader),通常能很好地处理嵌套JAR中的资源。如果仍有问题,可以尝试使用ResourceUtils或new ClassPathResource(“file.txt”)。
-
统一使用ClassLoader获取资源
:优先使用
4.3 JAR包太大或构建太慢
-
问题
:“胖JAR”动辄几十MB甚至上百MB,每次
mvn package都要花很长时间下载和打包依赖。 -
优化策略
:
-
依赖分析,去芜存菁
:定期运行
mvn dependency:analyze,检查Unused declared dependencies,移除真正用不到的依赖。检查Used undeclared dependencies,确认是否是必需的。 - 使用轻量级替代库 :例如,用OkHttp替代老旧的HttpClient,用SLF4J+Logback替代Log4j 1.x。
- 启用Docker分层(Spring Boot) :如前所述,这虽然不减少JAR本身大小,但能极大优化Docker镜像的构建和传输效率。
- 考虑非“胖JAR”方案 :如果环境可控(如容器化),回归“依赖外置”模式,结合Docker的卷挂载或分层,可能更高效。
-
使用Maven离线模式与本地仓库缓存
:在CI/CD服务器上维护好本地仓库缓存,并使用
mvn -o(offline)模式进行打包,可以跳过网络下载。
-
依赖分析,去芜存菁
:定期运行
4.4 版本冲突与NoSuchMethodError
-
问题表现
: 程序运行时抛出
NoSuchMethodError或NoSuchFieldError,但编译时一切正常。 - 根本原因 : 依赖的传递性导致了同一个库有多个版本被引入,而Maven默认选择了其中一个版本(遵循“最近定义”和“最短路径”原则),但这个版本可能缺少你的代码所调用的方法。
-
排查与解决
:
-
锁定依赖版本
:在顶级
pom.xml的<dependencyManagement>中显式声明常用库的版本,统一所有子模块的版本。 -
使用
mvn dependency:tree -Dverbose:查看详细的依赖树,找出冲突的库和它们的不同版本路径。 -
排除特定传递依赖
:在引入依赖时,使用
<exclusions>标签排除掉不需要的传递性依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> -
终极武器:Shade插件重定位
:如果冲突发生在你无法直接控制的第三方库之间,使用
shade插件的重定位功能,将其中一个库的包路径整体迁移走。
-
锁定依赖版本
:在顶级
4.5 签名与安全相关警告
-
问题
: 打包时或运行“胖JAR”时,控制台出现
“Invalid signature file digest for Manifest main attributes”或关于META-INF/*.SF的警告。 - 原因 : 有些依赖JAR是经过数字签名的(如某些旧版本的BouncyCastle)。当“胖JAR”插件将这些签名文件一并打包进去时,会破坏签名,导致JVM验证失败。
-
解决
: 在
shade或spring-boot-maven-plugin的配置中,过滤掉这些签名文件。<!-- 在shade插件中 --> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters><!-- 在spring-boot-maven-plugin中 --> <configuration> <excludes> <exclude> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> </exclude> </excludes> <!-- 或者更通用的过滤 --> <excludeGroupIds>org.bouncycastle</excludeGroupIds> </configuration>
5. 如何选择最适合你的打包方式?
面对这么多方法,到底该怎么选?我总结了一个决策流程图和场景建议,你可以对号入座:
决策流程:
-
你的项目是Spring Boot吗?
-
是
-> 毫不犹豫,选择
spring-boot-maven-plugin。如果考虑容器化部署,务必开启<layers>true</layers>。 - 否 -> 进入第2步。
-
是
-> 毫不犹豫,选择
-
你需要解决复杂的依赖冲突吗?或者你需要一个包含所有依赖的单一可执行JAR?
-
是,且需要解决冲突
-> 选择
maven-shade-plugin,并配置好重定位。 -
是,但冲突不复杂或可以规避
->
maven-shade-plugin或maven-assembly-plugin(后者配置更灵活)都可以。 - 否,我可以接受依赖外置 -> 进入第3步。
-
是,且需要解决冲突
-> 选择
-
你的交付物需要包含启动脚本、配置文件、文档等,形成一个完整的发布包吗?
-
是
-> 选择
maven-assembly-plugin,定制你的分发包结构。 -
否,我只需要一个可运行的JAR
-> 回到第2步的“是”分支,选择
shade或assembly打胖JAR。 -
否,我只需要一个库(Library)JAR
-> 使用默认的
maven-jar-plugin即可。
-
是
-> 选择
-
你对启动速度和镜像体积有极致要求,且项目是模块化的吗?
- 是 -> 深入研究 JLink + Docker多阶段构建 ,这是未来的方向,但前期有一定复杂度。
场景化建议表:
| 场景 | 推荐方案 | 关键理由 |
|---|---|---|
| 传统单体Spring Boot应用 |
spring-boot-maven-plugin
(开启分层)
| 开箱即用,生态支持好,分层优化Docker构建。 |
| 微服务(非Spring Boot) |
maven-shade-plugin
或
依赖外置+Docker优化
| 服务独立部署,胖JAR简单;依赖外置更适合CI/CD流水线优化。 |
| 提供二进制的客户端工具 |
maven-shade-plugin
| 用户只需一个JAR文件,双击或命令行直接运行,体验最好。 |
| 企业内网复杂依赖环境 |
maven-shade-plugin
(配合重定位)
| 内网库版本混乱,重定位能彻底隔离依赖,避免冲突。 |
| 需要生成包含脚本、配置的安装包 |
maven-assembly-plugin
| 灵活定义打包内容和结构,生成ZIP/TAR等格式,方便分发给运维或用户。 |
| 作为公共库发布到Maven中央仓库 |
默认的
maven-jar-plugin
| 标准格式,依赖由使用者管理,避免传递依赖污染。 |
| 对安全、体积有严苛要求的容器化应用 | JLink + 多阶段Docker构建 | 生成最小化运行时镜像,减少攻击面,提升启动速度。 |
打包不是一件一劳永逸的事情。随着项目演进、依赖更新、部署环境变化,你可能需要重新评估和调整打包策略。我的经验是,在项目初期选择一种简单可靠的方式(如Spring Boot插件或Shade插件),快速搭建起部署流水线。当遇到性能瓶颈、依赖冲突或新的部署需求时,再根据上述指南进行优化和调整。理解每种工具背后的原理,才能让你在遇到问题时,能快速定位并找到最适合的解决方案。

5313

被折叠的 条评论
为什么被折叠?



