BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル RMIベースのレガシー・アプリケーションをWebSocketに移行する

RMIベースのレガシー・アプリケーションをWebSocketに移行する

キーポイント

  • 技術的な負債、特にエンタープライズ・ソフトウェアにおける技術的負債は、我々が繰り返し直面しなければならない問題である。この記事では、旧式のRMI(Remote Method Invocation)プロトコルに基づく大規模なエンタープライズ・アプリケーションの技術的な負債を取り除き、最新のクラウド対応通信技術に移行するために、私がどのように対処したかに関連するユースケースを紹介する。

  • 提供されたユースケースでは、適切なオープンソースプロジェクトを選択し、フォークし、目的に合うように拡張した経験がある。オープンソースソフトウェアを選択し、使用し、拡張する能力は、最新のアプリケーション・ライフサイクル管理において戦略的である。

  • クラウド時代に台頭しつつあるリアクティブ・プログラミングのパラダイムは、関数型アプローチに基づくもので、新しいソフトウェア開発の課題により適しているように思われる。Javaの世界では、リアクティブ・ストリームは、Java開発におけるリアクティブ・アプローチを標準化する試みの1つである。移行で直面した最も重要な部分の1つは、古典的な同期リクエスト/レスポンスからリアクティブなものへの切り替えである。

  • JakartaEEはJavaEEの後継であり、エンタープライズ・アプリケーションの基準点である。このような移行の最も重要な目標の1つは、最終的な資産をJakartaEEコンテナ(Tomcat、WildFlyなど)内でデプロイ可能にすることである。

原文リンク(2023-04-05)

リモートメソッド呼び出し(RMI)とは何か?

RMIは、J2SE 5.0で初めて導入され、ネットワークを介したアプリケーションの相互運用性を実現するオールインワンのJavaベースのソリューションを提供した。その基本は次のとおりである。

  • クライアント・サーバー・モデルに基づくリモート・プロシージャ・コール(RPC)

  • 設計による同期

  • トランスポート・プロトコルとしてのTCP Socket

  • アプリケーション・プロトコルとしてJavaバイナリー(組み込み)シリアライゼーション

  • 双方向通信プロトコル

    • クライアントがサーバーを呼び、逆にサーバーがクライアントを呼び出す(コールバック)。

当時、これはJavaの世界では非常に革命的なことだった。なぜなら、ITの世界でインターネットが台頭していた時代に、相互運用性が容易に実現できるようになったからだ。

このことは、多くのクライアント・サーバー・アプリケーションが、この新しく魅力的な技術に基づいて開発されたことを意味している。

現代のインターネット時代にRMIを見ると、現代のウェブベースのアーキテクチャでは、フロントエンドのほとんどがインターネットブラウザをベースにしており、プロトコルはプラットフォームやテクノロジーに依存しないオープンスタンダードに基づいていることを考えると、このようなテクノロジーは明らかに古く、範囲外のように思える。

対象読者

以下に述べることから、この記事は、何らかの理由で大規模なレガシーRMIアプリケーションの近代化に取り組んでいるJava開発者やアーキテクトを対象としている。

私もそのような課題に直面したことがあり、その経験を共有したいと思う。

移行と書き換え

まず最初に、読者のために強調しておきたいことがある。マイグレーション」とは、「書き直す」という意味ではない。もしあなたがこのような「マイグレーション」に興味があるなら、これはレガシーJavaアプリケーションを近代化に一歩近づける新たな希望になるかもしれない。

どのようなタスクが必要で、そのようなマイグレーションの背後にある意味を包括的に理解してもらうために、私がある作品で取り組んだ実際のユースケースを紹介しよう。

ユースケース - RMIに基づく古いフルスタックJavaアプリケーションを進化させる

私は、JDK/JRE 8に移植された、古いが完全に動作する大規模なクライアント/サーバーJavaアプリケーションを持っていた。フロントエンドはSwing/AWTベースで、基礎となる通信プロトコルとしてRMIを使用していた。

要件

  1. Docker/Kubernetesを使ってアプリケーションをクラウドに移す。

  2. HTTP/Websocket/gRPCのような最新のウェブ準拠のトランスポート・プロトコルへの移行する。

課題:HTTP経由のRMIトンネリング

最初の評価は、RMI HTTPトンネリングを使用することだったが、最初から少し複雑すぎるように思え、アプリケーションはRMIコールバックを多用するので、HTTPのような一方向のプロトコルは目的に適していなかった。

そういう意味では、Socket以外ではWebsocketが最も目的に合ったプロトコルに思えたが、WebsocketをRMIの基礎プロトコルとしてプラグインする方法を理解するのに十分な労力を費やしたとしても、結果は時間の無駄だった :(。

課題:オープンソースの代替RMI実装を評価する

そこでもうひとつの解決策として、代替RMI実装を評価することにした。私は、理解しやすく、新しいプロトコルをプラグインできる柔軟で適応性のあるアーキテクチャを持つオープンソースの半完成品を特定しようと、それらを探してきた。

厳選されたオープンソース・プロジェクトを進化させる:LipeRMI

ネットサーフィンをしているうちに、Java用の軽量RMI代替実装として定義されたLipeRMIというGitHubホスト・プロジェクトにたどり着いた。LipeRMIは期待通りの要件を備えているように思えた。シンプルだが完全なアプリケーションでテストしたところ、うまくいった。そして驚くべきことに、RMIコールバックも非常にうまくサポートしていた。

オリジナルの実装がソケットをベースにしていたとしても、そのアーキテクチャは柔軟性に富んでおり、私のニーズを達成するために拡張・強化する可能性を確信できた。

LipeRMIの"オリジナル"を理解しよう

下の図は、オリジナルのプロジェクトで提示されたハイレベル・アーキテクチャである。

オリジナルのハイレベル・アーキテクチャ

見ての通り、非常にシンプルだ。主なコンポーネントは、アプリケーション・インターフェースと実装を把握しているCallHandlerである。クライアントとサーバーの両方がCallHandlerを使用し、両者間の接続セッションを作成するためにソケットを直接使用する。

LipeRMIの進化 -"フォーク"

最初のステップとして、私はプロジェクトをフォークし、拡張モデルとテストの両方を簡素化することで、より適切な管理を可能にするために、Mavenのマルチモジュールプロジェクトに変換した。

このようなリファクタリングの結果、以下のようなモジュールができた。

モジュール 概要
core コアの導入
socket 同期ソケットプロトコルを実装したコア拡張
websocket 非同期Websocketプロトコルを実装するコア反応型拡張機能
rmi-emul RMI API をエミュレートするためのコア拡張
examples 様々な例
cheerpj CheerpJをベースにしたWebAssemblyフロントエンド(実験的)

この記事では、coresocketwebsocketに焦点を当てることにする。core+socketは元のプロジェクトのモジュール的な再解釈と考えるべきで、websocketreactive-streamを使用して導入されたcoreのリアクティブ・プロトコルの抽象化を利用した全く新しい実装である。

コア・モジュール

プロトコルの抽象化

coreモジュールには、オリジナル・プロジェクトのコードの大部分を配置した。元のアーキテクチャを見ると、主な目標の1つは基礎となるプロトコルを分離/抽象化することであった。そのため、IServeriClientIRemoteCallerのようなインターフェイスを導入して、これを実現した。その結果、coreモジュールには特定プロトコルの実装がない。

下の図は、プロトコルの抽象化を可能にする新しいアーキテクチャの概要である。

プロトコルを抽象化したクラス図

ソケットモジュール

socketモジュールでは、coreモジュールが提供するすべての同期抽象化機能を実装している。基本的に元のプロジェクトのコードを再利用するが、それを新しいアーキテクチャに差し込む。

ソケット実装を使ったクラス図

コード例

新しいLipeRMI実装を使用する際の複雑さを理解してもらうために、動作例から抜粋したコードスニペットを以下に示す。

リモーティング可能なインターフェース

// Remotable Interface
public interface IAnotherObject extends Serializable {
    int getNumber();    
}

// Remotable Interface
public interface ITestService extends Serializable {

    public String letsDoIt();
    
    public IAnotherObject getAnotherObject();
    
    public void throwAExceptionPlease();
}

サーバ

// Server
public class TestSocketServer implements Constants {

    // Remotable Interface Implementation
    static class TestServiceImpl implements ITestService {
        final CallHandler callHandler;
        int anotherNumber = 0;

        public TestServiceImpl(CallHandler callHandler) {
            this.callHandler = callHandler;
        }

        @Override
        public String letsDoIt() {
            log.info("letsDoIt() done.");
            return "server saying hi";
        }

        @Override
        public IAnotherObject getAnotherObject() {
            log.info("building AnotherObject with anotherNumber= {}", anotherNumber);
            
            IAnotherObject ao = new AnotherObjectImpl(anotherNumber++);

            callHandler.exportObject(IAnotherObject.class, ao);

            return ao;
        }

        @Override
        public void throwAExceptionPlease() {
            throw new AssertionError("take it easy!");
        }
    }

    public TestSocketServer() throws Exception {

        log.info("Creating Server");
        SocketServer server = new SocketServer();

        final ITestService service = new TestServiceImpl(server.getCallHandler());

        log.info("Registering implementation");
        server.getCallHandler().registerGlobal(ITestService.class, service);

        server.start(PORT, GZIPProtocolFilter.Shared);

        log.info("Server listening");
    
    }

    public static void main(String[] args) throws Exception{
        new TestSocketServer();
    }


}

クライアント

// Client
public class TestSocketClient implements Constants {
    
    public static void main(String... args) {

        log.info("Creating Client");
        try( final SocketClient client = new SocketClient("localhost", PORT, GZIPProtocolFilter.Shared)) {

            log.info("Getting proxy");
            final ITestService myServiceCaller = client.getGlobal(ITestService.class);

            log.info("Calling the method letsDoIt(): {}", myServiceCaller.letsDoIt());

            try {
                log.info("Calling the method throwAExceptionPlease():");
                myServiceCaller.throwAExceptionPlease();
            }
            catch (AssertionError e) {
                log.info("Catch! {}", e.getMessage());
            }

            final IAnotherObject ao = myServiceCaller.getAnotherObject();

            log.info("AnotherObject::getNumber(): {}", ao.getNumber());
                            
        }
        
    }
    
}

進化するLipeRMI : reactive-streamを使ってフレームワークにリアクティブ性を追加する

残念ながら、ソケットは同期的なプログラミング・モデルを推進しており、websocketが推進する非同期的なものとはあまり相性が良くない。そこで、Reactive Streams標準を使ってフレームワークをリアクティブなアプローチに移行することにした。

設計ガイドライン

基本的なアイデアは、リクエストとレスポンスをイベントを使って単純に切り離し、リクエストはpublisherから、レスポンスはsubscriberから取得し、ライフサイクルのリクエスト/レスポンス全体をCompletableFuture(基本的にはJavaのPromiseデザインパターン)で管理することだった。

リアクティブ・プロトコルの抽象化(非同期)

前述の通りだ。coreモジュールでリアクティブ・ストリームを使うことを紹介したが、これはノンブロッキング・バックプレッシャーによる非同期ストリーム処理の標準であり、ネットワーク・プロトコルだけでなく、ランタイム環境を対象とした取り組みも包含している。

リアクティブ・ストリームのクラス図

インターフェース 説明文
Processor<T,R> プロセッサーは、サブスクライバーであると同時にパブリッシャーでもあり、両者の契約に従う処理段階を表す。
Publisher<T> パブリッシャーは、潜在的に無制限の数のシーケンス化されたエレメントのプロバイダーであり、サブスクライバーから受け取った要求に従ってそれらを発行する。
Subscriber<T> SubscriberのインスタンスをPublisher.subscribe(Subscriber)メソッドに渡した後、Subscriber.onSubscribe(Subscription)メソッドへの呼び出しを1回受け取ります。
Subscription サブスクリプションは、サブスクライバーがパブリッシャーにサブスクライブする1対1のライフサイクルを表します。

以下は、ReactiveClient抽象化を含む新しいコア・アーキテクチャである。

リアクティブ・プロトコルを抽象化したクラス図

リアクティブ・クライアントの実装は、抽象化されたReactiveClient クラスに含まれている。これはRemoteCallProcessorクラスに基づいており、リアクティブ・フローProcessorの実装である。これは、リモート・コールをトリガーするイベントを発行するPublisher、そのようなリモート・コールの結果を含むイベントを受信するSubscriberとしても機能する。最後に、イベントの相互作用はReactiveRemoteCallerによって調整される。

最後にWebsocketモジュールを実装する

reactive-streamの実装を紹介した後、ソケットからWebsocketに切り替えるのは簡単で、やりがいのあるコーディングの練習になった。

素早く検証し、概念実証を行うために、私はシンプルなオープンソースのマイクロフレームワークであるJava-WebSocketを使うことにした。 Java-WebSocketは、シンプルで効果的なWebsocket実装を提供するが、私の本当の目標は、そのWebsocket仕様を使ってJakarta EEにプラグインすることだ。この記事の最後の部分では、どのようなJakarta EE互換製品であれ、RMIプロトコルと互換性を持たせ、同時にアプリケーションの進化とスムーズな移行を保証する方法について述べる。

WebSocket実装を使ったクラス図

上のクラス図から分かるように、WSClientConnectionHandlerWSServerConnectionHandlerという2つの新しいハンドラ・クラスがあり、それぞれクライアントとサーバーに入出力されるイベントを管理すると同時に、各呼び出しにおける一貫性を管理している。

コード例

驚くべきことに、上で紹介したコード例は、Websocketでも基本的に同じように動作する。クライアントはSocketClient からからからLipeRMIWebSocketClientに 、サーバーはSocketServerから LipeRMIWebSocketServerに移動するだけで十分だ。それだけだ!

// Client
try( LipeRMIWebSocketClient client = new LipeRMIWebSocketClient(new URI(format( "ws://localhost:%d", PORT)), GZIPProtocolFilter.Shared)) 
{ 
    // use client
}

// Server
final LipeRMIWebSocketServer server = new LipeRMIWebSocketServer();

TomEEランタイムを使用してJakarta EEにLipeRMI Websocketをプラグインする

JakartaEEは現在、エンタープライズ・アプリケーション開発のデファクト・スタンダードとなっている。そのため、LipeRMIを戦略的に統合することができる。

Jakarta EEは本質的にJavaアプリケーション・サーバーを実現するための仕様であり、このことは、このような仕様に準拠したアプリケーション・サーバーを選択しなければならないことを意味する。LipeRMIの統合を試すために、私はJakarta EE Web Profileとして認定されているApache TomEEを選択した!

RMIは組み込みのサービス・ブローカーを提供するため、アプリケーション・サーバーなしで動作するように設計されているが、LipeRMIを使用することで、リモート・メソッド呼び出しとサービス・ブローカーを分離し、この分離により、JakartaEEコンテナ内でRMI処理をプラグインできるようになった。

ServerEndPoint

クライアントによってオープンされた全てのWebSocketセッションを管理するServerEndpointを作成することで、Jakarta WebSocket仕様の統合を開始しよう。

@ServerEndpoint( value = "/lipermi" )
public class WSServerConnectionHandler {

}

実装する最も重要な動作は、クライアントがメソッド呼び出しを要求している場合(RemoteCall)、または着信メッセージがコールバック呼び出し結果(RemoteReturn)である場合に応じて、RemoteCallまたはRemoteReturnという2つの異なるタイプのバイナリメッセージを処理することだ。

着信するWebsocketメッセージに対して実行される主なタスクを示す簡略化したシーケンス図を以下に示す。

これは、Websocketセッション・セッション上でリクエストとレスポンスを処理することの複雑さを理解してもらうために、元のコードを抜粋したものである。

@ServerEndpoint( value = "/lipermi" )
public class WSServerSessionHandler {

 @OnMessage
 public void onMessage(ByteBuffer buffer, Session webSocket) {
   try (final ByteArrayInputStream bais = new ByteArrayInputStream(buffer.array());
        final ObjectInputStream input = new ObjectInputStream(bais))
   {

       final Object objFromStream = input.readUnshared();
       final IRemoteMessage remoteMessage = filter.readObject(objFromStream);

       if (remoteMessage instanceof RemoteCall) {
           this.handleRemoteCall( webSocket, (RemoteCall)remoteMessage );
       } else if (remoteMessage instanceof RemoteReturn) {
           remoteReturnManager.handleRemoteReturn( (RemoteReturn) remoteMessage);
       } else {
           log.warn("Unknown IRemoteMessage type");
       }
   }
   catch( Exception ex ) {
       log.warn("error reading message", ex );
   }

 }
}

クライアントエンドポイント

JakartaEE は、クライアント側から、WebSocketContainer を取得するための ContainerProvider を提供する。これにより、WebSocketサーバーに接続して新しいセッションを取得できるようになる。

WebSocketContainer container = ContainerProvider.getWebSocketContainer();
session = container.connectToServer(this, serverUri);

reactive-stream実装と JakartaEE Websocketクライアント API を含むタスクの完全なリモートメソッド呼び出しフローを示すシーケンス図を以下に示す。

上の図は、3 つのマクロ・タスクに分割できる。

  1. リモートサービスプロキシを取得し、非同期のリクエストとレスポンスを管理するCompletableFutureを開始するメソッドを呼び出す。
  2. リモートコールプロセッサーにリクエストを送り、結果を管理するためにリモートリターンパブリッシャーへのサブスクリプションを作成する。
  3. Websocketセッションでバイナリ・リクエストを送信し、OnMessageWebsocketハンドラで結果を待つ。

実験と進化

RMI 実装を Websocket上に移動し、JakartaEE コンテナに準拠させることができたら、RMI の元のアーキテクチャをより現代的なものに進化させることが想像できる。

RMI設計の限界

RMI自体は、クライアントとサーバーがJavaを使って開発されることを前提に設計されている。特に、RMIアプリケーション・データ・プロトコルは、組み込みのJavaシリアライゼーションに依存しているからだ。今日、最新のアプリケーションはWebクライアントに特権を与えており、RMI技術では、Javaアプレット技術が非推奨になったこともあり、これは実現不可能に思える。しかし、私たちを助けてくれる新しい最先端技術がWebAssemblyだ。

WebAssemblyによる救済

WebAssemblyによって、ブラウザーの技術的障壁は取り除かれた。そのため、Javascript言語だけでなく、WebAssemblyに準拠したバイトコード(wasm)を生成できるコンパイラを持つすべてのプログラミング言語が、ブラウザのコンテキスト内で実行できる。

現在、WebAssemblyを生成する最も有名なプログラミング言語のひとつはRustだが、C#やSwiftなど、より成熟した他の言語もWebAssembly生成を提供している。しかし、Javaはどうだろうか?

JavaからWebAssemblyへ

JavaをWebAssemblyにコンパイルできる最も興味深いプロジェクトの一つがCheerpJだ。これはJava Swing/AWTとシリアライズもサポートしている。私はこのプロジェクトで実験してみたが、結果は非常に有望だった。実際、LipeRMIを使った簡単なチャットの開発に成功し、CheerpJを通してブラウザ内にJavaクライアントを直接デプロイした。

しかし、CheerpJとWebAssemblyについて深く掘り下げることは、この記事の範囲外であるが、おそらく次の記事のための非常に興味深い素材であろう。

結論

レガシープロジェクトのマイグレーションを始めたが、うまくいっている。多くの労力を必要とするが、結果は非常に有望であることを覚えておいてほしい。さらに、Websocketプロトコルに切り替えたことで、予想外のエキサイティングなシナリオが新たに開ける。

私のアイデアは、LipeRMIフォークに取り組んで、プロプライエタリなJavaのシリアライゼーションの代わりにJSONベースのシリアライゼーションを使用することだ。これにより、アプリケーションの移行が完了すれば、JavaScript/React.NETなどの他の技術でクライアントを開発できるようになる。

この記事が、私と同じ課題に取り組んでいる誰かの役に立つことを願っている。それでは、よいプログラミングを!

参考文献

作者について

この記事に星をつける

おすすめ度
スタイル

BT