Maven打包配置
FatJar & ThinJar
FatJar : 即UberJar,表示包含所有依赖包的 jar 包。启动的时候指定一个jar包就可以加载所有的业务代码及依赖包。如果依赖项较多,最终打出来的Jar包会庞大到数G。FatJar存在一个很大的问题:某次更新仅影响了很小一部分代码却要重新生成一个体积庞大的Jar包。如果再加入微服务、Docker镜像打包、跨网段传输等影响,大体积的劣势会更加明显。
ThinJar : 只含本maven项目下的类的Jar包,体积小,需要手动维护第三方Jar的lib目录。
(StackOverflow上的这个问题把ThinJar定义为本APP的代码+直接依赖,有点歧义但不纠结)
Jar的格式
Jar包以zip的方式压缩了一组类及其相关资源。目录结构大致如图
tips :VIM可以直接编辑压缩文件或者Jar包。
其中META-INF/MANIFEST.MF
是Jar包的元数据文件。举个例子
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: myth
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.ruoyi.file.RuoYiFileApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.7.7
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_351
Main-Class: org.springframework.boot.loader.JarLauncher
条目含义都很清晰,其中需要关注的是Main-Class
,它指定了程序启动的入口类。
Jar的启动方式
由此可以介绍一下可执行Jar与非可执行Jar的区别。
-
可执行Jar
在
MANIFEST.MF
指定了Main-Class
,通过java -jar xxx.jar
可以直接执行程序。 -
非可执行Jar
没有指定
Main-Class
,要执行它则需要通过java -cp 'MyProgram.jar:libs/*' main.Main
。
特别需要注意的是:-jar
与 -cp
不可以共存,如果同时使用,后者将被忽略。-cp
指定多个加载目录很容易,而-jar
如何增加额外的lib路径呢?
java提供了一个参数loader.path
来指定其他jar的加载路径,举个例子:java -Dloader.path=./lib -jar app.jar
三种打包Fatjar方法
-
非遮蔽方法(Unshaded)
解压所有 jar 文件,再重新打包成一个新的单独的 jar 文件。使用maven-assembly-plugin插件实现,其中预定义的jar-with-dependencies descriptor会打包编译结果,并带上所有的依赖,如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar。
举个dlink的例子
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>${maven-assembly-plugin.version}</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <!-- 可以设置jar包的入口类(可选) --> <mainClass>com.dlink.app.MainApp</mainClass> </manifest> </archive> <outputDirectory>${project.parent.parent.basedir}/build/extends</outputDirectory> </configuration> <executions> <execution> <id>make-assembly</id> <goals> <goal>single</goal> </goals> <phase>package</phase> </execution> </executions> </plugin>
结果如图
-
遮蔽方法(Shaded)
Shaded也会将所有的依赖打进jar包,与Unshaded不同之处在于它会将依赖重命名,重命名的过程即是Shade。这么处理的原因是有些时候程序需要使用同名依赖的不同版本,而相同全限定名的类只会被加载一次,如果使用了加载版本所不具有的方法就会抛出Class Not Found异常。同时,Java 代码中的所有引用在relocation后都使用被修改后的包名。实现Shaded打包的方式主要是使用
maven-shade-plugin
插件,此外还有Google的jarjar.jar
。举个Chunjun的例子
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.dtstack.chunjun.Main</mainClass> </transformer> </transformers> <artifactSet> <includes> <!--only include slf4j api --> <include>org.slf4j:slf4j-api</include> <include>com.google.guava:*</include> <include>com.google.code.gson:*</include> <include>org.apache.httpcomponents:*</include> <include>io.prometheus:*</include> <include>org.apache.avro:*</include> <include>com.fasterxml.jackson.core:*</include> <include>commons-*:*</include> </includes> </artifactSet> <relocations> <relocation> <pattern>com.google.common</pattern> <shadedPattern>shade.core.com.google.common</shadedPattern> </relocation> <relocation> <pattern>com.google.thirdparty</pattern> <shadedPattern>shade.core.com.google.thirdparty</shadedPattern> </relocation> <relocation> <pattern>org.apache.http</pattern> <shadedPattern>shade.core.org.apache.http</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>
其中
ManifestResourceTransformer
用于指定MANIFEST.MF
的mainClass,使Jar包成为一个可执行Jar包。includes
指定打包依赖的白名单,仅有白名单内的依赖会被打进FatJar。relocations
标签将类进行重命名,打包之后重命名的目录如下。└── shade └── core ├── com │ └── google │ ├── common │ └── thirdparty └── org └── apache └── http
-
嵌套方法(Jar of Jars)
Jar of Jars
也称nested-jars
。用这个方法可以直接将依赖jar打进jar包中,可以避免同名不同版本的依赖被覆盖。但是这个方法不被 JVM 原生支持,因为 JDK 提供的 ClassLoader 仅支持装载嵌套 jar 包的 class 文件。所以这种方法需要自定义 ClassLoader 以支持嵌套 jar。实现的打包插件是spring-boot-maven-plugin
,它会把所有依赖的jar包放进BOOT-INF/lib
下,并使用自定义的ClassLoaderJarLauncher
和PropertiesLauncher
进行加载。举个ruoyi的例子。打包配置比较简单
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
存在同名不同版本的依赖:
打包结果
常用打包插件使用方法
maven-assembly-plugin
maven-shade-plugin
spring-boot-maven-plugin
MANIFEST.MF 的 Main-Class
- JarLauncher : loader.path失效
- PropertiesLauncher
maven-dependency-plugin
maven-jar-plugin
maven-surefire-plugin
完整案例
flink
springboot
参考1
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.keqing</groupId>
<artifactId>kafka3</artifactId>
<version>0.0.1</version>
<name>kafka3</name>
<packaging>jar</packaging>
<build>
<plugins>
<!--
官方解释:These are miscellaneous tools available through Maven by default.
Dependency manipulation (copy, unpack) and analysis.
这个插件的作用:把第三方依赖包复制到target/lib/目录下,达到分离的目的。
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<!-- 复制第三方 jar 到项目目录下的 target/lib/ 下 -->
<execution>
<goals>
<!--
takes the list of project direct dependencies and optionally transitive
dependencies and copies them to a specified location, stripping the version
if desired.
This goal can also be run from the command line.
-->
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<excludeScope>provided</excludeScope>
<!-- 配置的作用:跳过复制第三方依赖这一步。这是在首次上传
第三方依赖到服务器之后,启用这个选项,可以不用在打包时
重复复制,节省时间。-->
<skip>false</skip>
</configuration>
</execution>
</executions>
</plugin>
<!--
官方解释:These plugins relate to packaging respective artifact types.
Build a JAR from the current project.
这个插件的作用:把项目打成jar包,插件配置的意思是:把第三方依赖的路径,
写入到MANIFEST.MF 文件中,格式是./lib/xxx.jar。这样做,就是 让项目包在
运行的时候,能够像以前一样找到第三方依赖包。
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<!-- 指定 Spring Boot 启动类,实际测试中必须 -->
<!-- <mainClass>com.keqing.Kafka3Application</mainClass>-->
<!-- 将所有第三方 jar 添加到项目 jar 的 MANIFEST.MF 文件中,这样运行 jar 时依赖包才能
被加载 。此为关键步骤,有了这一步,我们在把第三方依赖包与项目包分离的情况
下,在服务器运行,就和没有分离时, 是一摸一样的了。-->
<addClasspath>true</addClasspath>
<!-- 指定第三方 jar 的目标目录为 ./lib/-->
<classpathPrefix>./lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<!--
上面的插件,只是把项目包打成符合maven的标准格式,还有利用spring boot的插件,
把包打成符合spring boot的格式才行。
官方文档:https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/
为了让用户方便使用 Maven,少进行配置甚至不用配置,就需要用 Maven 构建项目。Maven 在安
装好后,自动为生命周期的主要阶段绑定很多插件的目标。
-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- repackage 时排除掉 第三方依赖 jar 文件,我们的可运行 Spring Boot 的 jar 文件瞬间变小 ^_^
下面的配置给出了怎样将生命周期的阶段与插件的目标相互绑定。这样,在执行mvn命令时,会自
动执行 这个插件的目标。
目标可以有一个默认的阶段绑定,我们将在下面讨论。
目标有一个默认的阶段绑定,然后它将在该阶段执行。但是,如果目标没有绑定到任何生命周期阶
段, 那么它就不会在构建生命周期中执行。
官方文档:https://maven.apache.org/guides/mini/guide-configuring-plugins.html
-->
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<!--配置重新打包时,要包含的第三方依赖包,配置为nothing,那么就会排除掉所有的第三方依赖
包-->
<configuration>
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
docker 部署
参考
Spring Boot 解析系列(二):FatJar 启动原理
Apache Maven Assembly Plugin 官方文档
Apache Maven Shade Plugin 官方文档