SpringBoot-高效打包Images

SpringBoot-高效打包Images

介绍

虽然将一个Jar打包为镜像很容易,只需一个基础JRE镜像就可生成一个SpringBoot项目镜像。
但这种方式创建出的镜像过于臃肿,升级下一个版本仍需重新将Jar打包到镜像中。

我们知道Docker镜像是分层打包的,所以我们可以将一些基础内容封装到基镜像中,之后更新部分用另一层处理。

解析SpringBoot Jar

一般情况下,我们运行SpringBoot项目就是打一个Jar包并通过java -jar *.jar指令去运行。
但其实我们还可以将Jar包解压后再运行,这样的话方便我们对项目进行热部署和分层打包。

我们来打一个样例Jar来看看里面的内容,只要一个基础的接口功能即可:

qz-demo.jar
├─BOOT-INF
│  ├─classes
│  │  └─com
│  │      └─example
│  │          └─qz
│  │              └─config
│  └─lib
├─META-INF
│  └─maven
│      └─com.example
│          └─qz
└─org
    └─springframework
        └─boot
            └─loader
                ├─archive
                ├─data
                ├─jar
                ├─jarmode
                └─util

解压后是上述样子,我们可以看一下 BOOT-INF 是我们真正的项目代码、
META-INF 是一些源信息、剩下的就是SpringBoot编译出来的基础文件。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.loader;

import org.springframework.boot.loader.archive.Archive;

public class JarLauncher extends ExecutableArchiveLauncher {
    static final Archive.EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
        return entry.isDirectory() ? entry.getName().equals("BOOT-INF/classes/") : entry.getName().startsWith("BOOT-INF/lib/");
    };

    public JarLauncher() {
    }

    protected JarLauncher(Archive archive) {
        super(archive);
    }

    protected boolean isPostProcessingClassPathArchives() {
        return false;
    }

    protected boolean isNestedArchive(Archive.Entry entry) {
        return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
    }

    protected String getArchiveEntryPathPrefix() {
        return "BOOT-INF/";
    }

    public static void main(String[] args) throws Exception {
        (new JarLauncher()).launch(args);
    }
}

org.springframework.boot.loader.JarLauncher这个类中我们可以看到,它是我们程序的入口。
也就是我们通过java -jar执行的入口。不难看出这里定义了代码与依赖的文件位置。

打开BOOT-INF可以看到有两个*.idx文件,classpath.idx文件中定义了所有依赖的 lib :

- "BOOT-INF/lib/lombok-1.18.24.jar"
- "BOOT-INF/lib/spring-boot-2.7.1.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-2.7.1.jar"
- "BOOT-INF/lib/logback-classic-1.2.11.jar"
- "BOOT-INF/lib/logback-core-1.2.11.jar"
- "BOOT-INF/lib/log4j-to-slf4j-2.17.2.jar"
- "BOOT-INF/lib/log4j-api-2.17.2.jar"
- "BOOT-INF/lib/jul-to-slf4j-1.7.36.jar"
- "BOOT-INF/lib/jakarta.annotation-api-1.3.5.jar"
- "BOOT-INF/lib/snakeyaml-1.30.jar"
- "BOOT-INF/lib/spring-context-support-5.3.21.jar"
- "BOOT-INF/lib/spring-beans-5.3.21.jar"
- "BOOT-INF/lib/spring-context-5.3.21.jar"
- "BOOT-INF/lib/spring-tx-5.3.21.jar"
- "BOOT-INF/lib/quartz-2.3.2.jar"
- "BOOT-INF/lib/mchange-commons-java-0.2.15.jar"
- "BOOT-INF/lib/slf4j-api-1.7.36.jar"
- "BOOT-INF/lib/jackson-databind-2.13.3.jar"
- "BOOT-INF/lib/jackson-annotations-2.13.3.jar"
- "BOOT-INF/lib/jackson-core-2.13.3.jar"
- "BOOT-INF/lib/jackson-datatype-jdk8-2.13.3.jar"
- "BOOT-INF/lib/jackson-datatype-jsr310-2.13.3.jar"
- "BOOT-INF/lib/jackson-module-parameter-names-2.13.3.jar"
- "BOOT-INF/lib/tomcat-embed-core-9.0.64.jar"
- "BOOT-INF/lib/tomcat-embed-el-9.0.64.jar"
- "BOOT-INF/lib/tomcat-embed-websocket-9.0.64.jar"
- "BOOT-INF/lib/spring-web-5.3.21.jar"
- "BOOT-INF/lib/spring-webmvc-5.3.21.jar"
- "BOOT-INF/lib/spring-aop-5.3.21.jar"
- "BOOT-INF/lib/spring-expression-5.3.21.jar"
- "BOOT-INF/lib/spring-core-5.3.21.jar"
- "BOOT-INF/lib/spring-jcl-5.3.21.jar"
- "BOOT-INF/lib/spring-boot-jarmode-layertools-2.7.1.jar"

可以看到这些都是我们初始创建SpringBoot项目所依赖的基础Jar。
layers.idx文件中定义了项目的一些依赖信息:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

剩下的两个文件相信都很熟悉,这里我就不在赘述。

Java提供了解压Jar包的功能,我们可以用jar -xf *.jar来解压Jar包。
并通过java org.springframework.boot.loader.JarLauncher的方式来运行我们的项目。
这样的运行方式,相对与Jar的运行要快上一些,当然也方便我们以后做热部署之类的功能。

当然除了上述的启动方式外,你还可以直接运行自己写好的SpringBootApplication类来启动。
java -cp BOOT-INF/classes:BOOT-INF/lib/* com.example.qz.SpringbootQzDemoApplication像这样。

当然用 JarLauncher 运行具有可预测的类路径顺序的好处。
Jar 包含一个 classpath.idx 文件,JarLauncher 在构建类路径时使用该文件。

分层打包Image

为了更容易创建优化的 Docker 镜像,Spring Boot 支持向 Jar 中添加层索引文件。
它提供了层列表和应包含在其中的 Jar 的部分。索引中的层列表是根据应将层添加到 Docker/OCI 映像的顺序进行排序的。
也就是之前提到的layers.idx这个文件。

FROM eclipse-temurin:11-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:11-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.JarLauncher"]

假设Dockerfile的路径为项目根路径,我们可以通过docker build .来开始编译过程。

或者使用docker build --build-arg JAR_FILE=path/to/myapp.jar .来编译也是可以的


SpringBoot-高效打包Images
https://blog.cikaros.top/doc/83b87764.html
作者
Cikaros
发布于
2022年8月17日
许可协议