GraalVM ネイティブイメージの導入

GraalVM ネイティブイメージは、Java アプリケーションをデプロイして実行する新しい方法を提供します。Java 仮想マシンと比較して、ネイティブイメージはより少ないメモリフットプリントで実行でき、はるかに高速な起動時間で実行できます。

これらは、コンテナーイメージを使用してデプロイされるアプリケーションに適しており、"Function as a service" (FaaS) プラットフォームと組み合わせると特に興味深いものになります。

JVM 用に作成された従来のアプリケーションとは異なり、GraalVM ネイティブイメージアプリケーションでは、実行可能ファイルを作成するために事前処理が必要です。この事前処理には、メインエントリポイントからアプリケーションコードを静的に分析することが含まれます。

GraalVM ネイティブイメージは、プラットフォーム固有の完全な実行可能ファイルです。ネイティブイメージを実行するために Java 仮想マシンを提供する必要はありません。

GraalVM を実際に使ってみたいだけの場合は、初めての GraalVM ネイティブアプリケーションの開発セクションにジャンプして、後でこのセクションに戻ることができます。

JVM デプロイ との主な違い

GraalVM ネイティブイメージが事前に生成されるという事実は、ネイティブアプリケーションと JVM ベースのアプリケーションの間にいくつかの重要な違いがあることを意味します。主な違いは次のとおりです。

  • アプリケーションの静的分析は、ビルド時に main エントリポイントから実行されます。

  • ネイティブイメージの作成時に到達できないコードは削除され、実行可能ファイルの一部にはなりません。

  • GraalVM はコードの動的要素を直接認識しないため、リフレクション、リソース、直列化、動的プロキシについて通知する必要があります。

  • アプリケーションのクラスパスはビルド時に固定され、変更できません。

  • 遅延クラスの読み込みはありません。実行可能ファイルに同梱されているものはすべて、起動時にメモリに読み込まれます。

  • 完全にサポートされていない Java アプリケーションのいくつかの側面に関して、いくつかの制限があります。

これらの違いに加えて、Spring は Spring 事前処理と呼ばれるプロセスを使用しており、これによりさらに制限が課されます。これらについて学ぶために、少なくとも次のセクションの最初を必ず参照してください。

GraalVM リファレンスドキュメントのネイティブイメージの互換性ガイド (英語) セクションでは、GraalVM の制限事項について詳しく説明しています。

Spring 事前処理について

典型的な Spring Boot アプリケーションは非常に動的であり、構成は実行時に実行されます。実際、Spring Boot 自動構成の概念は、正しく構成するためにランタイムの状態に反応することに大きく依存しています。

アプリケーションのこれらの動的な特徴について GraalVM に伝えることは可能ですが、そうすると静的分析の利点のほとんどが台無しになります。代わりに、Spring Boot を使用してネイティブイメージを作成する場合、閉じた世界が想定され、アプリケーションの動的な側面が制限されます。

閉じた世界の前提には、GraalVM 自体によって作成される制限に加えて、次の制限が含まれます。

  • アプリケーションで定義された Bean は実行時に変更できません。つまり、次のことを意味します。

    • Spring @Profile アノテーションとプロファイル固有の構成には制限があります

    • Bean が作成された場合に変更されるプロパティはサポートされていません (たとえば、@ConditionalOnProperty および .enable プロパティ)。

これらの制限が適用されると、ビルド時に Spring が事前処理を実行し、GraalVM が使用できる追加のアセットを生成することが可能になります。Spring AOT で処理されたアプリケーションは、通常、次のものを生成します。

  • Java ソースコード

  • バイトコード (動的プロキシなど)

  • GraalVM JSON ヒントファイル:

    • リソースのヒント (resource-config.json)

    • リフレクションのヒント (reflect-config.json)

    • 直列化のヒント (serialization-config.json)

    • Java プロキシのヒント (proxy-config.json)

    • JNI ヒント (jni-config.json)

ソースコード生成

Spring アプリケーションは Spring Bean で構成されます。内部的に、Spring Framework は 2 つの異なる概念を使用して Bean を管理します。Bean インスタンスは、実際に作成されたインスタンスのことで、他の Bean に注入することができるものです。Bean の属性とそのインスタンスの作成方法を定義するために使用される Bean 定義もあります。

典型的な @Configuration クラスを取ると:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MyConfiguration {

	@Bean
	public MyBean myBean() {
		return new MyBean();
	}

}

Bean 定義は、@Configuration クラスを解析し、@Bean メソッドを見つけることによって作成されます。上記の例では、myBean という名前のシングルトン Bean の BeanDefinition を定義しています。MyConfiguration クラス自体の BeanDefinition も作成しています。

myBean インスタンスが必要な場合、Spring は、myBean() メソッドを呼び出して結果を使用する必要があることを認識します。JVM で実行している場合、アプリケーションの起動時に @Configuration クラスの解析が行われ、リフレクションを使用して @Bean メソッドが呼び出されます。

ネイティブイメージを作成する場合、Spring は別の方法で動作します。実行時に @Configuration クラスを解析して Bean 定義を生成するのではなく、ビルド時に実行します。Bean 定義が検出されると、それらは処理され、GraalVM コンパイラーで分析できるソースコードに変換されます。

Spring AOT プロセスは、上記の構成クラスを次のようなコードに変換します。

import org.springframework.beans.factory.aot.BeanInstanceSupplier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;

/**
 * Bean definitions for {@link MyConfiguration}.
 */
public class MyConfiguration__BeanDefinitions {

	/**
	 * Get the bean definition for 'myConfiguration'.
	 */
	public static BeanDefinition getMyConfigurationBeanDefinition() {
		Class<?> beanType = MyConfiguration.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(MyConfiguration::new);
		return beanDefinition;
	}

	/**
	 * Get the bean instance supplier for 'myBean'.
	 */
	private static BeanInstanceSupplier<MyBean> getMyBeanInstanceSupplier() {
		return BeanInstanceSupplier.<MyBean>forFactoryMethod(MyConfiguration.class, "myBean")
			.withGenerator((registeredBean) -> registeredBean.getBeanFactory().getBean(MyConfiguration.class).myBean());
	}

	/**
	 * Get the bean definition for 'myBean'.
	 */
	public static BeanDefinition getMyBeanBeanDefinition() {
		Class<?> beanType = MyBean.class;
		RootBeanDefinition beanDefinition = new RootBeanDefinition(beanType);
		beanDefinition.setInstanceSupplier(getMyBeanInstanceSupplier());
		return beanDefinition;
	}

}
生成される正確なコードは、Bean 定義の性質によって異なる場合があります。

上記で、生成されたコードが @Configuration クラスと同等の Bean 定義を作成することがわかりますが、GraalVM が理解できる直接的な方法で作成されます。

myConfiguration Bean の Bean 定義と myBean の定義があります。myBean インスタンスが必要な場合は、BeanInstanceSupplier が呼び出されます。このサプライヤーは、myConfiguration Bean で myBean() メソッドを呼び出します。

Spring AOT 処理中、アプリケーションは Bean 定義が使用可能になるまで起動されます。AOT 処理フェーズでは Bean インスタンスは作成されません。

Spring AOT は、すべての Bean 定義に対してこのようなコードを生成します。また、Bean 後処理が必要な場合 (たとえば、@Autowired メソッドを呼び出す場合) にコードを生成します。AOT 処理されたアプリケーションが実際に実行されるときに ApplicationContext を初期化するために Spring Boot によって使用される ApplicationContextInitializer も生成されます。

AOT で生成されたソースコードは冗長になる可能性がありますが、非常に読みやすく、アプリケーションのデバッグ時に役立ちます。生成されたソースファイルは、Maven および build/generated/aotSources を Gradle と共に使用する場合、target/spring-aot/main/sources にあります。

ヒントファイルの生成

Spring AOT エンジンは、ソースファイルの生成に加えて、GraalVM で使用されるヒントファイルも生成します。ヒントファイルには、GraalVM がコードを直接調べても理解できないことを処理する方法を説明する JSON データが含まれています。

例: プライベートメソッドで Spring アノテーションを使用している可能性があります。Spring は、GraalVM であっても、プライベートメソッドを呼び出すためにリフレクションを使用する必要があります。このような状況が発生した場合、Spring はリフレクションヒントを記述して、プライベートメソッドが直接呼び出されなくても、ネイティブイメージで利用できる必要があることを GraalVM が認識できるようにします。

ヒントファイルは META-INF/native-image に生成され、GraalVM によって自動的に取得されます。

生成されたヒントファイルは、Maven および build/generated/aotResources を Gradle と共に使用する場合、target/spring-aot/main/resources にあります。

プロキシクラスの生成

Spring は、作成したコードを追加機能で強化するために、プロキシクラスを生成する必要がある場合があります。これを行うために、バイトコードを直接生成する cglib ライブラリを使用します。

アプリケーションが JVM で実行されている場合、プロキシクラスはアプリケーションの実行時に動的に生成されます。ネイティブイメージを作成する場合、これらのプロキシをビルド時に作成して、GraalVM に含めることができるようにする必要があります。

ソースコードの生成とは異なり、生成されたバイトコードは、アプリケーションのデバッグ時には特に役に立ちません。ただし、javap などのツールを使用して .class ファイルの内容をインスペクションする必要がある場合は、Maven の場合は target/spring-aot/main/classes、Gradle の場合は build/generated/aotClasses で見つけることができます。