ネイティブ・アプリケーションを作成するためのヒント
このガイドには、Java アプリケーションをネイティブ実行可能ファイルとして実行しようとしたときに発生する可能性のある問題を回避するためのさまざまなヒントやコツが記載されています。
適用されるソリューションが異なる可能性のある 2 つの文脈を区別していることに注意してください。
-
アプリケーションの文脈では、
pom.xml
を修正して、native-image
設定を調整することに依存することになります。 -
エクステンションの文脈では、Quarkus はこれらすべてを簡素化するための多くのインフラストラクチャーを提供しています。
コンテキストに応じて適切なセクションを参照してください。
アプリケーションでのネイティブのサポート
GraalVM は多くの制約を課しており、アプリケーションをネイティブ実行可能なものにするには、いくつかの調整が必要になるかもしれません。
リソースのインクルード
デフォルトでは、ネイティブ実行可能ファイルをビルドする際に、GraalVM はクラスパス上にあるリソースを、作成するネイティブ実行可能ファイルには含めません。ネイティブ実行可能ファイルの一部となるリソースは、明示的に設定する必要があります。
Quarkus では、META-INF/resources
(Web リソース) にあるリソースを自動的にインクルードしますが、このディレクトリー以外の場所は自分で設定する必要があります。
その他のリソースは明示的に宣言する必要があります。 |
quarkus.native.resources.includes
設定プロパティの使用
より多くのリソースをネイティブ実行可能ファイルに含めるには、quarkus.native.resources.includes
という設定プロパティーと、quarkus.native.resources.excludes
リソースを除外するための quarkus.native.resources.excludes
を使用するのが最も簡単です。
どちらの設定プロパティーもグロブパターンに対応しています。
たとえば、application.properties
で以下のプロパティーを設定すると、
quarkus.native.resources.includes=foo/**,bar/**/*.txt
quarkus.native.resources.excludes=foo/private/**
以下が含まれます。
-
foo/
ディレクトリーとそのサブディレクトリーにあるすべてのファイルのうち、foo/private/
とそのサブディレクトリーにあるファイルを除く。 -
bar/
ディレクトリーとそのサブディレクトリーにあるすべてのテキストファイル。
設定ファイルの使用
If globs are not sufficiently precise for your use case and you need to rely on regular expressions, or if you prefer relying on the GraalVM infrastructure,
you can also create a resource-config.json
JSON file defining which resources should be included.
This, and other native image configuration files, should be placed under the src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
folder.
This way they will be automatically parsed by the native build, without additional configuration.
GraalVMインフラストラクチャに依存するということは、新しいMandrelとGraalVMのバージョンがリリースされるたびに、設定ファイルを最新の状態に保つ責任があるということです。 Please also note that the |
このようなファイルの例を以下に示します:
{
"resources": [
{
"pattern": ".*\\.xml$"
},
{
"pattern": ".*\\.json$"
}
]
}
このパターンは有効な Java 正規表現です。ここでは、すべての XML ファイルと JSONファイル をネイティブ実行可能ファイルに含めています。
このトピックの詳細については、 ネイティブ・イメージでリソースにアクセスする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 {
}
Note: By default the @RegisterForReflection
annotation will also registered any potential nested classes for reflection. If you want to avoid this behavior, you can set the ignoreNested
attribute to true
.
設定ファイルの使用
また、GraalVMインフラストラクチャに依存することを好むなら、リフレクションのためにクラスを登録するために設定ファイルを使用することもできます。
GraalVMインフラストラクチャに依存するということは、新しいMandrelとGraalVMのバージョンがリリースされるたびに、設定ファイルを最新の状態に保つ責任があるということです。 |
As an example, in order to register all methods of class com.acme.MyClass
for reflection, we create reflect-config.json
(the most common location is within src/main/resources
)
[
{
"name" : "com.acme.MyClass",
"allDeclaredConstructors" : true,
"allPublicConstructors" : true,
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredFields" : true,
"allPublicFields" : true
}
]
このファイルのフォーマットの詳細については、 ネイティブイメージにおけるGraalVMリフレクション ガイドを参照してください。 |
The final order of business is to make the configuration file known to the native-image
executable.
To do that, place the configuration file under the src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
folder.
This way they will be automatically parsed by the native build, without additional configuration.
Registering for proxy
Analogous to @RegisterForReflection
, you can use @RegisterForProxy
to register interfaces for dynamic proxy:
@RegisterForProxy
public interface MyInterface extends MySecondInterface {
}
Note that MyInterface
and all its super interfaces will be registered.
Also, in case the interface is in a third-party jar, you can do it by using an empty class that will host the @RegisterForProxy
for it.
@RegisterForProxy(targets={MyInterface.class, MySecondInterface.class})
public class MyReflectionConfiguration {
}
Note that the order of the specified proxy interfaces is significant. For more information, see Proxy javadoc. |
クラスの初期化を遅らせる
デフォルトでは、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. To define proxy classes use -H:DynamicProxyConfigurationFiles=<comma-separated-config-files> and -H:DynamicProxyConfigurationResources=<comma-separated-config-resources> options.
Solving this issue requires creating a proxy-config.json
file under the src/main/resources/META-INF/native-image/<group-id>/<artifact-id>
folder.
This way the configuration will be automatically parsed by the native build, without additional configuration.
For more information about the format of this file, see the Dynamic Proxy Metadata in JSON 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 のオプションの依存関係 の使用法を見つけることは、アプリケーションで同様の問題が発生する可能性があります。この種の依存関係は避けるべきであり、代わりに、オプションの依存関係と対話するコードは別のモジュールに移動する必要があります。
シングルトンの強制
As already explained in the delay class initialization section, Quarkus marks all code to be initialized at build time by default. This means that, unless marked otherwise, static variables will be assigned at build time, and static blocks will be executed at build time too.
これにより、通常は実行ごとに異なる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 オーバーライドの注意点
Certain commonly used Java methods are overridden by user classes,
e.g. toString
, equals
, hashCode
…etc.
The majority of overrides do not cause problems,
but if they use third party libraries (e.g. for additional formatting),
or use dynamic language features (e.g. reflection or proxies),
they can cause native image build to fail.
Some of those failures might be solvable via configuration,
but others can be more tricky to handle.
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");
}
}
このような構文を使用すると、-H:DynamicProxyConfigurationResources
オプションが自動的に native-image
コマンドラインに追加されることを意味します。
プロキシ・クラスの詳細については、 GraalVM による動的プロキシの手動設定 ガイドを参照してください。 |
ネイティブイメージでのロギング
Apache Commons Logging や Log4j などのロギングコンポーネントを必要とする依存関係を使用していて、ネイティブ実行可能ファイルをビルドする際に ClassNotFoundException
が発生する場合、ロギングライブラリーを除外し、対応する JBoss Logging アダプターを追加することで解決できます。
For more details please refer to the Logging guide.