您现在的位置是:首页 >技术交流 >Maven uber-jar(带依赖的打包插件)maven-shade-plugin网站首页技术交流
Maven uber-jar(带依赖的打包插件)maven-shade-plugin
本文是对 maven-shade-plugin
常用配置的介绍,更详细的学习请参照 Apache Maven Shade Plugin 官方文档
通过使用 maven-shade-plugin
插件进行 Maven 的打包操作,可以将项目中的依赖一同添加到最终的项目 Jar 包内,maven-shade-plugin
插件有两个目标,我们要学习的是插件的 shade
目标,建议使用时与 Maven 生命周期的 package
阶段绑定
这中打包后带依赖的 Jar 包一般称为 uper-jar
或 fat-jar
不管 pom.xml
是否声明了 Maven 的默认打包插件 maven-jar-plugin
,也不管是否声明了其他打包插件,maven-jar-plugin
都会在 package
阶段最先执行,而 maven-shade-plugin
插件的 shade
目标,正是对 maven-jar-plugin
打包后的 Jar 包进行二次打包,同时将项目依赖的添加进去
最基础的 maven-shade-plugin 使用
按照如下配置 15~29 行,就可以正确的将 maven-shade-plugin
插件的 shade
目标与 Maven 的生命周期 package
阶段绑定,之后我们就可以通过 Maven 生命周期命令 mvn package
执行插件完成打包操作:
<!-- 模拟项目中使用的依赖 -->
<dependencies>
<!-- 依赖 commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
<!-- 对项目构建进行配置 -->
<build>
<plugins>
<!-- 引入 maven-shade-plugin 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
打包后,会将打包文件放到 ${project.build.directory}
中(默认是 target 目录),如下图:
上图中,我们重点关注两个文件:
-
ares5k-package-1.0-SNAPSHOT.jar:
maven-shade-plugin
对maven-jar-plugin
生成的 Jar 包进行二次打包后的 Jar 包,这个 Jar 包内已经包函项目的依赖了 -
original-ares5k-package-1.0-SNAPSHOT.jar:原始 Jar 包,
maven-jar-plugin
生成的不包含项目依赖的 Jar 包,maven-shade-plugin
为了避免原始 Jar 包和新 Jar 包名字冲突,对原始 Jar 包进行了重命名,添加了original-
前缀
将 maven-shade-plugin
生成的 Jar 包解压,观察其内部结构可以发现,maven-shade-plugin
打包后并没有将项目中依赖的 Jar 包直接存放,而是将依赖的 Jar 包解压后存放的:
这个特性很重要,因为 maven-shade-plugin
的另一个重要功能 <重命名> 就是基于这个实现的
另外,通过上面配置创建的 Jar 包是不能够直接运行的,因为我们没有指定项目的入口类,所以只适合让其他项目引用
生成可执行的 Jar 包 和 常用的资源转换类
仿照下面 19~27 行,就可以生成可直接运行的 Jar 包:
<!-- 对项目构建进行配置 -->
<build>
<plugins>
<!-- 引入 maven-shade-plugin 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<transformers>
<!-- 添加入口类 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<!-- 入口类 -->
<mainClass>com.ares5k.BB</mainClass>
</transformer>
</transformers>
</configuration>
</plugin>
</plugins>
</build>
上面代码中 ManifestResourceTransformer
是 maven-shade-plugin
插件提供的资源转换类,就是用来指定入口类的,除此之外,还有两个常用的资源转换类,AppendingTransformer
和 ServicesResourceTransformer
AppendingTransformer
这个资源转换类的作用,是在打包时,当碰见项目中或依赖中有同名的资源文件,要对文件内容进行追加操作,而不是覆盖
最常见的场景就是项目中引入大量 Spring 依赖时,每个 Spring 依赖都有自己的 META-INF/spring.handlers
、META-INF/spring.schemas
和 META-INF/spring.factories
等文件,这种情况下利用 maven-shade-plugin
插件打包,就会发生文件覆盖的问题,如果被覆盖的文件内容和新文件的内容一样,可能会掩盖这个现象,但是当内容不一样时,就会导致项目运行错误
以 Spring 为例,演示同名文件追加操作的配置 16~27 行:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<transformers>
<!-- 设置 META-INF/spring.handlers 文件追加而非覆盖 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<!-- 设置 META-INF/spring.schemas 文件追加而非覆盖 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
<!-- 设置 META-INF/spring.factories文件追加而非覆盖 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.factories</resource>
</transformer>
</transformers>
</configuration>
</plugin>
ServicesResourceTransformer
这个资源转换类的作用,是在打包时,当碰见同名的 SPI 接口文件时,会将文件内容追加而不是覆盖
我们使用 Java SPI 机制时,会在 META-INF/services
中创建与接口同名的文件来记录具体的实现类,当有多个同名文件时,说明该接口有多个实现类,这时候就可以利用 ServicesResourceTransformer
将文件内容合并,而不是覆盖 16~21 行:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<transformers>
<!-- 设置 Java SPI 追加而非覆盖 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</plugin>
包名重命名
重命名功能也是 maven-shade-plugin
插件的一个重要功能,这个功能可以对项目中或依赖中的包名进行重命名,包名重命名后还会把引用的地方一并修改,非常方便
这个功能的使用场景,主要是版本冲突,举个例子吧,前置条件如下:
-
我们的项目 A 中引用了
commons-lang3
,另一个需要依赖我们项目的工程 B 也引入了commons-lang3
-
两个
commons-lang3
的版本不一样,拥有相同包名、类名,但功能不同的类
基于上述条件,两个 commons-lang3
中相同包名类名的类只会被 JVM 加载一个,这就可能导致项目运行时出问题,比如调用的是未被加载的类中的功能,这时候就可以使用包名重命名功能防止 JVM 只加载一个类的情况
下面配置中 17~27 行就是对包名重新命名:
<!-- 引入 maven-shade-plugin 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<relocations>
<!-- 包名重命名 -->
<relocation>
<!-- 旧包名 -->
<pattern>org.apache.commons.lang3</pattern>
<!-- 新包名 -->
<shadedPattern>shade.org.apache.commons.lang3</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
重命名后,项目中引用的地方也会自动修改,我们通过反编译看一下重命名后使用原包名的 .class
文件,引用的包名已经自动替换成新的名字:
打包时排除依赖
排除整个依赖
打包时,想排除不需要的依赖,可以使用下面配置 17~24 行,这种方式是将整个依赖排除掉,语法为 groupId:artifactId
:
<!-- 引入 maven-shade-plugin 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<artifactSet>
<!-- 排除依赖,groupId:artifactId -->
<excludes>
<exclude>org.apache.commons:commons-lang3</exclude>
</excludes>
</artifactSet>
</configuration>
</plugin>
排除依赖中的部分文件
也可以更详细的配置,排除依赖中的某些文件,而不是整个依赖排除,17~30 行 :
<!-- 引入 maven-shade-plugin 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<filters>
<!-- 过滤 commons-lang3 依赖中的内容-->
<filter>
<artifact>org.apache.commons:commons-lang3</artifact>
<excludes>
<!-- 排除 commons-lang3 依赖中 org/apache/commons/lang3/math 下的所有文件-->
<exclude>org/apache/commons/lang3/math/**</exclude>
<!-- 排除 commons-lang3 依赖中 META-INF 下所有扩展名是 .txt 的文件-->
<exclude>META-INF/*.txt</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
排除项目中未使用的依赖
将项目中,引用后,实际没有使用的依赖排除掉,这个功能可以帮我们排除多余的包,来减小最后生成的 Jar 包的大小,
用的时候要注意一点,因为插件判断依赖是否使用,是通过依赖中的类是否被项目中的类引用来作为依据,假设我们使用 XML 版的 Spring,在 <bean>
中配置了依赖中的类,这个时候插件是无法检测到的,所以仍会将其排除
<!-- 引入 maven-shade-plugin 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<!-- 绑定 Maven 生命周期的 package 阶段 -->
<phase>package</phase>
<goals>
<!-- package 阶段执行时,让其调用插件的 repackage 目标 -->
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- 设置插件属性 -->
<configuration>
<!-- 将项目中未使用的依赖排除 -->
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
与其他常用打包插件比较
除了 maven-shade-plugin
之外,常用的打包插件还有 maven-jar-plugin
和 spring-boot-maven-plugin
,使用方法在我其他文章中也有记录,在此将它们作一个简单的比较
maven-shade-plugin
maven-shade-plugin
也可以将项目的依赖打进最终的项目 Jar 包中,但是其与 spring-boot-maven-plugin
不同的是,spring-boot-maven-plugin
是直接将依赖的 Jar 包放进项目的 Jar 包中,而 maven-shade-plugin
则是将依赖的 Jar 包解压,然后将解压后的文件放进最终的项目 Jar 包中
maven-shade-plugin
将依赖的 Jar 包解压后添加到项目的 Jar 包中的做法,为 maven-shade-plugin
带来了另一个重要的功能 <重命名>,因为将依赖的 Jar 包解压后都是以文件形式存在,所以 maven-shade-plugin
支持对对依赖的某个具体文件进行重命名,maven-shade-plugin
在重命名时,不只是将文件名字修改,连我们项目中对其引用的地方都会一同修改
重命名的做法可以避免版本冲突,想详细了解的可以参考我 maven-shade-plugin
的文章
spring-boot-maven-plugin
spring-boot-maven-plugin
是 Spring 提供的一个 Maven 打包插件,可以通过 maven 的插件命令运行,但是一般习惯将它与 maven 生命周期绑定,然后通过 maven 生命周期命令运行,它的特点是可以将项目中依赖的 Jar 包添加到最终生成的项目 Jar 包中
spring-boot-maven-plugin
主要是对 maven-jar-plugin
生成的项目 Jar 包进行二次打包,并将项目依赖的 Jar 包添加进项目的 Jar 包中
maven-jar-plugin
maven 生命周期中 package
阶段的默认插件,不管是否在 pom.xml
中主动声明,也不管是否有其他的 package
阶段插件被绑定,其在 package
阶段都会被最先执行
使用 maven-jar-plugin
打包时,不会将依赖的 Jar 包添加到生成的项目 Jar 包中,所以当项目中使用依赖时,需要自己准备依赖的 Jar 包,这样 maven-jar-plugin
打出的项目 Jar 包才能被成功运行