The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

ネイティブ・アプリケーションを作成するためのヒント

このガイドには、Java アプリケーションをネイティブ実行可能ファイルとして実行しようとしたときに発生する可能性のある問題を回避するためのさまざまなヒントやコツが記載されています。

適用されるソリューションが異なる可能性のある 2 つの文脈を区別していることに注意してください。

  • アプリケーションの文脈では、pom.xml を修正して、 native-image 設定を調整することに依存することになります。

  • エクステンションの文脈では、Quarkus はこれらすべてを簡素化するための多くのインフラストラクチャーを提供しています。

コンテキストに応じて適切なセクションを参照してください。

アプリケーションでのネイティブのサポート

GraalVM は多くの制約を課しており、アプリケーションをネイティブ実行可能なものにするには、いくつかの調整が必要になるかもしれません。

リソースのインクルード

デフォルトでは、ネイティブ実行可能ファイルをビルドする際に、GraalVM はクラスパス上にあるリソースを作成するネイティブ実行可能ファイルには含めません。ネイティブ実行可能ファイルの一部に含めるリソースは、明示的に設定する必要があります。

Quarkus では、META-INF/resources (Web リソース) にあるリソースを自動的にインクルードしますが、このディレクトリー以外の場所は自分で設定する必要があります。

META-INF/resources にあるものはすべて、静的な Web リソースとして公開されるので、ここでは非常に注意が必要です。このディレクトリーは、「これらのリソースを自動的にネイティブ実行可能ファイルに含める」ためのショートカットではなく、静的な Web リソースのためにのみ使用する必要があります。

その他のリソースは明示的に宣言する必要があります。

追加でリソースをネイティブ実行可能ファイルに含めるには、quarkus.native.resources.includes という設定プロパティーと、リソースを除外するための quarkus.native.resources.excludes を使用するのが最も簡単です。

どちらの構成プロパティーもグロブパターンに対応しています。

たとえば、application.properties で以下のプロパティーを設定すると、

quarkus.native.resources.includes=foo/**,bar/**/*.txt
quarkus.native.resources.excludes=foo/private/**

以下が含まれます。

  • foo/ ディレクトリーとそのサブディレクトリーにあるすべてのファイルのうち、foo/private/ とそのサブディレクトリーにあるファイルを除く。

  • bar/ ディレクトリーとそのサブディレクトリーにあるすべてのテキストファイル。

グロブでは不十分で正規表現に頼る必要があるユースケースや、GraalVM インフラストラクチャーを使用することを好む場合は、どのリソースを含めるべきかを定義する JSON ファイル resources-config.json (最も一般的な場所は src/main/resources 内) を作成します:

{
  "resources": [
    {
      "pattern": ".*\\.xml$"
    },
    {
      "pattern": ".*\\.json$"
    }
  ]
}

このパターンは有効な Java 正規表現です。ここでは、すべての XML ファイルと JSONファイル をネイティブ実行可能ファイルに含めています。

You can find more information about this topic in the GraalVM documentation.

最後にやるべきことは、application.properties に適切な設定を追加して、設定ファイルを native-image 実行ファイルに知らせることです。

quarkus.native.additional-build-args =-H:ResourceConfigurationFiles=resources-config.json

前のスニペットでは、src/main/resources に追加されたため、ファイルのパス全体を指定するのではなく、単に resources-config.json を使用することができました。ファイルが別のディレクトリーに追加されていた場合は、適切なファイルパスを手動で指定する必要がありました。

複数のオプションはカンマで区切ることができます。例えば、次のように使用することができます。

quarkus.native.additional-build-args =\
    -H:ResourceConfigurationFiles=resources-config.json,\
    -H:ReflectionConfigurationFiles=reflection-config.json

さまざまなリソースが含まれており、追加のリフレクションの登録を担保するため。

何らかの理由で application.properties に前述の構成を追加することが望ましくない場合には、ビルドツールで効果的に同様の操作を行うように構成することも可能です。

Maven を使用する場合は、以下のような構成にすることができます。

<profiles>
    <profile>
        <id>native</id>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
            <quarkus.native.additional-build-args>-H:ResourceConfigurationFiles=resources-config.json</quarkus.native.additional-build-args>
        </properties>
    </profile>
</profiles>

リフレクションのための登録

ネイティブ実行可能ファイルを構築する際、GraalVM は閉世界仮説に基づいて動作します。これは、コールツリーを解析し、直接使用しないクラス/メソッド/フィールドをすべて削除します。

リフレクションで使用される要素はコールツリーの一部ではないので、デッドコードとして排除されます (他のケースでは直接呼び出されない場合)。ネイティブ実行可能ファイルにこれらの要素を含めるには、リフレクションに明示的に登録する必要があります。

これは非常によくあるケースです。JSONライブラリは一般的にリフレクションを使用してオブジェクトをJSONにシリアライズする為です。

    public class Person {
        private String first;
        private String last;

        public String getFirst() {
            return first;
        }

        public void setFirst(String first) {
            this.first = first;
        }

        public String getLast() {
            return last;
        }

        public void setValue(String last) {
            this.last = last;
        }
    }

    @Path("/person")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public class PersonResource {

        private final Jsonb jsonb;

        public PersonResource() {
            jsonb = JsonbBuilder.create(new JsonbConfig());
        }

        @GET
        public Response list() {
            return Response.ok(jsonb.fromJson("{\"first\":  \"foo\", \"last\":  \"bar\"}", Person.class)).build();
        }
    }

上記のコードを使用した場合、ネイティブ実行可能ファイルを使用すると以下のような例外が発生します:

Exception handling request to /person: org.jboss.resteasy.spi.UnhandledException: javax.json.bind.JsonbException: Can't create instance of a class: class org.acme.jsonb.Person, No default constructor found

あるいはJackson を使用している場合は以下の通りです:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.acme.jsonb.Person and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

さらに厄介なのは、例外がスローされずに JSON の結果が完全に空になってしまうことです。

このタイプの問題を解決するには、2 つの方法があります。

@RegisterForReflection アノテーションの使用

リフレクション用のクラスを登録する最も簡単な方法は、@RegisterForReflection アノテーションを使用することです。

@RegisterForReflection
public class MyClass {
}

クラスがサードパーティの jar にある場合は、そのクラスの @RegisterForReflection をホストする空のクラスを使用することで行うことができます。

@RegisterForReflection(targets={ MyClassRequiringReflection.class, MySecondClassRequiringReflection.class})
public class MyReflectionConfiguration {
}

なお、 MyClassRequiringReflectionMySecondClassRequiringReflection はリフレクションに登録されますが、 MyReflectionConfiguration は登録されません。

この機能は、オブジェクトマッピング機能を使用するサードパーティのライブラリ(JacksonやGSONなど)を使用する際に便利です。

@RegisterForReflection(targets = {User.class, UserImpl.class})
public class MyReflectionConfiguration {

}

設定ファイルの使用

設定ファイルを使用してリフレクション用のクラスを登録することも可能です。

例えば、クラス com.acme.MyClass のすべてのメソッドをリフレクションのために登録するには、reflection-config.json を作成します (最も一般的な場所は src/main/resources の中です)。

[
  {
    "name" : "com.acme.MyClass",
    "allDeclaredConstructors" : true,
    "allPublicConstructors" : true,
    "allDeclaredMethods" : true,
    "allPublicMethods" : true,
    "allDeclaredFields" : true,
    "allPublicFields" : true
  }
]

For more details on the format of this file, please refer to the GraalVM documentation.

最後にやるべきことは、application.properties に適切な設定を追加して、設定ファイルを native-image 実行ファイルに知らせることです。

quarkus.native.additional-build-args =-H:ReflectionConfigurationFiles=reflection-config.json

前のスニペットでは、src/main/resources に追加されたため、ファイルのパス全体を指定するのではなく、単に reflection-config.json を使用することができました。ファイルが別のディレクトリーに追加されていた場合は、適切なファイルパスを手動で指定する必要がありました。

複数のオプションはカンマで区切ることができます。例えば、次のように使用することができます。

quarkus.native.additional-build-args =\
    -H:ResourceConfigurationFiles=resources-config.json,\
    -H:ReflectionConfigurationFiles=reflection-config.json

さまざまなリソースが含まれており、追加のリフレクションの登録を担保するため。

何らかの理由で application.properties に前述の構成を追加することが望ましくない場合には、ビルドツールで効果的に同様の操作を行うように構成することも可能です。

Maven を使用する場合は、以下のような構成にすることができます。

<profiles>
    <profile>
        <id>native</id>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
            <quarkus.native.additional-build-args>-H:ReflectionConfigurationFiles=reflection-config.json</quarkus.native.additional-build-args>
        </properties>
    </profile>
</profiles>

クラスの初期化を遅らせる

デフォルトでは、Quarkus はビルド時にすべてのクラスを初期化します。

特定のクラスの初期化が静的ブロックで行われる場合、実行時に延期する必要がある場合があります。通常、このような設定を省略すると、以下のような実行時例外が発生します。

Error: No instances are allowed in the image heap for a class that is initialized or reinitialized at image runtime: sun.security.provider.NativePRNG
Trace: object java.security.SecureRandom
method com.amazonaws.services.s3.model.CryptoConfiguration.<init>(CryptoMode)
Call path from entry point to com.amazonaws.services.s3.model.CryptoConfiguration.<init>(CryptoMode):

もう一つのよくあるエラーの原因は、GraalVMによって取得されたイメージヒープが Random / SplittableRandom インスタンスを含んでいる場合です。

Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap. Instances created during image generation have cached seed values and don't behave as expected.

これは、Quarkus がビルド時に静的な Random/SplittableRandom フィールドを持つクラスを初期化し、この特定のインスタンスがイメージヒープに一時的に含まれることが原因である場合がほとんどです。

この Random/SplittableRandom の問題については、 このブログ記事 に詳細な情報があります。

これらの場合、実行時に違反しているクラスの初期化を遅らせることが解決策になる可能性があり、それを達成するには、 --initialize-at-run-time=<package or class> 設定ノブを使用できます。

上記の例のように、quarkus.native.additional-build-args 設定プロパティーを使用して native-image 設定に追加する必要があります。

You can find more information about all this in the GraalVM documentation.

quarkus.native.additional-build-args 設定プロパティーを介して複数のクラスやパッケージを指定する必要がある場合は、, シンボルをエスケープする必要があります。その例を以下に示します。

quarkus.native.additional-build-args=--initialize-at-run-time=com.example.SomeClass\\,org.acme.SomeOtherClass

application.properties の代わりに Maven 設定を使用する場合。

<quarkus.native.additional-build-args>--initialize-at-run-time=com.example.SomeClass\,org.acme.SomeOtherClass</quarkus.native.additional-build-args>

プロキシークラスの管理

ネイティブアプリケーションを書く際には、実装するインターフェースのリストを指定して、イメージのビルド時にプロキシークラスを定義する必要があります。

このような状況では、以下のようなエラーが発生します。

com.oracle.svm.core.jdk.UnsupportedFeatureError: Proxy class defined by interfaces [interface org.apache.http.conn.HttpClientConnectionManager, interface org.apache.http.pool.ConnPoolControl, interface com.amazonaws.http.conn.Wrapped] not found. Generating proxy classes at runtime is not supported. Proxy classes need to be defined at image build time by specifying the list of interfaces that they implement. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.

Solving this issue requires adding the -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> option and to provide a dynamic proxy configuration file. You can find all the information about the format of this file in the GraalVM documentation.

モジュール化のメリット

ネイティブ実行可能ファイルのビルド時に、GraalVM はアプリケーションのコールツリーを分析し、必要なすべてのコードを含むコードセットを生成します。モジュラーコードベースを使用することは、アプリケーションの未使用部分またはオプション部分の問題を回避すると同時に、ネイティブ実行可能ファイルのビルド時間とサイズの両方を削減するための鍵となります。このセクションでは、ネイティブアプリケーションのモジュール化の利点の背後にある詳細について学習します。

コードが十分にモジュール化されていない場合、生成されたネイティブ実行可能ファイルは、ユーザーが必要とするよりも多くのコードになる可能性があります。機能が使用されず、コードがネイティブ実行可能ファイルにコンパイルされる場合、これはネイティブコンパイル時間とメモリー使用量、およびネイティブ実行可能ディスクスペースと開始ヒープサイズの無駄になります。サードパーティーのライブラリーや特殊な API サブシステムを使用すると、ネイティブコンパイルエラーや実行時エラーが発生し、それらの使用が十分にモジュール化されていない場合、さらに多くの問題が発生します。最近の問題は、Java の AWT API を使用してイメージを含む XML ファイルをデシリアライズできる JAXB ライブラリーに見られます。大多数の Quarkus XML ユーザーはイメージをデシリアライズする必要がないため、ネイティブ実行可能ファイルに JAXB AWT コードを追加するように Quarkus を特別に設定しない限り、ユーザーアプリケーションに Java AWT コードを含める必要はありません。ただし、AWT を使用する JAXB コードは、残りの XML 解析コードと同じ jar にあるため、この分離を実現するのはかなり複雑で、Java バイトコード置換を使用して回避する必要がありました。これらの置換は維持するのが難しく、壊れやすいため、最後の手段にする必要があります。

モジュール式のコードベースは、この種の問題を回避するための最良の方法です。上記の JAXB/AWT の問題を考えると、イメージを扱う JAXB コードが別のモジュールまたは jar (例: jaxb-images) にあった場合、Quarkus は、構築時に画像を含むXMLファイルをシリアライズ/デシリアライズする必要があるとユーザーが特に要求しない限り、そのモジュールを含めないように選択することができます。

モジュラーアプリケーションのもう 1 つの利点は、ネイティブ実行可能ファイルに組み込む必要があるコードセットを削減できることです。コードセットが小さいほど、ネイティブ実行可能ファイルのビルドが高速になり、生成されるネイティブ実行可能ファイルが小さくなります。

ここで重要なポイントは次のとおりです。オプションの機能、特に大きなフットプリントを持つサードパーティーのライブラリーや API サブシステムに依存する機能は、別のモジュールに保持することが最善のソリューションです。

アプリケーションに同様の問題があるかどうかを確認するにはどうすればよいですか? アプリケーションの詳細な調査は別として、 https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.htmlMaven のオプションの依存関係 の使用法を見つけることは、アプリケーションで同様の問題が発生する可能性があります。この種の依存関係は避けるべきであり、代わりに、オプションの依存関係と対話するコードは別のモジュールに移動する必要があります。

シングルトンの強制

遅延クラス初期化 セクションですでに説明したように、Quarkus はデフォルトでビルド時にすべてのコードを初期化するようマークします。つまり、特に指定がない限り、静的変数はビルド時に代入され、静的ブロックもビルド時に実行されます。

これにより、通常は実行ごとに異なるJavaプログラムの値が、常に一定の値として返されるようになります。例えば、 System.currentTimeMillis() という値が割り当てられている静的フィールドは、Quarkus ネイティブ実行ファイルとして実行されると、常に同じ値を返します。

静的変数の初期化に依存するシングルトンは、同様の問題に悩まされます。たとえば、クエリーを実行する REST エンドポイントと共に、静的初期化に基づくシングルトンがあるとします。

@Path("/singletons")
public class Singletons {

    @GET
    @Path("/static")
    public long withStatic() {
        return StaticSingleton.startTime();
    }
}

class StaticSingleton {
    static final long START_TIME = System.currentTimeMillis();

    static long startTime() {
        return START_TIME;
    }
}

singletons/static のエンドポイントに問い合わせると、アプリケーションを再起動しても常に同じ値が返されます。

$ curl http://localhost:8080/singletons/static
1656509254532%

$ curl http://localhost:8080/singletons/static
1656509254532%

### Restart the native application ###

$ curl http://localhost:8080/singletons/static
1656509254532%

enum クラスに依存しているシングルトンも同じ問題の影響を受けます。

@Path("/singletons")
public class Singletons {

    @GET
    @Path("/enum")
    public long withEnum() {
        return EnumSingleton.INSTANCE.startTime();
    }
}

enum EnumSingleton {
    INSTANCE(System.currentTimeMillis());

    private final long startTime;

    private EnumSingleton(long startTime) {
        this.startTime = startTime;
    }

    long startTime() {
        return startTime;
    }
}

singletons/enum エンドポイントがクエリーされると、アプリケーションが再起動された後でも、常に同じ値が返されます。

$ curl http://localhost:8080/singletons/enum
1656509254601%

$ curl http://localhost:8080/singletons/enum
1656509254601%

### Restart the native application ###

$ curl http://localhost:8080/singletons/enum
1656509254601%

これを修正する 1 つの方法は、CDI の @Singleton アノテーションを使用してシングルトンを構築することです。

@Path("/singletons")
public class Singletons {

    @Inject
    CdiSingleton cdiSingleton;

    @GET
    @Path("/cdi")
    public long withCdi() {
        return cdiSingleton.startTime();
    }
}

@Singleton
class CdiSingleton {
    // Note that the field is not static
    final long startTime = System.currentTimeMillis();

    long startTime() {
        return startTime;
    }
}

再起動するたびに、 singletons/cdi をクエリすると、JVM モードと同じように、異なる値が返却されます:

$ curl http://localhost:8080/singletons/cdi
1656510218554%

$ curl http://localhost:8080/singletons/cdi
1656510218554%

### Restart the native application ###

$ curl http://localhost:8080/singletons/cdi
1656510714689%

静的フィールドや列挙型に依存しながらシングルトンを強制する別の方法は、実行時までクラス初期化を遅延させる ことです。CDIベースのシングルトンの良い利点は、クラスの初期化が制約されないので、ユースケースに応じて、ビルド時または実行時に初期化されるべきかを自由に決定できることです。

よくある Java API オーバーライドの注意点

特定の一般的に使用される Java メソッドは、toStringequalshashCode などのユーザークラスによってオーバーライドされます。オーバーライドの大部分は問題を引き起こしませんが、サードパーティーのライブラリー (追加の書式設定など) を使用したり、動的言語機能 (リフレクションやプロキシーなど) を使用したりすると、ネイティブイメージのビルドが失敗する可能性があります。これらの障害の中には、設定によって解決できるものもありますが、処理がより難しいものもあります。

GraalVM ポイントツー分析の観点からは、アプリケーションが明示的に呼び出さなくても、これらのメソッドオーバーライドで何が起こるかが問題になります。これは、これらのメソッドが JDK 全体で使用されているためであり、これらの呼び出しの 1 つが制約のない型 (たとえば、java.lang.Object) で実行されるだけで、分析でその特定のすべての実装をプルする必要があるためです。方法。

Quarkus 拡張機能でネイティブをサポートする

Quarkus のエクステンションでのネイティブのサポートは、Quarkus がこれらすべてを単純化するための多くのツールを提供しているため、さらに簡単になりました。

ここで説明したことはすべて、Quarkus エクステンションの文脈でのみ機能し、アプリケーションでは機能しません。

リフレクションの登録

Quarkus では、ReflectiveClassBuildItem を使用することで、エクステンションへのリフレクションの登録が簡単になり、JSON 設定ファイルが不要になります。

リフレクション用のクラスを登録するには、Quarkus プロセッサークラスを作成し、リフレクションを登録するビルドステップを追加する必要があります。

public class SaxParserProcessor {

    @BuildStep
    ReflectiveClassBuildItem reflection() {
        // since we only need reflection to the constructor of the class, we can specify `false` for both the methods and the fields arguments.
        return new ReflectiveClassBuildItem(false, false, "com.sun.org.apache.xerces.internal.parsers.SAXParser");
    }

}

More information about reflection in GraalVM can be found here.

リソースのインクルード

エクステンションのコンテキストでは、Quarkus は、エクステンションの作成者が NativeImageResourceBuildItem を指定できるようにすることで、JSON 設定ファイルの必要性を排除しています。

public class ResourcesProcessor {

    @BuildStep
    NativeImageResourceBuildItem nativeImageResourceBuildItem() {
        return new NativeImageResourceBuildItem("META-INF/extra.properties");
    }

}

For more information about GraalVM resource handling in native executables please refer to the GraalVM documentation.

遅延クラス初期化

Quarkus では、エクステンションの作成者が簡単に RuntimeInitializedClassBuildItem を登録できるようにすることで、物事をシンプルにしています。簡単な例としては、次のようなものが考えられます。

public class S3Processor {

    @BuildStep
    RuntimeInitializedClassBuildItem cryptoConfiguration() {
        return new RuntimeInitializedClassBuildItem(CryptoConfiguration.class.getCanonicalName());
    }

}

このような構文を使用すると、 --initialize-at-run-time オプションが自動的に native-image コマンドラインに追加されることを意味します。

For more information about --initialize-at-run-time, please read the GraalVM documentation.

プロキシークラスの管理

同様に、Quarkus では、エクステンションの作成者が NativeImageProxyDefinitionBuildItem を登録することができます。その例としては、以下のようなものがあります。

public class S3Processor {

    @BuildStep
    NativeImageProxyDefinitionBuildItem httpProxies() {
        return new NativeImageProxyDefinitionBuildItem("org.apache.http.conn.HttpClientConnectionManager",
                "org.apache.http.pool.ConnPoolControl", "com.amazonaws.http.conn.Wrapped");
    }

}

このような構文を使用すると、-H:DynamicProxyConfigurationResources オプションが自動的に native-image コマンドラインに追加されることを意味します。

For more information about Proxy Classes you can read the GraalVM documentation.

ネイティブイメージでのロギング

Apache Commons Logging や Log4j などのロギングコンポーネントを必要とする依存関係を使用していて、ネイティブ実行可能ファイルをビルドする際に ClassNotFoundException が発生する場合、ロギングライブラリーを除外し、対応する JBoss Logging アダプターを追加することで解決できます。

詳しくは、Loggingガイド をご覧ください。