使用JLink为Spring Boot创建较小Docker镜像


在这篇博文中,我们将深入探讨如何利用 JLink 优化 Docker 镜像大小,从而增强应用程序安全性和性能。我们将展示如何使用 JLink 并将其与 Docker 集成,以高效部署 Spring Boot 或通用 Java 应用程序。 

在 Docker 容器中部署 Java 应用程序时,开发人员经常会遇到 Docker 镜像的大小问题。解决这个问题的方法之一是使用JLink,这是JDK 9中引入的工具。

JLink(Java Linker)是一个命令行工具,它将一组模块及其依赖项组装并优化为自定义运行时镜像。这本质上意味着它创建了一个最小的 Java 运行时环境,仅包含应用程序所需的必要模块。

$ jlink --module-path $JAVA_HOME/jmods:mlib --add-modules my.module --output myRunTime

在上面的命令中,my.module是您的模块,myRuntime是 JLink 将创建的自定义运行时镜像。 

JLink 在创建更小的 Docker 镜像中的作用
在为 Java 应用程序创建 Docker 镜像时,镜像的大小通常是一个问题——特别是对于带有许多依赖项的 Spring Boot 应用程序。大型 Docker 镜像可能会导致启动时间更长、存储成本增加以及部署过程变慢。

在 Java 的旧版本中,Java 开发工具包 (JDK) 附带了 Java 运行时环境 (JRE)。只需要 JRE 即可运行创建的 Java 工件。因此,过去,通常在 Docker 镜像中使用 JRE 或为容器选择 JRE 基础镜像。较新版本的 Java 并不总是附带 JRE,尽管某些供应商可能仍会创建 JRE 和相应的基础镜像。您可以使用这些或更适合您的应用程序的特定 Java 运行时。

JLink 使您能够创建仅包含必要模块的最小 Java 运行时。通过这样做,它可以显着减小 Docker 镜像的大小。例如,标准 Java 运行时环境可能超过 200 MB,但使用 JLink,您可以将其降至 50 MB 以下。

JLink 用于 Spring Boot 
Spring Boot 为您的应用程序创建一个包含所有依赖项的 fat JAR。此外,许多 Spring Boot 应用程序缺少模块声明。这不一定是问题,但我们需要确定应用程序需要哪些模块及其所有依赖项。 

1、使用Jdeps查找模块
Jdeps 是一个显示包级或类级依赖关系的 Java 工具。该工具在 Java 8 中引入,可用于了解应用程序的依赖关系,然后可用于使用 JLink 创建自定义运行时镜像。
当确保所有依赖项都位于一个目录中时,我们可以使用 jdeps 打印依赖项的摘要。

jdeps -cp 'mydeps/lib/*' -recursive --multi-release 17 -s target/MyJar.jar

同样,我们可以使用 jdeps 递归打印 Spring Boot 应用程序和依赖项的所有模块依赖项。

jdeps --ignore-missing-deps -q  --recursive  --multi-release 17  --print-module-deps  --class-path 'mydeps/lib/*'  target/MyJar.jar

jdeps 生成的输出使 JLink 能够创建一个仅包含此 Spring-Boot 应用程序所需的模块的 Java 运行时。

将 Spring Boot 依赖项输出到文件夹中
如前所述,Spring Boot 创建一个包含所有依赖项的 fat JAR。然而,依赖项以特定方式打包在 JAR 内,因此 jdeps 不容易访问。有两种简单的解决方案可以获取 jdeps 的依赖关系。

  • 解压 Spring Boot 创建的 fat JAR 文件。 
    • 如果您已经创建了构建工件并且不愿意或无法重建应用程序,则此选项非常有用。依赖项将被解压到/BOOT/libs/.
  • 使用构建工具中的插件将依赖项复制到特定文件夹。
    • 例如,在 Maven 中,这可以通过使用maven-dependency-plugin. 在下面的示例中,/target/dependencyMaven 完成打包阶段后,依赖项将被复制到文件夹中。

<project>
    <!-- ... other configurations ... -->

    <build>
        <plugins>
            <!-- Add the maven-dependency-plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.2</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <!-- Configure the output directory for the dependencies -->
                            <outputDirectory>${project.build.directory}/dependency</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- ... other configurations ... -->
</project>

使用自定义 Java 运行时构建 Docker 
现在,让我们结合 jdeps 和 JLink 来构建自定义 Java 运行时。通过这个运行时,我们可以专门为 Spring Boot 应用程序创建一个完美的、最小的 Docker 镜像。

FROM maven:3-eclipse-temurin-17 as build
RUN mkdir /usr/src/project
COPY . /usr/src/project
WORKDIR /usr/src/project
RUN mvn package -DskipTests
RUN jar xf target/JavaCoffeeShop.jar
RUN jdeps --ignore-missing-deps -q  \
    --recursive  \
    --multi-release 17  \
    --print-module-deps  \
    --class-path 'BOOT-INF/lib/*'  \
    target/JavaCoffeeShop.jar > deps.info
RUN jlink \
    --add-modules $(cat deps.info) \
    --strip-debug \
    --compress 2 \
    --no-header-files \
    --no-man-pages \
    --output /myjre
FROM debian:bookworm-slim
ENV JAVA_HOME /user/java/jdk17
ENV PATH $JAVA_HOME/bin:$PATH
COPY --from=build /myjre $JAVA_HOME
RUN mkdir /project
COPY --from=build /usr/src/project/target/JavaCoffeeShop.jar /project/
WORKDIR /project
ENTRYPOINT java -jar JavaCoffeeShop.jar

在上面的示例中,我使用了多阶段 Docker 构建。初始构建阶段基于eclipse-temurin包含 Maven 的 JDK 17 映像。该阶段用于:

  • 创建 Java 工件。使用 Maven,我创建了包含完整应用程序的胖可执行 JAR 文件。
  • 解压 JAR 文件以获取所有依赖项。仅当您不使用maven-dependency-plugin前面所述的方法时才需要这样做。如果包含它,则可以跳过此步骤
  • 使用 jdeps 获取必要的模块。指向包含所有依赖 JAR 文件和最终工件的文件,并将列表保存在deps.info.
  • 运行 JLink 以创建自定义 Java 运行时。使用deps.info作为输入并将其存储在/myjre. 我们只添加 JLink 所需的模块并删除调试信息、手册页和头文件。 

第二个也是最后一个阶段基于debian:stable-slim镜像构建生产镜像。

  • 设置环境变量。将 设为JAVA_HOME我要复制到的路径myjre,然后添加JAVA_HOME到PATH.
  • 复制 JLink 创建的 Java 运行时。引用第一阶段并将自定义 Java 运行时复制到定义为 的位置JAVA_HOME。
  • 复制创建的 Java 工件。将创建的胖可执行 Spring Boot JAR 复制到专用项目目录。
  • 设置入口点

在为 Spring Boot Java 应用程序创建 Docker 镜像时,JLink 具有以下几个优势:
  1. 减小镜像大小:如前所述,JLink 可以帮助减小 Docker 镜像的大小,从而加快部署速度并降低存储成本。
  2. 更快的启动时间:更小的 Docker 镜像意味着您的应用程序可以更快地启动,这对于需要快速扩展的应用程序至关重要。
  3. 安全性:通过仅包含必要的模块,可以减少应用程序的攻击面。更少的模块意味着更少的潜在安全漏洞。

说到安全性,必须提到 Snyk 在确保应用程序安全方面的作用。Snyk 是一款开发人员安全工具,可以扫描源代码、开源包、容器映像和云配置中的漏洞。借助 Snyk Container 和 Snyk Open Source,您可以检测并修复应用程序及其依赖项中的安全问题 - 包括 Docker 映像中的安全问题。

$ snyk container test your-repo/your-image:tag

在上面的命令中,your-repo/your-image:tag是您的 Docker 镜像。Snyk 将对其进行扫描并报告任何检测到的漏洞,以及如何修复这些漏洞的建议。

总之
JLink 是一个功能强大的工具,可以帮助您为 Spring Boot Java 应用程序创建更小、更安全的 Docker 映像。与 Snyk 等安全工具相结合,您可以确保您的应用程序高性能且安全。