#chiroito ’s blog

Java を中心とした趣味の技術について

GraalVM の native-imageで PostgreSQL の DataSource が使えない

要約

Java の native-image で PostgreSQL の JDBC ドライバに含まれる DataSource を使うには META-INF/native-image/serialization-config.json が必要です。

org.postgresql.ds.common.BaseDataSource を継承している全ての DataSource が対象になる可能性があります。

問題

以下のような PostgreSQL へ接続するアプリを作って native-image 化しました。

public class NativeImageTest {
    public static void main(String[] args) throws Exception {

        PGPoolingDataSource ds = new PGPoolingDataSource();

        ds.setUrl("jdbc:postgresql://localhost:5432/postgres");
        ds.setUser("postgres");
        ds.setPassword("postgres");

        try(Connection con = ds.getConnection();
            PreparedStatement stmt = con.prepareStatement("SELECT datname FROM pg_database");
            ResultSet rs = stmt.executeQuery()) {

            rs.next();
            System.out.println(rs.getString("datname"));
        }
    }
}

JVM上では問題なく動いたにもかかわらず、native-image では動きません。

Exception in thread "main" org.postgresql.util.PSQLException: Failed to setup DataSource.
        at org.postgresql.ds.PGPoolingDataSource.initialize(PGPoolingDataSource.java:275)
        at org.postgresql.ds.PGPoolingDataSource.getConnection(PGPoolingDataSource.java:331)
        at dev.chiroito.NativeImageTest.main(NativeImageTest.java:16)
Caused by: java.io.InvalidClassException: java.util.Properties; no valid constructor
        at java.base@11.0.19/java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:158)
        at java.base@11.0.19/java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:746)
        at java.base@11.0.19/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2202)
        at java.base@11.0.19/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1687)
        at java.base@11.0.19/java.io.ObjectInputStream.readObject(ObjectInputStream.java:489)
        at java.base@11.0.19/java.io.ObjectInputStream.readObject(ObjectInputStream.java:447)
        at org.postgresql.ds.common.BaseDataSource.readBaseObject(BaseDataSource.java:1582)
        at org.postgresql.ds.common.BaseDataSource.initializeFrom(BaseDataSource.java:1592)
        at org.postgresql.ds.PGPoolingDataSource.initialize(PGPoolingDataSource.java:273)
        ... 3 more

java.util.Properties クラスのデシリアライズができないことが原因です。

解決方法

シリアライズの設定を追加しましょう。シリアライズの設定は META-INF/native-image/serialization-config.json ファイルに記載します。

場所は以下の位置に置きましょう。

META-INF/native-image/serialization-config.json のファイルの中身は以下の通りです。java.util.Properties が継承している java.util.Hashtablejava.util.Dictionary も記載します。

[
  {
    "name":"java.util.Properties"
  },
  {
    "name":"java.util.Hashtable"
  },
  {
    "name":"java.util.Dictionary"
  }
]

クラスが足りないと以下のメッセージが出力されます。

Exception in thread "main" com.oracle.svm.core.jdk.UnsupportedFeatureError: SerializationConstructorAccessor class not found for declaringClass: java.util.Hashtable (targetConstructorClass: java.util.Dictionary). Usually adding java.util.Hashtable to serialization-config.json fixes the problem.

記載が終わったら再び native-image 化すればOKです。

nektos/actにイベントの情報を渡す

GitHub でリリースするときに、指定したバージョンをワークフロー内で使いたい時にnektos/act ではどうしたら良いか分からなかったので試したメモ。

Webにある記事や GitHub のマニュアルによると、${{ github.event.release.tag_name }} でタグの名前が取得できるらしい。

Webhook のイベントとペイロード - GitHub Docs

しかしながら、どうやってnektos/act へその情報を渡せば良いのかはドキュメントを見ても理解できませんでした。

GitHub - nektos/act: Run your GitHub Actions locally 🚀

test.yml というスモールケースを作って、先ほどの Github のマニュアルとnektos/actのドキュメントを眺めながら調査しました。

name: test

on:
  release:
    types:
      - published

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Deploy Production
        run: |
          export VERSION=`echo ${{ github.event.release.tag_name }} | awk '{print substr($0, 2)}'`
          echo ${VERSION}

その結果、github.event.release.tag_nameevent 以下を要素とした JSON を作ったらうまくいきました。

> cat event.json
{
  "release": {
    "tag_name": "vworkflow_test5"
  }
}

このファイルを使ったactの実行は以下のように -e ファイル名 でできます。

act -W .github/workflows/test.yml -e event.json

nektos/act でシークレットを使う

GitHub Action で Maven Central へアップロードするワークフローを作成していたときに困ったのでメモ

ワークフローは以下のYAMLで、シークレットには以下の2種類があります。

  • ユーザ名やパスワード、パスフレーズのように1行の文字列として表現されるもの
  • 鍵のように複数行からなる文字列として表現されるもの

これらはそれぞれ act へ渡す方法が異なります。

name: Deploy Snapshot

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          java-package: 'jdk'
          cache: maven
          server-id: 'ossrh'
          server-username: OSSRH_USERNAME
          server-password: OSSRH_PASSWORD
          gpg-passphrase: SIGN_KEY_PASS
          gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}

      - name: Test
        run: mvn -B package --file pom.xml

      - name: Deploy Production
        env:
          OSSRH_USERNAME: ${{ secrets.MAVEN_USERNAME }}
          OSSRH_PASSWORD: ${{ secrets.MAVEN_USER_PASSWORD }}
          SIGN_KEY_PASS: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
        run: mvn -B deploy --no-transfer-progress -DskipTests --file jfr4jdbc-driver/pom.xml

act は以下のように実行します。

act --secret-file secretfile -s MAVEN_GPG_PRIVATE_KEY="$(< gpg.pem)" 

1行で記載できるものは --secret-file ファイル名 として指定し、複数行で記載するものは-s 変数名="$(< ファイル名)"とします。

1行で記載できるシークレットは以下のようにまとめてファイルに記載できます。

> cat secretfile
MAVEN_GPG_PASSPHRASE=xxx
MAVEN_USERNAME=xxx
MAVEN_USER_PASSWORD=xxx

鍵など1行で記載できないシークレットそれぞれファイルを用意します。

> cat gpg.pem
-----BEGIN PGP PRIVATE KEY BLOCK-----

xxxxx
-----END PGP PRIVATE KEY BLOCK-----