1087 字
5 分钟
Spring Boot3 本机镜像构建

简单玩玩 Java 构建本机镜像,像 Go 一样直接启动

介绍#

我们构建 Java 项目的构件产出通常是 jar 包,运行它需要使用 JRE 环境。

jar 包的优势很明显,跨平台,放哪个平台上都可以跑,很方便。但是对于内存的消耗会大一些。而 Graalvm 可以把 Jar 包编译为本机可执行文件,不再需要 JRE 运行环境,启动很快,内存占用也会少一点。

年初时候我尝试把生产项目改成本机模式,发现复杂度太高了,很难在编译时候让编译器识别到全部需要的依赖。Java的反射和接口一类用的太多了。遂放弃了。

最近用到了一个简单的 Spring boot项目 simple-boot-douban-api 发现可以玩一玩。

升级 Spring Boot 3#

在检出项目后,修改项目 pom 配置,把 spring-boot-starter-parent 升级到了 3.3.2

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

修改 JDK 使用 17 版本, jsoup也跟着升级一下。

<properties>
        <java.version>17</java.version>
        <jsoup.version>1.18.1</jsoup.version>
</properties>

Spring boot 2 ==> 3 的升级,Java EE 变成了 Jakarta EE 包路径基本都发生了改变 比如 javax.servlet ==> jakarta.servlet

使用 IDEA 可以在使用重构进行迁移

79ax4t

搞完之后,mvn clean install 后,测试启动项目,ok

增加本机构建#

还是在 pom 文件中,增加 native 相关配置。

    <profiles>
        <profile>
            <id>native</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.graalvm.buildtools</groupId>
                        <artifactId>native-maven-plugin</artifactId>
                        <executions>
                            <execution>
                                <id>build-native</id>
                                <goals>
                                    <goal>compile-no-fork</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

本地要安装 graalvm jdk17 环境,并设置为JAVA_HOME。之后便可以尝试构建了。 命令 mvn -Pnative -DskipTests=true native:compile

在经历一段时间后,会在target目录中生成 simple-boot-douban-api 文件, ./simple-boot-douban-api 直接可以启动。

尝试调用,出现报错 找不到 caffeine cache相关的类。网上搜索一下,有给出解决方案的,使用 spring 提供的接口,把依赖的类型注册进去。测试了一下挺麻烦的,也没解决问题。spring core本身提供的内建的缓存,直接用也行。遂移除依赖 spring-boot-starter-cachecaffeine ,重新构建,功能OK。

  1. 对比 simple-boot-douban-api.jar 20MB的体积,simple-boot-douban-api 大小涨到 70MB,使用 upx 处理后,大约 30MB。体积还行。
  2. 启动速度上 jar包是 1.004,本机启动 0.07,快了不少
  3. 内存占用大约少了1/3

设备 Macbook pro M3 max

Docker镜像构建#

目前我需要构建 x64 和 arm64 的镜像,使用docker的buildx 拉起两个环境,分别构建。

示例 Dockerfile

# Start with base image
FROM vegardit/graalvm-maven:latest-java17 AS build
WORKDIR /app
COPY ./ /app
RUN --mount=type=cache,id=maven,target=/mvn/store mvn -Dmaven.repo.local=/mvn/store -Pnative -DskipTests=true clean  native:compile
RUN upx /app/target/simple-boot-douban-api
# Start with base image
FROM debian:bookworm-slim

WORKDIR /app
# Add Maintainer Info
LABEL maintainer="jianyun8023"

# Add a temporary volume
VOLUME /tmp

# Expose Port 8085
EXPOSE 8085

ENV DOUBAN_CONCURRENCY_SIZE="5"
ENV DOUBAN_BOOK_CACHE_SIZE="1000"
ENV DOUBAN_BOOK_CACHE_EXPIRE="24h"
ENV DOUBAN_PROXY_IMAGE_URL="true"

# Add Application Jar File to the Container
COPY --from=build /app/target/simple-boot-douban-api simple-boot-douban-api

# Run the JAR file
ENTRYPOINT ["bash","-c","/app/simple-boot-douban-api"]

Github Action 配置

name: Build simple-boot-douban-api Images
on:
  workflow_dispatch:
  release:
    types: [published]
env:
  IMAGE_NAME: simple-boot-douban-api
  VERSION: '0.0.1'

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Set VERSION for release
        if: github.event_name == 'release'
        run: echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV

      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up Java
        uses: actions/setup-java@v3
        with:
          distribution: 'adopt'
          java-version: '17'

      - name: Create Maven repository directory
        run: |
          mkdir -p ~/.m2/repository  
          chmod 755 ~/.m2/repository

      - name: Cache Maven packages
        uses: actions/cache@v4
        id: cache
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: |
            ${{ runner.os }}-maven-

      - name: inject Maven packages cache into docker
        uses: reproducible-containers/[email protected]
        with:
          cache-map: |
            {  
              "~/.m2/repository": "/mvn/store"  
            }
          skip-extraction: ${{ steps.cache.outputs.cache-hit }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      - name: Log in to registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: ./
          platforms: linux/amd64,linux/arm64
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ env.VERSION }}
          push: true
          tags: |
            ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}  
            ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest

镜像信息 hZECuA

个人感受#

使用 GraalVM 可以构建出 Java 的本机镜像,可以降低资源的使用量,提高启动速度。但是它带来的繁琐度和维护成本增加,个人感觉没太大吸引力。使用 Go 做这件事它不香吗?

对企业来说,维护难度低比少点内存,少占一点资源更有吸引力。至于启动速度,我想一分钟以内的差距应该问题不大。

对我而言,非常简单的项目可能会玩一下,如果我写这个项目,我会换成 Go 实现。复杂项目使用它太麻烦了。当然,构建本机镜像可以做到像Go那像简单,我会很乐意使用的。

参考资料#

  1. simple-boot-douban-api: https://github.com/fugary/simple-boot-douban-api
  2. Native Images with Spring Boot and GraalVM: https://www.baeldung.com/spring-native-intro
Spring Boot3 本机镜像构建
https://www.jianyun.run/posts/spring-boot-native-image-build/
作者
唐长老日志
发布于
2024-08-20
许可协议
CC BY-NC-SA 4.0