zanyrain

zanyrain

github
twitter_id

GraalVM を使用して Java ネイティブイメージを構築する

最近、小さな要求があり、毎日定時にクラスのグループに大学入試カウントダウン画像を投稿することになりました。無駄に試してみるという方針で、GraalVM の使用を試してみることにしました。

プロジェクトコードは Java で書かれており、まず jar にパッケージ化し、その後 GraalVM Native Image を使用してネイティブイメージを生成します。全体の機能は非常にシンプルで、まず画像を生成し、その後企業微信の Webhook アドレスを通じて送信します。

1. ネイティブイメージの構築プロセス#

プロジェクトは無害な JSON ライブラリに依存しています:

<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20230227</version>
</dependency>

主なコードは次のようになります。

package com.example;

import org.json.JSONObject;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.Month;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Base64;

public class App 
{
    public static void main( String[] args ) throws IOException, FontFormatException, NoSuchAlgorithmException, URISyntaxException, InterruptedException {
        LocalDate gkDate = LocalDate.of(2024, Month.JUNE, 7);
        LocalDate now = LocalDate.now(ZoneId.of("Asia/Shanghai"));
        long between = ChronoUnit.DAYS.between(now, gkDate);
        if (between < 0) {
            return;
        }
        String word1 = String.valueOf(between);
        String word2 = "日";
        System.out.println("現在の時間" + now + "から大学入試まで" + between + "日");
        InputStream bgImageFile = App.class.getClassLoader().getResourceAsStream("bg.png");
        InputStream fontFile = App.class.getClassLoader().getResourceAsStream("LXGWWenKaiLite-Bold.ttf");
        BufferedImage image = ImageIO.read(bgImageFile);
        Graphics graphics = image.getGraphics();
        Font font0 = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(Font.BOLD,250);
        Font font1 = font0.deriveFont(Font.BOLD,90);
        graphics.setColor(Color.RED);
        drawCenteredString(graphics,word1,new Rectangle(246,956,578,308),font0);
        graphics.setColor(Color.BLACK);
        drawCenteredString(graphics,word2,new Rectangle(246,1236,578,150),font1);
        graphics.dispose();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ImageIO.write(image,"png",baos);
        image.flush();
        baos.flush();
        byte[] byteArray = baos.toByteArray();
        baos.close();
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        md5.update(byteArray);
        byte[] digest = md5.digest();
        String md5str = encodeHexString(digest);
        Base64.Encoder encoder = Base64.getEncoder();
        String s1 = encoder.encodeToString(byteArray);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("msgtype","image");
        JSONObject imageJson = new JSONObject();
        imageJson.put("base64",s1);
        imageJson.put("md5",md5str);
        jsonObject.put("image",imageJson);
        HttpClient httpClient = HttpClient.newBuilder().build();
        HttpRequest httpRequest = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(jsonObject.toString()))
                .header("Content-Type", "application/json")
                .uri(new URI("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=put-your-key-here")).build();
        HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
        System.out.println("サービスからの応答" + httpResponse.body());
    }


    /**
     * Rectangleの中央に文字列を描画します。
     *
     * @param g Graphicsインスタンス。
     * @param text 描画する文字列。
     * @param rect 文字列を中央に配置するためのRectangle。
     */
    public static void drawCenteredString(Graphics g, String text, Rectangle rect, Font font) {
        // フォントメトリクスを取得
        FontMetrics metrics = g.getFontMetrics(font);
        // 文字列のX座標を決定
        int x = rect.x + (rect.width - metrics.stringWidth(text)) / 2;
        // 文字列のY座標を決定(注意:上部が画面の0であるため、アセントを追加します)
        int y = rect.y + ((rect.height - metrics.getHeight()) / 2) + metrics.getAscent();
        // フォントを設定
        g.setFont(font);
        // 文字列を描画
        g.drawString(text, x, y);
    }

    public static String encodeHexString(byte[] byteArray) {
        StringBuffer hexStringBuffer = new StringBuffer();
        for (int i = 0; i < byteArray.length; i++) {
            hexStringBuffer.append(byteToHex(byteArray[i]));
        }
        return hexStringBuffer.toString();
    }

    public static String byteToHex(byte num) {
        char[] hexDigits = new char[2];
        hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
        hexDigits[1] = Character.forDigit((num & 0xF), 16);
        return new String(hexDigits);
    }
}

Jar パッケージを作成するための Maven 設定項目を追加します。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>
                            com.example.App
                        </mainClass>
                    </manifest>
                </archive>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
            </configuration>
        </execution>
    </executions>
</plugin>

これでネイティブイメージを作成できるはずですか?いいえ!
GraalVM Native Image はリフレクション、JNI、リソースアクセスなどのメカニズムの使用に制限があり、上記のコードではgetResourceAsStreamを使用し、デスクトップモジュールに属する ImageIO クラスを参照しているため、JNI の使用を避けることはできません。ここで追加の設定ファイルを追加する必要があります:

resources
├── bg.png
├── LXGWWenKaiLite-Bold.ttf
└── META-INF
    └── native-image
        └── com
            └── example
                ├── jni-config.json
                ├── native-image.properties
                ├── reflect-config.json
                └── resource-config.json

native-image.properties ファイルは、ビルド時に必要なパラメータを提供します。

Args = -H:ResourceConfigurationResources=${.}/resource-config.json -H:JNIConfigurationResources=${.}/jni-config.json -H:ReflectionConfigurationResources=${.}/reflect-config.json

以下のコマンドを使用して、GraalVM が残りの 3 つの設定ファイルを生成するのを手伝うことができます。

 /opt/bellsoft/liberica-vm-23.0.1-openjdk17/bin/java -agentlib:native-image-agent=config-output-dir=./native-image/                                                          
  -jar gkdjs-service-1.0-SNAPSHOT-jar-with-dependencies.jar

整理が完了したら、パッケージ化を続けることができます。

/opt/bellsoft/liberica-vm-23.0.1-openjdk17/bin/native-image -jar ./gkdjs-service-1.0-SNAPSHOT-jar-with-dependencies.jar --enable-http --enable-https --no-fallback

最終的に生成された内容は次のとおりです:

gkdjs-service-1.0-SNAPSHOT-jar-with-dependencies  実行可能ファイル
libawt_headless.so
libawt.so
libawt_xawt.so
libfontmanager.so
libfreetype.so
libjavajpeg.so
libjava.so
libjvm.so
liblcms.so

最初のファイルを直接実行すれば大丈夫です。実行可能ファイルは 50MB 以上ありますが、スペースが時間に変わることを願っています。

2. イメージのパッケージ化と公開#

Dockerfile を作成します。

FROM debian:bookworm-slim
WORKDIR app
RUN apt update && apt install -y xvfb
RUN apt install -y fontconfig

COPY target/gkdjs-service-1.0-SNAPSHOT-jar-with-dependencies app
COPY target/lib*.so ./
ENTRYPOINT /app/app

デスクトップやフォント関連の内容を使用しない場合は、xvfb や fontconfig は必要ありません。その後、イメージを生成して直接実行できます。
定期的に実行する必要があることに注意してください。loginctl enable-linger $userを使用すると、現在のユーザーが常にログインした状態になります。そうしないと、crontab を終了すると実行されなくなります。

これは私が XLog に投稿した最初のブログ記事です。皆さんに気に入っていただけると嬉しいです。💖

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。