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」を押します。

これにより、アプリケーションが実行され、デバッガ・セッションがバックグラウンドで開始されます。

デバッグ・セッションでのホットスワップの使用

デバッガ・セッションを実行すると、セッションを再起動する必要なく、幅広いコード変更(ホットスワップ)を適用できます。独自のアプリケーションで、または次の手順に従って、これを自由に試してください:

  1. 新しいJavaアプリケーションを作成します。
  2. 次の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);
               }
           }
    
  3. Espressoで実行していることをjava.vm.nameプロパティが示していることを確認します。
  4. runDemo()の最初の行に行ブレークポイントを設定します。
  5. Java on Truffleで実行するように実行構成を設定し、「Debug」を押します。次のように表示されます:

    ホットスワップ・デバッグ・セッション: デバッグ出力

  6. ブレークポイントで一時停止している間に、runDemo()の本体からメソッドを抽出します:

    ホットスワップ・デバッグ・セッション: メソッドの抽出

  7. 「Run」→「Debugging Actions」→「Reload Changed Classes」に移動して、変更をリロードします:

    ホットスワップ・デバッグ・セッション: 変更されたクラスのリロード

  8. 「Debug」→「Frames view」で<obsolete>:-1の現在のフレームを確認して、変更が適用されたことを検証します:

    ホットスワップ・デバッグ・セッション: フレーム・ビュー

  9. 新しく抽出したメソッドの最初の行にブレークポイントを設定し、「Resume Program」を押します。ブレークポイントにヒットします:

    ホットスワップ・デバッグ・セッション: ブレークポイントの設定およびプログラムの再開

  10. printRandom()のアクセス修飾子をprivateからpublic staticに変更してみます。変更をリロードします。「Resume Program」を押して変更が適用されたことを検証します:

    ホットスワップ・デバッグ・セッション: アクセス修飾子の変更

Java on Truffleでの拡張ホットスワップ機能のデモのビデオ・バージョンを視聴してください。

サポートされる変更

Java on Truffleの拡張ホットスワップは、ほぼ完全な機能です。次の変更がサポートされています:

ノート: インスタンス・フィールドをクラス階層に移動すると、可能なかぎり状態が保持されます。例として、プルアップ・フィールドのリファクタリングなどがあります。このリファクタリングでは、元のサブクラスの既存のすべてのインスタンスが、以前に格納された値をスーパークラス・フィールドから読み取ることができます。一方、変更前にフィールドが存在しなかった関連のないサブクラス・インスタンスの場合、新しいフィールド値は言語のデフォルトになります(つまり、オブジェクト型のフィールドの場合は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ではわずかな変更が必要です。開始するには:

  1. リポジトリをクローニングします:
      git clone git@github.com:javeleon/micronaut-core.git
    
  2. ビルドして、ローカル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として設定してください。

  1. IDEで、サンプル・プロジェクト内のルートbuild.gradleに移動します。追加します:
     run.jvmArgs+="-truffle"
    
  2. また、拡張されたMicronautフレームワークを以前に公開したMavenローカル・リポジトリを追加します。たとえば:
     repositories {
     mavenLocal()
     ...
     }
    
  3. gradle.propertiesで、公開したMicronautのバージョンを更新します。たとえば:
     micronautVersion=2.5.8-SNAPSHOT
    

    これで、すべての設定が行われました。

  4. assembleタスクを実行し、定義されたrun Gradleタスクを使用して実行構成を作成します。

  5. 「デバッグ」ボタンを押してアプリケーションをデバッグ・モードで起動します。これにより、拡張されたホットスワップ・サポートが可能になります。

  6. アプリケーションが起動したら、http://localhost:8080/conferences/randomに移動してConferenceControllerからレスポンスを取得していることを確認します。

  7. サンプル・アプリケーション内のクラスに様々な変更を試行し(たとえば、@Controllerマッピングを別の値に変更する、または新しい@Get注釈付きメソッドを追加するなど)、ホットスワップを適用してその優れた処理を確認します。新しい@Controllerクラスを定義した場合、必要なのはクラスのコンパイルのみであり、ファイル・システムの監視によって変更が取得されると、明示的にホットスワップを使用する必要がなくリロードを確認できます。