一、Spring Boot Layered Jar优化Docker构建
Spring Boot Layered Jar 是一种特殊的 Jar 包格式,由 Spring Boot 2.3 及以上版本提供。这种格式主要是为了优化 Docker 镜像的构建和部署过程。以下是关于 Spring Boot Layered Jar 的详细解释:
1. 分层结构
- 背景:传统的 Spring Boot Jar 包是一个包含所有依赖、资源和应用程序代码的“胖”Jar(fat Jar)。这种结构在每次代码变更时都需要重新构建和上传整个 Jar 包,这会导致存储和时间的浪费,特别是在网络速度较慢的情况下。
- Layered Jar:为了解决这个问题,Spring Boot 引入了 Layered Jar 的概念。Layered Jar 将应用程序的不同部分(如依赖库、Spring Boot 加载器、应用程序代码等)分隔到不同的层中。这样,当应用程序代码发生变化时,只需要重新构建和上传相应的层,而不是整个 Jar 包。
2. 如何使用
- 配置:在 Maven 项目中,要启用 Layered Jar,只需在
pom.xml
文件中为spring-boot-maven-plugin
插件添加<layers><enabled>true</enabled></layers>
配置。 - 打包:启用 Layered Jar 后,使用 Maven 打包命令(如
mvn package
)将生成一个包含多个层的 Jar 文件。这个文件可以使用java -jar
命令直接运行,就像传统的 Jar 包一样。 - Docker 镜像构建:当使用 Docker 构建镜像时,Layered Jar 的优势更加明显。由于 Docker 镜像也是分层的,因此可以利用 Layered Jar 的分层特性来优化镜像的构建过程。这样,当应用程序代码发生变化时,只需要重新构建包含代码变更的层,而不是整个镜像。
3. 优势
- 提高构建效率:由于只需要重新构建和上传变更的层,因此可以大大减少构建和部署时间。
- 节省存储空间:分层结构避免了每次代码变更都需要重新上传整个 Jar 包的情况,从而节省了存储空间。
- 更好的缓存利用:Docker 可以更有效地利用缓存机制,因为未变更的层可以从缓存中直接获取,而不需要重新构建。
4. 注意事项
- 兼容性:Layered Jar 需要 Spring Boot 2.3 及以上版本才支持。同时,为了充分利用 Layered Jar 的优势,建议使用较新版本的 Docker 和相关的构建工具。
- 配置细节:虽然启用 Layered Jar 只需要简单的配置更改,但为了获得最佳效果,可能需要根据项目的具体情况调整配置参数(如层的划分方式等)。
总的来说,Spring Boot Layered Jar 是一种优化 Docker 镜像构建和部署过程的解决方案。它通过分层结构减少了每次代码变更时需要重新构建和上传的数据量,从而提高了构建效率并节省了存储空间。
二、什么是 Layered Jar or War
一个 repackaged 的jar文件在BOOT-INF/classes
和BOOT-INF/lib
中分别包含应用程序的类和依赖项。同样,一个可执行的war文件在WEB-INF/classes
中包含应用程序的类,在WEB-INF/lib和WEB-INF/lib-provided
中包含依赖项。对于那些需要从jar或war的内容构建Docker镜像的情况,能够进一步分离这些目录以便将它们写入不同的层是很有用的。
分层归档使用与普通重新打包的jar或war相同的布局,但包括一个描述每一层的附加元数据文件(layers.idx
)。
默认情况下,定义了以下层:
dependencies
:用于不包含SNAPSHOT
版本的任何依赖项。spring-boot-loader
:用于加载器类。snapshot-dependencies
:用于包含SNAPSHOT
版本的任何依赖项。application
:用于本地模块依赖项、应用程序类和资源。
模块依赖项是通过查看当前构建的所有模块来识别的。如果模块依赖项只能因为已经安装到Maven的本地缓存中而得到解析,并且它不是当前构建的一部分,那么它将被识别为常规依赖项。
层的顺序很重要,因为它决定了当应用程序的某一部分发生变化时,之前的层被缓存的可能性有多大。默认顺序是dependencies
、spring-boot-loader
、snapshot-dependencies
、application
。最不可能改变的内容应该首先添加,其次是更有可能改变的层。
默认情况下,打包的jar文件中包括layers.idx
文件。要禁用此功能,您可以按以下方式进行操作:
<project><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><layers><enabled>false</enabled></layers></configuration></plugin></plugins></build>
</project>
通过将这些内容写入不同的层,可以更有效地利用Docker的缓存机制,从而提高构建效率。
三、自定义分层配置
根据您的应用程序需求,您可能想要调整如何创建层并添加新层。这可以通过使用一个单独的配置文件来完成,该文件的注册方式如下所示:
<project><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><layers><enabled>true</enabled><configuration>${project.basedir}/src/layers.xml</configuration></layers></configuration></plugin></plugins></build>
</project>
配置文件描述了如何将归档文件分离成不同的层,以及这些层的顺序。以下示例展示了如何明确定义上述默认顺序:
<layers xmlns="http://www.springframework.org/schema/boot/layers"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/boot/layershttps://www.springframework.org/schema/boot/layers/layers-3.2.xsd"><application><into layer="spring-boot-loader"><include>org/springframework/boot/loader/**</include></into><into layer="application" /></application><dependencies><into layer="application"><includeModuleDependencies /></into><into layer="snapshot-dependencies"><include>*:*:*SNAPSHOT</include></into><into layer="dependencies" /></dependencies><layerOrder><layer>dependencies</layer><layer>spring-boot-loader</layer><layer>snapshot-dependencies</layer><layer>application</layer></layerOrder>
</layers>
layers XML格式在三个部分中定义:
-
<application>
块定义了如何将应用程序类和资源进行分层。 -
<dependencies>
块定义了如何将依赖项进行分层。 -
<layerOrder>
块定义了层应该写入的顺序。
在<application>
和<dependencies>
部分中,嵌套的<into>
块用于为层声明内容。这些块按照从上到下的定义顺序进行评估。任何未被前面的块声明的内容仍然可供后续块考虑。
<into>
块使用嵌套的<include>
和<exclude>
元素来声明内容。<application>
部分使用Ant风格的路径匹配来定义包含/排除表达式。<dependencies>
部分使用group:artifact[:version]
模式。它还提供了<includeModuleDependencies />
和<excludeModuleDependencies />
元素,这些元素可用于包含或排除本地模块依赖项。
如果没有定义<include>
,那么所有内容(未被先前的块声明的内容)都会被考虑进去。
如果没有定义<exclude>
,那么就不会应用任何排除规则。
从上面的<dependencies>
示例中,我们可以看到,第一个<into>
将为application.layer
声明所有的模块依赖项。下一个<into>
将为snapshot-dependencies
层声明所有的SNAPSHOT依赖项。最后一个<into>
将声明剩下的任何内容(在这种情况下,是指任何非SNAPSHOT的依赖项)为dependencies
层。
<application>
块有类似的规则。首先为spring-boot-loader
层声明org/springframework/boot/loader/**
的内容。然后为application
层声明任何剩余的类和资源。
注意:<into>
块的定义顺序通常与层的写入顺序不同。因此,必须始终包含<layerOrder>
元素,并且必须涵盖所有由<into>
块引用的层。
四、在Dockerfile中使用layer构建镜像
1、如何在Dockerfile中使用
我们将使用分层特性来创建一个优化的Docker镜像。当您创建一个包含层索引文件的jar包时,spring-boot-jarmode-layertools jar
将被添加为您jar的依赖项。只要该jar在类路径上,您就可以在特殊模式下启动应用程序,该模式允许引导代码运行与您的应用程序完全不同的东西,例如,提取层的东西。
layertools
模式不能与包含启动脚本的完全可执行的Spring Boot归档文件一起使用。在构建打算与layertools
一起使用的jar文件时,应禁用启动脚本配置。
以下是如何以layertools
jar模式启动jar包:
$ java -Djarmode=layertools -jar my-app.jar
这将提供以下输出:
使用方法:java -Djarmode=layertools -jar my-app.jar可用命令:list 列出可从jar中提取的层extract 从jar中提取层以创建镜像help 有关任何命令的帮助
extract
命令可用于轻松将应用程序拆分为要添加到Dockerfile的层。
以下是一个使用jarmode
的Dockerfile示例。
FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extractFROM eclipse-temurin:17-jre
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
假设上述Dockerfile位于当前目录中,您的Docker镜像可以使用docker build命令构建,或者选择指定应用程序jar的路径,如下例所示:
$ docker build --build-arg JAR_FILE=path/to/myapp.jar .
这是一个多阶段的Dockerfile。builder阶段提取稍后需要的目录。每个COPY
命令都与jarmode
提取的层相关。
当然,Dockerfile可以在不使用jarmode
的情况下编写。您可以使用unzip
和mv
的某种组合将内容移动到正确的层,但jarmode
简化了这一过程。
2、构建步骤说明
第一阶段:builder
FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM eclipse-temurin:17-jre as builder
:这行代码开始了第一阶段,命名为builder
。它使用了一个包含 Java 17 JRE 的基础镜像eclipse-temurin:17-jre
。WORKDIR application
:设置工作目录为application
。ARG JAR_FILE=target/*.jar
:定义了一个参数JAR_FILE
,默认值为target/*.jar
,这意味着它会尝试复制target
目录下的任何 JAR 文件。COPY ${JAR_FILE} application.jar
:将构建好的 JAR 文件复制到镜像的application.jar
。RUN java -Djarmode=layertools -jar application.jar extract
:这行命令使用 Spring Boot 的layertools
功能来解压 JAR 文件的不同层。允许将 JAR 文件分解为不同的层,以便于在 Docker 镜像中更有效地缓存和复用这些层。
第二阶段:生产镜像
FROM eclipse-temurin:17-jre
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
FROM eclipse-temurin:17-jre
:这行代码开始了第二阶段(也是最终的生产镜像阶段),它同样使用了eclipse-temurin:17-jre
作为基础镜像。WORKDIR application
:再次设置工作目录为application
。- 接下来的四个
COPY
指令都从builder
阶段复制了之前通过layertools extract
分解出来的不同层到当前阶段。这样做的好处是,如果这些层在未来的构建中没有变化,Docker 可以利用缓存机制,避免重复构建这些层,从而提高构建效率。COPY --from=builder application/dependencies/ ./
:复制依赖项层。COPY --from=builder application/spring-boot-loader/ ./
:复制 Spring Boot 加载器层。COPY --from=builder application/snapshot-dependencies/ ./
:如果有快照依赖项,则复制它们。COPY --from=builder application/application/ ./
:复制应用程序本身的文件和资源。
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
:设置容器的入口点,以便在容器启动时运行 Spring Boot 应用程序。
通过这种方式,Dockerfile 利用了多阶段构建和 Spring Boot 的层工具来创建一个优化了大小和构建时间的 Docker 镜像。
参考
- packaging.layers
- container-images