Java on Truffleでの拡張ホットスワップ機能
Java on Truffleを使用すると、強化されたHotSwap機能を利用できます。これは、実行中のアプリケーションを再起動することなく、開発時にコードを自然に進化させることが可能です。拡張ホットスワップの利点を得るために、アプリケーションをデバッグ・モードで起動して標準のIDEデバッガをアタッチする以外に特定の構成を行う必要はありません。
Java on Truffleでのデバッグ
任意のIDEデバッガを使用して、Java on Truffleランタイムで実行されているJavaアプリケーションをデバッグできます。たとえば、IntelliJ IDEAからのデバッガ・セッションの開始は実行構成に基づきます。デバッガを同じ環境内のJavaアプリケーションにアタッチするには、メイン・メニューで「Run」→「Debug…」→「Edit Configurations」に移動し、「Environment」を展開して、JREの値とVMオプションの値を確認します。GraalVMがプロジェクトのJREとして表示され、VMオプションに-truffle -XX:+IgnoreUnrecognizedVMOptions
が含まれている必要があります。まだサポートされていない-javaagent
引数がIntellijによって自動的に追加されるため、-XX:+IgnoreUnrecognizedVMOptions
を指定する必要があります。「Debug」を押します。
これにより、アプリケーションが実行され、デバッガ・セッションがバックグラウンドで開始されます。
デバッグ・セッションでのホットスワップの使用
デバッガ・セッションを実行すると、セッションを再起動する必要なく、幅広いコード変更(ホットスワップ)を適用できます。独自のアプリケーションで、または次の手順に従って、これを自由に試してください:
- 新しいJavaアプリケーションを作成します。
-
次の
main
メソッドを開始点として使用します:public class HotSwapDemo { private static final int ITERATIONS = 100; public static void main(String[] args) { HotSwapDemo demo = new HotSwapDemo(); System.out.println("Starting HotSwap demo with Java on Truffle: 'java.vm.name' = " + System.getProperty("java.vm.name")); // run something in a loop for (int i = 1; i <= ITERATIONS; i++) { demo.runDemo(i); } System.out.println("Completed HotSwap demo with Java on Truffle"); } public void runDemo(int iteration) { int random = new Random().nextInt(iteration); System.out.printf("\titeration %d ran with result: %d\n", iteration, random); } }
- Espressoで実行していることを
java.vm.name
プロパティが示していることを確認します。 runDemo()
の最初の行に行ブレークポイントを設定します。-
Java on Truffleで実行するように実行構成を設定し、「Debug」を押します。次のように表示されます:
-
ブレークポイントで一時停止している間に、
runDemo()
の本体からメソッドを抽出します: -
「Run」→「Debugging Actions」→「Reload Changed Classes」に移動して、変更をリロードします:
-
「Debug」→「Frames view」で
<obsolete>:-1
の現在のフレームを確認して、変更が適用されたことを検証します: -
新しく抽出したメソッドの最初の行にブレークポイントを設定し、「Resume Program」を押します。ブレークポイントにヒットします:
-
printRandom()
のアクセス修飾子をprivate
からpublic static
に変更してみます。変更をリロードします。「Resume Program」を押して変更が適用されたことを検証します:
Java on Truffleでの拡張ホットスワップ機能のデモのビデオ・バージョンを視聴してください。
サポートされる変更
Java on Truffleの拡張ホットスワップは、ほぼ完全な機能です。次の変更がサポートされています:
- メソッドの追加および削除
- コンストラクタの追加および削除
- インタフェースに対するメソッドの追加および削除
- メソッドのアクセス修飾子の変更
- コンストラクタのアクセス修飾子の変更
- フィールドの追加および削除
- フィールド・タイプの変更
- 階層内のフィールドの移動および状態の保持(次のノートを参照)
- クラス・アクセス修飾子に対する変更(抽象修飾子やfinal修飾子など)
- ラムダに対する変更
- 新しい匿名内部クラスの追加
- 匿名内部クラスの削除
- スーパークラスの変更
- 実装されたインタフェースの変更
ノート: インスタンス・フィールドをクラス階層に移動すると、可能なかぎり状態が保持されます。例として、プルアップ・フィールドのリファクタリングなどがあります。このリファクタリングでは、元のサブクラスの既存のすべてのインスタンスが、以前に格納された値をスーパークラス・フィールドから読み取ることができます。一方、変更前にフィールドが存在しなかった関連のないサブクラス・インスタンスの場合、新しいフィールド値は言語のデフォルトになります(つまり、オブジェクト型のフィールドの場合はnull、int型の場合は0)。
GraalVM 22.1.0の時点で、次の制限が残っています:
- 列挙に対する変更
ホットスワップ・プラグインAPI
Java on Truffleを使用すると、拡張ホットスワップ機能を利用できます。これは、実行中のアプリケーションを再起動することなく、開発時にコードを自然に進化させることが可能です。コードのリロード(HotSwap)は強力なツールですが、注釈の変更、実装されているサービスやBeanなどのフレームワーク固有の変更など、あらゆる種類の変更を反映するには不十分です。このような場合、変更が実行中のインスタンスに完全に反映される前に、構成またはコンテキストをリロードするためにコードを実行する必要があることがよくあります。ここでは、Truffle on Javaのホットスワップ・プラグインAPIが役に立ちます。Truffle on Javaのホットスワップ・プラグインAPIは、IDEでソース・コードの編集に対応して変更を反映するように適切なフックを設定することで、フレームワーク開発者を対象にしています。主な設計原則は、指定されたホットスワップ・イベントで起動する様々なホットスワップ・リスナーを登録できることです。たとえば、静的イニシャライザの再実行機能、一般的なホットスワップ後コールバック、特定のサービス・プロバイダの実装が変更されたときのフックなどがあります。
ノート: ホットスワップ・プラグインAPIは開発中であり、ホットスワップ・リスナーのよりきめ細かい登録がコミュニティからのリクエストに応じて追加される可能性があります。コミュニティ・サポート・チャネルを通じて、APIを形成するのに役立つ拡張リクエストを送信することをお薦めします。
Micronautでのより強力なリロード・サポートを有効にする実行中の例をよく検討し、ホットスワップ・プラグインAPIを確認します。
Micronautホットスワップ・プラグイン
Micronautホットスワップ・プラグインの実装例は、Micronaut-coreのフォークとしてホストされます。次の手順はmacOS Xの設定に基づいており、Windowsではわずかな変更が必要です。開始するには:
- リポジトリをクローニングします:
git clone git@github.com:javeleon/micronaut-core.git
- ビルドして、ローカルMavenリポジトリに公開します:
cd micronaut-core ./gradlew publishMavenPublicationToMavenLocal
これで、ホットスワップに対応しているMicronautのバージョンができました。拡張されたバージョンのMicronautを使用するサンプル・アプリケーションを設定する前に、プラグインが内部で実行している動作を確認します。
重要なクラスはMicronautHotSwapPlugin
で、アプリケーションのソース・コードに対して特定の変更が行われたときにリロードできるアプリケーション・コンテキストに保持されます。クラスは次のようになります:
final class MicronautHotSwapPlugin implements HotSwapPlugin {
private final ApplicationContext context;
private boolean needsBeenRefresh = false;
MicronautHotSwapPlugin(ApplicationContext context) {
this.context = context;
// register class re-init for classes that provide annotation metadata
EspressoHotSwap.registerClassInitHotSwap(
AnnotationMetadataProvider.class,
true,
() -> needsBeenRefresh = true);
// register ServiceLoader listener for declared bean definitions
EspressoHotSwap.registerMetaInfServicesListener(
BeanDefinitionReference.class,
context.getClassLoader(),
() -> reloadContext());
EspressoHotSwap.registerMetaInfServicesListener(
BeanIntrospectionReference.class,
context.getClassLoader(),
() -> reloadContext());
}
@Override
public String getName() {
return "Micronaut HotSwap Plugin";
}
@Override
public void postHotSwap(Class<?>[] changedClasses) {
if (needsBeenRefresh) {
reloadContext();
}
needsBeenRefresh = false;
}
private void reloadContext() {
if (Micronaut.LOG.isInfoEnabled()) {
Micronaut.LOG.info("Reloading app context");
}
context.stop();
context.flushBeanCaches();
context.start();
// fetch new embedded application bean which will re-wire beans
Optional<EmbeddedApplication> bean = context.findBean(EmbeddedApplication.class);
// now restart the embedded app/server
bean.ifPresent(ApplicationContextLifeCycle::start);
}
}
ホットスワップAPIに関するロジックは、このクラスのコンストラクタにあります。Micronautは、注釈メタデータが収集され、生成されたクラスの静的フィールドに格納されるコンパイル時注釈処理を中心に設計されています。開発者がMicronaut注釈付きクラスに変更を加えるたびに、対応するメタデータ・クラスが再生成されます。標準ホットスワップは静的イニシャライザを再実行しない(およびするべきではない)ため、ホットスワップ・プラグインでは静的イニシャライザはメタデータ(Micronaut生成クラス)を提供するすべてのクラスに対して再実行されます。したがって、このAPIメソッドEspressoHotSwap.registerClassInitHotSwap
が使用されます:
public static boolean registerClassInitHotSwap(Class<?> klass, boolean onChange, HotSwapAction action)
これにより、特定のクラス、さらにそれらのサブクラスに対するクラス変更にリスナーが登録されます。onChange
変数は、含まれるコードが変更された場合のみ静的イニシャライザを再実行するように指示します。action
パラメータは、静的イニシャライザが再実行されたときに特定のアクションを起動するフックです。ここでは、静的イニシャライザが再実行されるたびに、needsBeenRefresh
フィールドをtrueに設定するための関数を渡します。ホットスワップ・アクションが完了すると、プラグインはpostHotSwap
コールを受信し、trueのneedsBeenRefresh
に応じて、reloadContext
メソッドでアプリケーション・コンテキストをリロードするためにMicronaut固有のコードを実行します。
新しいクラスの検出およびインジェクション
ホットスワップは、実行中のアプリケーションでクラスをホットスワップできるように設計されています。ただし、開発者がまったく新しいクラス(Micronautの新しい@Controller
クラスなど)を導入した場合、ホットスワップは新しいクラスを注入しません。これは、最低限の内部クラスロード・ロジックに関する知識を必要とするためです。
クラスがフレームワークによって検出される標準的な方法は、ServiceLoader
メカニズムを使用する方法です。Truffle on JavaホットスワップAPIには、メソッドEspressoHotSwap.registerMetaInfServicesListener
を使用してサービス実装変更リスナーを登録するための組込みサポートがあります:
public static boolean registerMetaInfServicesListener(Class<?> serviceType, ClassLoader loader, HotSwapAction action)
現在のサポートは、META-INF/services
のクラスパス・ベースのサービス・デプロイメントに関する実装変更のリスニングに限定されます。登録済クラス・タイプに対する一連のサービス実装が変更されるたびに、action
が起動されます。Micronautのホットスワップ・プラグインで、reloadContext
が実行され、変更が自動的に取得されます。
ノート: サービス実装の変更によって発生するホットスワップ・アクションは、ホットスワップと無関係に実行されます。開発者として、実行中のアプリケーションの新しい機能を表示するには、IDEからホットスワップを実行する必要はありません。
Micronautの次のレベルのホットスワップ
Micronautホットスワップ・プラグインの動作を確認したので、この機能を実際のアプリケーションで使用してください。最初のMicronaut Graalアプリケーションの作成のチュートリアルから作成されたサンプル・アプリケーションを次に示します。例のソースは、ここから既製のGradleプロジェクトとしてダウンロードできます。IDEでプロジェクトをダウンロードして解凍し、開きます。
続行する前に、Java on Truffleがインストール済であることを確認し、GraalVMをプロジェクトSDKとして設定してください。
- IDEで、サンプル・プロジェクト内のルート
build.gradle
に移動します。追加します:run.jvmArgs+="-truffle"
- また、拡張されたMicronautフレームワークを以前に公開したMavenローカル・リポジトリを追加します。たとえば:
repositories { mavenLocal() ... }
gradle.properties
で、公開したMicronautのバージョンを更新します。たとえば:micronautVersion=2.5.8-SNAPSHOT
これで、すべての設定が行われました。
-
assemble
タスクを実行し、定義されたrun
Gradleタスクを使用して実行構成を作成します。 -
「デバッグ」ボタンを押してアプリケーションをデバッグ・モードで起動します。これにより、拡張されたホットスワップ・サポートが可能になります。
-
アプリケーションが起動したら、
http://localhost:8080/conferences/random
に移動してConferenceController
からレスポンスを取得していることを確認します。 - サンプル・アプリケーション内のクラスに様々な変更を試行し(たとえば、
@Controller
マッピングを別の値に変更する、または新しい@Get
注釈付きメソッドを追加するなど)、ホットスワップを適用してその優れた処理を確認します。新しい@Controller
クラスを定義した場合、必要なのはクラスのコンパイルのみであり、ファイル・システムの監視によって変更が取得されると、明示的にホットスワップを使用する必要がなくリロードを確認できます。