Tips for writing native applications
このガイドには、Java アプリケーションをネイティブ実行可能ファイルとして実行しようとしたときに発生する可能性のある問題を回避するためのさまざまなヒントやコツが記載されています。
適用されるソリューションが異なる可能性のある 2 つの文脈を区別していることに注意してください。
-
アプリケーションの文脈では、
pom.xmlを修正して、native-image設定を調整することに依存することになります。 -
エクステンションの文脈では、Quarkus はこれらすべてを簡素化するための多くのインフラストラクチャーを提供しています。
コンテキストに応じて適切なセクションを参照してください。
アプリケーションでのネイティブのサポート
GraalVM は多くの制約を課しており、アプリケーションをネイティブ実行可能なものにするには、いくつかの調整が必要になるかもしれません。
リソースのインクルード
デフォルトでは、ネイティブ実行可能ファイルをビルドする際に、GraalVM はクラスパス上にあるリソースを、作成するネイティブ実行可能ファイルには含めません。ネイティブ実行可能ファイルの一部となるリソースは、明示的に設定する必要があります。
Quarkus では、META-INF/resources (Web リソース) にあるリソースを自動的にインクルードしますが、このディレクトリー以外の場所は自分で設定する必要があります。
|
その他のリソースは明示的に宣言する必要があります。 |
Using the quarkus.native.resources.includes configuration property
ネイティブ実行可能ファイルにより多くのリソースを含めるには、 quarkus.native.resources.includes 設定プロパティーを使用するのが最も簡単な方法です。この設定プロパティーは glob パターンをサポートします。
たとえば、 application.properties に次のプロパティーを記述すると:
quarkus.native.resources.includes=foo/**,bar/**/*.txt
以下が含まれます。
-
foo/ディレクトリーおよびそのサブディレクトリー内のすべてのファイル、 -
bar/ディレクトリーとそのサブディレクトリーにあるすべてのテキストファイル。
設定ファイルの使用
If you prefer relying on the GraalVM infrastructure directly,
you can also create a reachability-metadata.json file with a resources section defining which resources should be included.
This file should be placed under the src/main/resources/META-INF/native-image/<group-id>/<artifact-id> folder.
This way it will be automatically parsed by the native build, without additional configuration.
|
GraalVMインフラストラクチャに依存するということは、新しいMandrelとGraalVMのバージョンがリリースされるたびに、設定ファイルを最新の状態に保つ責任があるということです。 Note that the |
|
Legacy individual configuration files ( |
このようなファイルの例を以下に示します:
{
"resources": [
{
"glob": "META-INF/**/*.xml"
},
{
"glob": "config/**/*.json"
}
]
}
The glob patterns use the same syntax as quarkus.native.resources.includes.
Here we include all the XML files located under META-INF and JSON files located under config into the native executable.
|
Avoid using highly generic patterns like
|
|
このトピックの詳細については、 ネイティブ・イメージでリソースにアクセスするGraalVM ガイドを参照してください。 |
リフレクションのための登録
ネイティブ実行可能ファイルを構築する際、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: jakarta.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 {
}
なお、 MyClassRequiringReflection と MySecondClassRequiringReflection はリフレクションに登録されますが、 MyReflectionConfiguration は登録されません。
この機能は、オブジェクトマッピング機能を使用するサードパーティのライブラリ(JacksonやGSONなど)を使用する際に便利です。
@RegisterForReflection(targets = {User.class, UserImpl.class})
public class MyReflectionConfiguration {
}
注記: デフォルトでは、@RegisterForReflection アノテーションは、リフレクションの対象となる可能性のあるネストされたクラスも登録します。この動作を回避するには、ignoreNested 属性を true に設定します。
設定ファイルの使用
また、GraalVMインフラストラクチャに依存することを好むなら、リフレクションのためにクラスを登録するために設定ファイルを使用することもできます。
|
GraalVMインフラストラクチャに依存するということは、新しいMandrelとGraalVMのバージョンがリリースされるたびに、設定ファイルを最新の状態に保つ責任があるということです。 |
|
The legacy |
As an example, in order to register all methods of class com.acme.MyClass for reflection, we create reachability-metadata.json under src/main/resources/META-INF/native-image/<group-id>/<artifact-id>:
{
"reflection": [
{
"type" : "com.acme.MyClass",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
}
]
}
|
For more information about the format of this file, see the GraalVM Reachability Metadata guide. |
Place the configuration file under the src/main/resources/META-INF/native-image/<group-id>/<artifact-id> folder.
This way it will be automatically parsed by the native build, without additional configuration.
プロキシーの登録
@RegisterForReflection と同様に、@RegisterForProxy を使用して動的プロキシーのインターフェイスを登録できます。
@RegisterForProxy
public interface MyInterface extends MySecondInterface {
}
MyInterface とそのすべてのスーパーインターフェイスが登録されることに注意してください。
また、インターフェイスがサードパーティーの jar 内にある場合は、@RegisterForProxy をホストする空のクラスを使用して実行できます。
@RegisterForProxy(targets={MyInterface.class, MySecondInterface.class})
public class MyReflectionConfiguration {
}
|
指定されたプロキシーインターフェイスの順序は重要であることに注意してください。詳細は、Proxy javadoc を参照してください。 |
リソースバンドルとリソースファイルの登録
リソースバンドルは、 @RegisterResourceBundle を使用して、ネイティブイメージに含めるように登録できます:
@RegisterResourceBundle(bundleName = "messages")
@RegisterResourceBundle(bundleName = "errors", moduleName = "foo")
public class NativeConfiguration {
}
その他のリソースファイルは、 @RegisterResources を使って、ネイティブイメージに含まれるように登録することができます:
@RegisterResources(globs = ["path1/level*/**", "path2/level5/**"])
@RegisterResources(globs = ["**.txt"])
public class NativeConfiguration {
}
クラスの初期化を遅らせる
デフォルトでは、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 フィールドを持つクラスを初期化し、この特定のインスタンスがイメージヒープに一時的に含まれることが原因である場合がほとんどです。
|
この |
これらの場合、実行時に違反しているクラスの初期化を遅らせることが解決策になる可能性があり、それを達成するには、 --initialize-at-run-time=<package or class> 設定ノブを使用できます。
上記の例のように、quarkus.native.additional-build-args 設定プロパティーを使用して native-image 設定に追加する必要があります。
|
詳細については、 ネイティブ・イメージにおけるGraalVMクラスの初期化 ガイドを参照してください。 |
|
|
プロキシークラスの管理
ネイティブアプリケーションを書く際には、実装するインターフェースのリストを指定して、イメージのビルド時にプロキシークラスを定義する必要があります。
このような状況では、以下のようなエラーが発生します。
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.
|
The legacy |
To solve the issue you can create a reachability-metadata.json file under the src/main/resources/META-INF/native-image/<group-id>/<artifact-id> folder with a proxy section.
For more information about the format, see the GraalVM Reachability Metadata guide.
または、quarkus エクステンションを作成し、プロキシークラスを プロキシークラスの管理 で説明されているように登録することもできます。。
モジュール化のメリット
ネイティブ実行可能ファイルのビルド時に、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 のオプションの依存関係 の使用法を見つけることは、アプリケーションで同様の問題が発生する可能性があります。この種の依存関係は避けるべきであり、代わりに、オプションの依存関係と対話するコードは別のモジュールに移動する必要があります。
シングルトンの強制
delay class initialization セクションですでに説明したように、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%
静的フィールドや列挙型に依存しながらシングルトンを強制する別の方法は、delay its class initialization until run time を行うことです。CDIベースのシングルトンの良い利点は、クラスの初期化が制約されないので、ユースケースに応じて、ビルド時または実行時に初期化されるべきかを自由に決定できることです。
よくある Java API オーバーライドの注意点
特定の一般的に使用される Java メソッドは、toString、equals、hashCode などのユーザークラスによってオーバーライドされます。オーバーライドの大部分は問題を引き起こしませんが、サードパーティーのライブラリー (追加の書式設定など) や動的言語機能 (リフレクションやプロキシーなど) を使用したりすると、ネイティブイメージのビルドが失敗する可能性があります。これらの障害の中には、設定によって解決できるものもありますが、処理がより難しいものもあります。
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");
}
}
|
GraalVMにおけるリフレクションの詳細については、 ネイティブイメージにおけるGraalVM リフレクション ガイドを参照してください。 |
リソースのインクルード
エクステンションのコンテキストでは、Quarkus は、エクステンションの作成者が NativeImageResourceBuildItem を指定できるようにすることで、JSON 設定ファイルの必要性を排除しています。
public class ResourcesProcessor {
@BuildStep
NativeImageResourceBuildItem nativeImageResourceBuildItem() {
return new NativeImageResourceBuildItem("META-INF/extra.properties");
}
}
|
ネイティブ実行可能ファイルでのGraalVMリソースの取り扱いについては、 ネイティブ・イメージでのGraalVMリソース・アクセス ガイドを参照してください。 |
遅延クラス初期化
Quarkus では、エクステンションの作成者が簡単に RuntimeInitializedClassBuildItem を登録できるようにすることで、物事をシンプルにしています。簡単な例としては、次のようなものが考えられます。
public class S3Processor {
@BuildStep
RuntimeInitializedClassBuildItem cryptoConfiguration() {
return new RuntimeInitializedClassBuildItem(CryptoConfiguration.class.getCanonicalName());
}
}
このような構文を使用すると、 --initialize-at-run-time オプションが自動的に native-image コマンドラインに追加されることを意味します。
|
|
プロキシークラスの管理
同様に、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");
}
}
これにより、Quarkus はプロキシークラスを処理するために必要な設定を生成できるようになります。
Alternatively, you may create a reachability-metadata.json file as described in プロキシークラスの管理.
|
どちらの場合も、追加の設定なしで、ネイティブビルドによって設定が自動的に解析されます。 ネイティブ実行ファイルでのプロキシークラスの使用の詳細は、https://www.graalvm.org/jdk21/reference-manual/native-image/dynamic-features/DynamicProxy/[Dynamic Proxy in Native Image] および GraalVM Configure Dynamic Proxies Manually を参照してください。 |
ネイティブイメージでのロギング
Apache Commons Logging や Log4j などのロギングコンポーネントを必要とする依存関係を使用していて、ネイティブ実行可能ファイルをビルドする際に ClassNotFoundException が発生する場合、ロギングライブラリーを除外し、対応する JBoss Logging アダプターを追加することで解決できます。
詳細は、ログ記録ガイド を参照してください。