※本記事は、Aleks Seović による “Hello, Coherence Community Edition, Part 2: Building the client” を翻訳したものです。

本記事をPDFでダウンロード (PDF)


ReactとJavaFXを使い、Coherence CEバックエンド・アプリケーションと連動可能なフロントエンド・クライアントを開発する方法

2020年 10月 9日

 

本シリーズの第1回の記事では、オラクルのオープンソース製品であるCoherence Community Edition(CE)に格納したTo Doリストのタスクを管理するREST APIの実装方法について説明しました。REST APIのテストにはcurlベースのインタフェースを使いましたが、curlは、この世でもっともユーザー・フレンドリなインタフェースというわけではありません。

本記事では、前回の記事のバックエンド・コードを利用する2つのクライアントを実装します。1つはReactベースのWeb UI(図1参照)、もう1つはJavaFXを使ったデスクトップUIです。

図1: To Do リスト・アプリケーションのユーザー・フレンドリなインタフェース

 

クライアント・アプリケーションと Coherence CE

まずは、基本的な内容について確認しておきましょう。クライアント・アプリケーションでは、どのようにしてCoherence CEに接続し、データにアクセスすればよいのでしょうか。

Coherence CEでは、クラスタ・メンバー・クライアントとリモート・クライアントという2種類のクライアントをサポートしています。

前回の記事で実装したREST APIは、クラスタ・メンバー・クライアントの例です。クラスタ・メンバー・クライアントでは、ストレージを有効化することも、ストレージを無効化することもできます。この2種類のメンバーは本質的に同じものですが、ストレージを無効化したメンバーではローカルにデータが保存されないという点が異なります。

前回の記事で、このプロジェクトではデータ・ストアとアプリ・サーバーを分けることもできると述べましたが、それはこのことを指しています。ストレージを有効化したメンバーですべてのデータを管理し、それとは別に、ストレージを無効化したメンバーとしてHelidon Webサーバーを実行してREST APIを提供することもできました。このような構成が妥当な場合もあります。データ・ストアとアプリ・サーバーの独立性が高まり、個別にスケーリングできるようになるからです。簡略化するため、少なくとも今のところはそのような構成にはしないことにしました。

クラスタ・メンバー・クライアントを使う利点は、クライアントがクラスタ・トポロジとパーティションの割当てを完全に認識している点にあります。そのため、クライアントは、ネットワーク・ホップ1回だけで、Coherence CEに格納されているすべてのデータ・オブジェクトに直接アクセスすることができます。欠点は、クライアントが他のクラスタと同一の、高速で低遅延のネットワーク上に存在しなければならない点です。さらに、クライアントが暴走を始めて無応答になった場合、クラスタ全体を不安定にしてしまう可能性もあります。

一方、リモート・クライアントは少し異なる仕組みで動作します。他のクラスタ・メンバーに直接接続するのではなく、プロキシ・サーバーに接続します。通常、このプロキシ・サーバーはストレージを無効化したクラスタ・メンバーです。

Coherenceでは以前より、Coherence*ExtendとCoherence RESTプロキシという2種類のプロキシをサポートしています。そこに、3つ目の種類として、Coherence gRPCが加わりました。

Coherence*Extend : 独自仕様による、TCPベースのRPCプロトコルで、オラクルの既存のJava、.NET、C++のクライアント実装で使用します。Coherence*Extendはかなり前(2006年)から存在しており、下位互換性および上位互換性を保つ形で多くのバージョンのOracle Coherenceに対応しています。また、多くのミッション・クリティカルなアプリケーションで使用されている実績があります。その一方で、Coherence*Extendでは同期的な独自処理が使用されているため、最新のクラウド技術と相性がよいとは限りません。さらに、Coherence*Extendを使って実装されたクライアントには、やや古いため、言語でサポートされている最新機能をほとんど使用していないものもあります(特に.NETクライアント)。

Coherence REST プロキシ:このプロキシでは汎用REST APIを実装しており、Coherence CEで管理されているプラットフォームや言語データの大半からアクセスすることができます。ただし、RESTや、ベースとなるHTTPプロトコル自体の性質上、多少の制限があり、機能はネイティブ・クライアントほど充実していません。また、構成を行うのが少しばかり面倒かもしれません。
率直に言うなら、このRESTプロキシに意義があった時期もありましたが、今ではそれほどニーズや用途はないものと思われます。前回の記事で紹介しましたが、完全なJava APIを自由に使用し、Helidon統合(推奨)や組込みHTTPサーバーを通して公開する、アプリケーション固有のREST APIを実装することも、Coherence RESTプロキシと同じくらい容易です。

Coherence gRPC : 最新のCoherence CEリリース(20.06)で導入された3つ目のプロキシです。
Coherence gRPCは、転送にgRPCを使用するプロキシ実装で、Coherence*Extendの現実的な代替機能です。Helidon gRPC Server上に構築されているこのプロキシから、直接得られるメリットがいくつかあります。最新のクラウド技術との相性がはるかによく、さまざまなHTTPロードバランサや、KubernetesのIngressコントローラに対応しており、非同期的です。また、gRPC自体も、関連するほぼすべてのプラットフォームや言語でサポートされています。

現時点で、オラクルが提供しているのはCoherence gRPCのネイティブJavaクライアントのみで、後ほどJavaFXクライアントを実装するときはこのクライアントを使います。しかし、順調にいけば近いうちにネイティブのNode.js/JavaScriptクライアントが公開されるでしょう。また、最新の.NETおよびC++のクライアントに加え、新たにPython、Golang、Swiftのクライアントも登場する予定です。

公式にサポートされていないプラットフォーム用のクライアントが必要な場合は、自分で記述できるようになるでしょう。オラクルがサポートするサービスおよびメッセージのプロトコル・バッファ定義は、すでに一般公開されています。独自のクライアントを実装する場合に、既存のクライアント実装を参考にすることができるようになるでしょう。

リモート・クライアントの利点は、同じネットワーク上に存在しなくてもよいことです。また、必要な数だけ導入でき、クラスタのメンバー構成に影響を与えることなく、自由に追加したり削除したりすることができます。当然ながら、Java以外の言語で書くこともできます。欠点は、クライアントからのすべてのリクエストがプロキシを経由する必要があるため、すべての操作に対してネットワークのホップが1回追加され、待機時間も必要になることです。

前置きはこのくらいにして、クライアントの実装に取りかかりましょう。

 

React クライアントの実現

前回の記事でも触れましたが、筆者はフロントエンドの開発者ではありません。Reactを選んだことにも深い意味はなく、(多少)経験があったというだけの理由です。AngularやVue.jsなど、人気のある他のフロントエンド・フレームワークで同じようなフロントエンドを実装することも難しくないでしょう。

実は、筆者の同僚であるTim Middletonがすでに別のWebフロントエンドを実装しています。こちらはOracle JavaScript Extension Toolkit(Oracle JET)を使ったものですが、UIをデータ・モデルにバインディングし、イベントを通して更新するという考え方は同じです。Oracle JETクライアントのソース・コードはGitHubで公開されていますので、興味がある方はご覧ください。

ここで取り上げたすべてのフレームワークには、標準のNode.js開発ツールチェーンを使ってUIのビルドとテストを行うことができるという共通点があります。そしてその結果に納得したら、アプリケーションを「コンパイル」して一連の静的なHTML、JavaScript、CSSファイルを生成し、静的コンテンツの提供が可能な任意のWebサーバーで公開することができます。

そのため、サンプル・アプリケーションでは、REST APIを提供しているHelidon Web Serverを流用し、そこで静的なフロントエンドも併せて公開することができます。このアプローチでは、アプリケーションがかなり簡略化されます。別のサーバーのデプロイや管理を行う必要がありません。また、フロントエンドとREST APIが同じオリジン上に存在するので、クロスオリジン・リソース共有(CORS)の問題に対処する必要もありません。

 

React クライアントのセットアップ

Reactクライアントを構築するために、いくつかのことを決める必要があります。

  • フロントエンドのソースをどこに配置するか
  • 既存の Maven ビルドの一部としてどのようにフロントエンドを構築するか
  • Helidon で提供するためには、「コンパイルした」フロントエンドをどのようにパッケージングすればよいか

これらを逆の順番で決めていくことにします。

Helidonでは、静的コンテンツをファイル・システムから提供することも、クラスパスから提供することもできます。後者の方がはるかに管理しやすいので、ここではフロントエンドのすべての静的ファイルをサーバーJARファイルにパッケージングし、そこからコンテンツを提供するようにHelidonを構成します。具体的には、以下の内容のMETA-INF/microprofile-config.propertiesファイルをプロジェクトに追加します。


server.static.classpath.location=/web
server.static.classpath.welcome=index.html

必要な作業はこれだけです。これで、クラスパスのwebディレクトリから静的コンテンツを提供し、index.htmlをデフォルトのウェルカム・ファイルとして使うようにHelidonが構成されました。

Mavenビルドの一部としてフロントエンドを構築し、Helidonが想定する場所にフロントエンドの静的コンテンツが格納されるようにするには、どうすればよいでしょうか。これを行うために、筆者が作成したnpm-maven-pluginを次のようにして使用します。このプラグインにより、npmベースのビルドをMavenビルドに埋め込みます。


<plugin>
  <groupId>com.seovic.maven.plugins</groupId>
  <artifactId>npm-maven-plugin</artifactId>
  <version>1.0.4</version>
  <executions>
    <execution>
      <id>build-frontend</id>
      <goals>
        <goal>run</goal>
      </goals>
      <phase>generate-resources</phase>
      <configuration>
        <workingDir>${project.basedir}/src/main/web</workingDir>
        <script>build</script>
      </configuration>
    </execution>
  </executions>
</plugin>

上記のコードによって、Mavenビルドのgenerate-resourcesフェーズに、package.jsonで定義されたビルド・スクリプトが実行されます。また、このコードでは、フロントエンドのソース・コードをサーバー・プロジェクトのsrc/main/webディレクトリ以下に格納することが明示されています。図2をご覧ください。

図2:Maven プロジェクトの一部になったフロントエンド・アプリケーション

フロントエンド・アプリケーション(とその開発者)は、アプリケーションを取り囲むMaven構造をまったく意識していないことに注意してください。フロントエンド・アプリケーションの開発者にとっては、上記の図で選択されているwebディレクトリがJavaScriptプロジェクトのルートです。

この構成なら、フロントエンド開発者がMavenに習熟していなくても作業を進めやすくなります。ただしこれは、フロントエンドをビルドした後、生成された静的ファイルをMavenが認識している構造にコピーしなければならないということを意味します。この点は、標準のmaven-resources-pluginアーティファクトIDを使えば簡単に解決します。


<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>3.1.0</version>
  <executions>
    <execution>
      <id>copy-frontend</id>
      <phase>generate-resources</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>
           ${project.build.directory}/classes/web
        </outputDirectory>
        <resources>
          <resource>
            <directory>${project.basedir}/src/main/web/build</directory>
            <filtering>true</filtering>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>

以上で完成です。これでプロジェクト構造が完成したので、いよいよ実装に入ることにしましょう。
 

 

React クライアントの実装

本記事でフロントエンド実装のすべてを説明することはできません。UIに関連するコードの大半はCoherence CEとは無関係で、すぐに本の1章分を費やすことにもなってしまうからです。コードは、アプリケーションの他の部分と合わせてGitHubで公開しているので、興味のある方はリポジトリをクローンして詳細を確認してみてください。

ここでは、クライアント・アプリケーションの状態管理の側面に加え、フロントエンド・アプリと、前回の記事で作成したバックエンドREST APIとの間の相互作用に着目します。

クライアントでのローカルの状態管理には、Reduxを使います。Reduxが唯一の選択肢というわけではありませんが、今回のアプリケーションで実装するイベント駆動アーキテクチャにはうまく適合します。Reduxでは、reducerにアクションをディスパッチすることで、アプリケーションの状態を更新します。厳密に言えば、Reduxでは状態を不変として扱うので、実際には何も更新してはいません。正確には、既存の状態と、受け取ったアクションに基づいて、新しい状態を作成しています。

まだよくわからない方のために(筆者自身も、完全に理解するまでしばらく時間がかかりました)、To Doリストのクライアントサイド表現のreducerを見てみることにしましょう。うまくいけば、理解の助けになるはずです。


export default function todos(state = [], action = null) {
  switch (action.type) {
    case INIT_TODOS:
      return action.todos || [];

    case ADD_TODO:
      return [
        {
          id: action.id,
          createdAt: action.createdAt,
          completed: false,
          description: action.description
        },
        ...state
      ];

    case DELETE_TODO:
      return state.filter(todo =>
        todo.id !== action.id
      );

    case UPDATE_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, description: action.description } :
          todo
      );

    case COMPLETE_TODO:
      return state.map(todo =>
        todo.id === action.id ?
          { ...todo, completed: action.completed } :
          todo
      );

    default:
      return state;
  }
}

上記の関数では、To Doリストの状態を扱うreducerを定義しています。switch文の各条件は、それぞれ異なるアクション・タイプを扱っており、現在の状態と、受け取ったアクションのペイロードに基づいて新しい状態を返します。UIは、状態が変わるたびにその変化に反応し、適切に自身を更新します(そのため、このフレームワークは「反応」を表すReactと呼ばれています)。

理解すべき重要なことは、この状態はフロントエンド・アプリのローカルであり、この時点ではCoherence CEバックエンドで管理されている状態とは関係がないことです。この点を解決し、2つの状態を結びつけるためには、次のことが必要です。

  • アプリケーションがロードされたときに、ローカルの状態を初期化する
  • サーバーから受け取ったイベントに基づいてローカルの状態を更新する

ReactアプリケーションのメインのApp.jsコンポーネントでこの2つのタスクを実現するコードは、次のようになります。


let initialized = false;

function init(actions) {
    actions.fetchAllTodos();

    // サーバーサイド・イベントの登録
    let source = new EventSource('/api/tasks/events');

    source.addEventListener("insert", (e) => {
      let todo = JSON.parse(e.data);
      actions.addTodo(todo.id, todo.createdAt, todo.description);
    });

    source.addEventListener("update", (e) => {
      let todo = JSON.parse(e.data);
      actions.updateTodo(todo.id, todo.description, todo.completed);
    });

    source.addEventListener("delete", (e) => {
      let todo = JSON.parse(e.data);
      actions.deleteTodo(todo.id);
    });

    source.addEventListener("end", (e) => {
      console.log("end");
      source.close();
    });

    initialized = true;
}

const App = ({todos, actions}) => {
    if (!initialized) {
      init(actions);
    }

    return (
        <div>
        <Header />
        <TodoInput addTodo={actions.addTodoRequest}/>
        <MainSection todos={todos} actions={actions}/>
        </div>
    )
};

先ほどの2つのタスクには、どちらも上記のコードのinit関数で対処しています。この関数では最初に、次に示すfetchAllTodosアクションを呼び出します。するとREST APIが呼び出され、その結果がreducerにディスパッチされます。


export const initTodos = (todos) => ({type: types.INIT_TODOS, todos});

export function fetchAllTodos() {
  return (dispatch) => {
    request
      .get('/api/tasks')
      .end(function (err, res) {
        console.log(err, res);
        if (!err) {
          dispatch(initTodos(res.body));
        }
      });
  }
}

次に、サーバーで実装された、イベントのエンドポイントを使ってイベント・ソースを登録します。受け取ったそれぞれのイベントは、Reduxのreducerにディスパッチして処理されます。

アクション自体は、2つのグループに分かれています。1つはReduxが管理するローカルの状態を更新するものです。もう1つは、REST APIへのリモート呼出しを行い、Coherence CEでサーバーサイドの状態を更新するものです。

その他のローカル・アクションは、上記のinitTodosアクションと同様で、対応するアクション・タイプをペイロードに追加しているだけです。これにより、reducerでそのアクションを適用できます。たとえば、addTodoアクションは次のように定義されています。


export const addTodo = (id, createdAt, description) => 
                       ({type: types.ADD_TODO, id, createdAt, description});

一方、リモート・アクションでは、単純にサーバーに対してREST呼出しを行います。Reduxの状態を直接更新するのではなく、先ほど登録したイベント・リスナーを使って、サーバーサイドの状態の変更をローカルの状態に適用します。

たとえば、上記のTodoInputコンポーネントに渡されるaddTodoRequestアクションでは、単純にリクエストを送信してレスポンスをログに出力します。


export function addTodoRequest(text) {
  return (dispatch) => {
    request
      .post('/api/tasks')
      .send({description: text})
      .end(function (err, res) {
        console.log(err, res);
      });
  }
}

続いて、先ほど定義したinsertイベント・ハンドラで、JSON形式で受け取った実際のタスクをReduxの状態に追加します。


source.addEventListener("insert", (e) => {
  let todo = JSON.parse(e.data);
  actions.addTodo(todo.id, todo.createdAt, todo.description);
});

このリアクティブなイベント駆動アプローチは、次の2つの重要な結果につながります。

  • サーバーサイドの状態を変更する場合、サーバーにリクエストを送信することだけを考えればよいので、アクションの実装がしやすくなる。サーバーサイドの状態と、クライアントサイドの状態の動機について心配する必要はない。
  • どのくらいアンドが変更を行ったかにかかわらず、クライアント UI がサーバーサイドの状態の変化に反応することになる。

よく使われているデータ・ストアの実装の大半では、サーバーにポーリングを行ったり、クライアントの機能でUIを最新に保たなければならなかったりします。Coherence CEが実現するこのイベント駆動アプローチは、それよりもはるかに効率がよいことがおわかりいただけたかと思います。

Reactフロントエンドの実装についての説明は、これで終わりです。次は、JavaFXクライアントを実装します。

 

JavaFX クライアントの実現

JavaFXクライアント(図3参照)のほとんどは、前回の記事で説明した、サーバーサイドのREST API実装とよく似たものになります。同じNamedMap APIを使い、Coherence CEイベントを監視して、まったく同じデータ・アクセス手法を多く使用します。

図3:JavaFX クライアント

ただし、いくつか違う点があるので、その点について説明します。その前に、プロジェクトのセットアップを行う必要があります。

 

JavaFX クライアント・プロジェクトのセットアップ

ここでは、同じMavenプロジェクト内の別のモジュールとしてJavaFXクライアントを実装します。そこで、最初にクライアントのPOMファイルを作成します。


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.oracle.coherence.examples</groupId>
  <artifactId>todo-list-client</artifactId>
  <version>1.0.0-SNAPSHOT</version>

  <properties>
    <coherence.groupId>com.oracle.coherence.ce</coherence.groupId>
    <coherence.version>20.06</coherence.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-java-client</artifactId>
      <version>${coherence.version}</version>
    </dependency>
    <dependency>
      <groupId>${coherence.groupId}</groupId>
      <artifactId>coherence-json</artifactId>
      <version>${coherence.version}</version>
    </dependency>

    <!-- JavaFX の依存性 -->
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-controls</artifactId>
      <version>14.0.2.1</version>
    </dependency>
    <dependency>
      <groupId>org.openjfx</groupId>
      <artifactId>javafx-fxml</artifactId>
      <version>14.0.2.1</version>
    </dependency>

    <!-- CDI サポート -->
    <dependency>
      <groupId>de.perdoctus.fx</groupId>
      <artifactId>javafx-cdi-bootstrap</artifactId>
      <version>2.0.0</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.weld.se</groupId>
      <artifactId>weld-se-core</artifactId>
      <version>3.1.4.Final</version>
    </dependency>

  </dependencies>

</project>

上記のコードに、驚く点や非常に興味深い点は何もありません。Coherence JavaクライアントとJSONシリアライズのサポートに加え、JavaFXとContexts and Dependency Injection(CDI)のサポートに必要な依存性がコードに含まれていることに注意してください。

ただし、これだけでは不十分です。サーバーサイドとの通信を可能にするプロキシがクライアントにはないからです。この点を修正するため、サーバーのPOMファイルに次の依存性を追加します。


<dependency>
  <groupId>${coherence.groupId}</groupId>
  <artifactId>coherence-grpc-proxy</artifactId>
  <version>${coherence.version}</version>
</dependency>
<dependency>
  <groupId>${coherence.groupId}</groupId>
  <artifactId>coherence-json</artifactId>
  <version>${coherence.version}</version>
</dependency>

まとめると、ここには次の2つの依存性を記述しています。

  • Coherence gRPC プロキシ:gRPC クライアントが必要とする gRPC サービス実装が含まれる。
  • Coherence JSON : クライアントと同じ依存性。クライアントとサーバーとの間で使用する JSON シリアライズをサポートする。

これでほぼ十分です。Coherence gRPCプロキシはHelidon gRPC Serverを用いて構築されており、これは推移的依存性として追加されます。Helidon Web Serverと同じく、Helidon gRPC Serverがクラスパス内に存在すれば、CDIによって起動時に読み込まれます。そして、検出されたすべてのgRPCサービスが自動的にデプロイされます。他には何も必要ありません。このプロジェクトの目的においては、Helidon gRPC Serverのデフォルトの構成で問題なく動作するからです。

Mavenプロジェクトの作成に加え、META-INF/beans.xmlファイルを作成してCDIを有効化する必要があります。


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
        version="2.0"
        bean-discovery-mode="annotated"/>

これでクライアント・プロジェクトの準備が済み、サーバー・プロジェクトに必要な依存性を追加しましたので、クライアントの実装に着手できます。

 

JavaFX クライアントの実装

Reactクライアントと同じように、UIの実装について詳しく述べることはせず、Coherence CEと通信するコードについて重点的に説明します。最初の手順は、タスク用のデータ・モデル・クラスの作成です。このクラスは、サーバー側のクラスとほとんど同じです。


package com.oracle.coherence.examples.todo.client;

public class Task
    {
    private String id;
    private long createdAt;
    private String description;
    private Boolean completed;

    /**
     * Task インスタンスの作成
     *
     * @param description  タスクの説明
     */
    public Task(String description)
        {
        this.id = UUID.randomUUID().toString().substring(0, 6);
        this.createdAt = System.currentTimeMillis();
        this.description = description;
        this.completed = false;
        }

    // 簡潔さを優先し、アクセッサは省略
    }

サーバーサイドとクライアントサイドのTaskクラスの実装の違いは2点だけです。1つ目の違いは、クライアントのクラスでSerializableインタフェースを実装していないことですが、この点はさほど重要ではありません。しかし、2つ目の違いは重要なので、説明する必要があります。

2つのクラスには同じ一連のフィールドがあり、ほとんど同じように見えますが、違うパッケージに格納されているので、同じクラスではありません。つまり、Javaのシリアライズを使ってクライアントとサーバーとの間でデータをマーシャル(Javaオブジェクト化)することはできません。これを行うには、同じデータ・クラスを共有する必要があるからです。

クライアントとサーバーとの間でJSONを使ってデータをマーシャルする理由の1つは、そこにあります。そうすることにより、クライアントとサーバーの両方に対応したペイロードを、パイプの両側で別々のクラスにデシリアライズできます。Portable Object Format(POF)の使用を選択した場合も同じことが言えるでしょう。パフォーマンスの観点からすれば、こちらの方が優れた選択かもしれませんが、このサンプル・アプリケーションではパフォーマンスをそれほど気にしていません。

しかし、まだ1つの問題が残っています。前回のREST API実装では、JSONペイロードをデシリアライズするときに、強く型付けされたJAX-RSのメソッド・シグネチャから使用するクラスを推測できます。今回はそれとは異なり、Coherence CEのNamedMap<K,V>はジェネリック・インタフェースなので、型を推測することはできません。この問題を解決するために、CoherenceのJSONでは、シリアライズを行う際に、デフォルトで型情報をJSONペイロードに含めるようになっています。この型情報は、JSONペイロードの@classメタプロパティの部分にあります。

たとえば、サーバーサイドのインスタンスをシリアライズしたJSONペイロードは、次のようになります。


{
  "@class": "com.oracle.coherence.examples.todo.server.Task",
  "id": "a3f764",
  "completed": true,
  "createdAt": 1596105656378,
  "description": "Write an article"
}

問題に気づいたでしょうか。JSONペイロードに埋め込まれているクラス名は、クライアントには存在しません。ありがたいことに、Javaのシリアライズとは異なり、CoherenceのJSONでは型エイリアスがサポートされています。この仕組みを使うと、同じエイリアスを使ってサーバーとクライアントで別々のクラスを登録し、両方に対応したJSONペイロードを生成することができます。

これを実現するには、クライアントとサーバーの両方でGensonBundleProviderを実装します


public class JsonConfig
        implements GensonBundleProvider
    {
    @Override
    public GensonBundle provide()
        {
        return new GensonBundle()
            {
            public void configure(GensonBuilder builder)
                {
                builder.addAlias("Task", Task.class);
                }
            };
        }
    }

上記のコードにはパッケージ名を含めていませんが、パッケージ名を除けばクラスはまったく同じです。クラスパス上で利用できるものが、Taskの実装として使われます。

次に、META-INF/servicesディレクトリにcom.oracle.coherence.io.json.GensonBundleProviderファイルを追加します。これは、サービス・ローダーがカスタム・プロバイダを検出できるようにし、サーバーサイドではcom.oracle.coherence.examples.todo.server.JsonConfig、クライアントサイドではcom.oracle.coherence.examples.todo.client.JsonConfigの内容をもとにカスタム・プロバイダを構成できるようにするためです。

必要なJSON構成を配置すると、先ほどのペイロードは次のようになります。


{
  "@type": "Task",
  "id": "a3f764",
  "completed": true,
  "createdAt": 1596105656378,
  "description": "Write an article"
}

このペイロードは、クライアントとサーバーで、ローカルに登録されたTaskクラスの実装を使ってデシリアライズできます。

なお、CoherenceのJSONでは、大幅にカスタマイズされた組込みバージョンのGenson JSONシリアライザを使っていることに注意してください。この組込みのGensonシリアライザは、アプリケーションが公式のGensonリリースも使う場合に競合しないよう、別のパッケージに格納されています。

ほとんどの場合、この点について気にかける必要はありません。必要に応じてデータ・クラスにJSON-BまたはJacksonのアノテーションを付加していれば、すべて問題なく動作するはずです。また、その他の目的で別のJSON実装を使うこともできます。実際には、以上が完了すると、REST APIではJSON-Bのリファレンス実装であるEclipse Yasson(Helidonに含まれています)を使います。

それでは、その他のクライアント実装の確認を続けましょう。重要なロジックは、すべてTaskManagerクラスに含まれています。


@ApplicationScoped
public class TaskManager
    {
    /**
     * 完了したタスクを取得する {@link Filter} 
     */
    private static final Filter<Task> COMPLETED = 
            Filters.equal("completed", true);

    /**
     * アクティブなタスクを取得する {@link Filter}
     */
    private static final Filter<Task> ACTIVE = 
            Filters.equal("completed", false);

    /**
     * タスクのマップ
     */
    @Inject
    @Remote
    private NamedMap<String, Task> tasks;

    ...
    }

上記のコードはおなじみのはずです。さまざまなクエリーに使用する2つのフィルタに対して静的フィールドを定義し、タスクを含むNamedMapを注入しています。

ただし、重要な違いが1つあります。Coherence Java gRPCクライアントが取得したNamedMapを注入するには、注入ポイントに@Remote修飾子を追加しなければなりません。これを行わないと、Coherence CDI拡張機能はデフォルトのNamedMap実装を注入し、JavaFXクライアントがクラスタに参加しようとするでしょう。これは望む動作ではありません。

上記のコードを動作させるには、正しいセッションを使うようにgRPCクライアントを構成する必要があります。

セッションでは、サーバーとの接続に使用するgRPCチャネルや、使用するシリアライザを定義します。それでは、クライアント・モジュールのsrc/main/resourcesディレクトリに次のapplication.yamlファイルを追加しましょう。


coherence:
  sessions:
    - name: default
      serializer: json
      channel: default

このファイルでは、クライアントがJSONシリアライザとデフォルトのgRPCチャネルを使用するように構成しています。これにより、クライアントはlocalhost:1408のgRPCサーバーに接続を試みます。

テストで使用するセットアップはまさにこのとおりであるため、今のところはこれで問題ありません。しかし、サーバーをKubernetesにデプロイしたら、クライアントから接続するために追加の構成が必要になります。次回の記事ではこれを行います。

朗報なのは、構成にHelidon MP Configを使っているため、上記の値はすべて、システム・プロパティや環境変数を使って簡単に上書き(または新しい値を追加)できることです。この点も、次回の記事で説明したいと思います。

なお、クライアントはJSONシリアライザを使うように明示的に構成していますが、プロキシでそのようなことはしていない点に注意してください。ありがたいことに、そうする必要はありません。プロキシは、利用可能なシリアライズ形式(CDIまたはサービス・ローダーで検出されます)をすべてサポートし、クライアントから指示されたシリアライザを使用します。

理論的には、クライアントがリクエストごとに異なる形式を使用する可能性もあります。しかし実際のところ各クライアントは、プロキシに接続している間、上記のセッションで構成されている形式を使い続けることになるでしょう。

TaskManagerの実装についての説明を続けましょう。次は、データ・アクセス・メソッドに注目します。


public void addTodo(String description)
    {
    Task todo = new Task(description);
    tasks.put(todo.getId(), todo);
    }

public Collection<Task> getAllTasks()
    {
    return tasks.values();
    }

public Collection<Task> getActiveTasks()
    {
    return tasks.values(ACTIVE);
    }

public Collection<Task> getCompletedTasks()
    {
    return tasks.values(COMPLETED);
    }

public void removeTodo(String id)
    {
    tasks.remove(id);
    }

public void removeCompletedTasks()
    {
    tasks.invokeAll(COMPLETED, Processors.remove(Filters.always()));
    }

public void updateCompleted(String id, Boolean completed)
    {
    tasks.invoke(id, Processors.update("setCompleted", completed));
    }

public void updateText(String id, String description)
    {
    tasks.invoke(id, Processors.update("setDescription", description));
    }

上記のコードはどこも、REST APIで実装したコードによく似ています。このコードでは、tasksマップに対する基本的な作成、読取り、更新、削除(CRUD)操作を実装するために、標準のMap API(put、remove、valuesなど)と、NamedMap API(invoke、invokeAll、そしてフィルタを受け取る、valuesのオーバーロードなど)を使っています。

Coherenceのアグリゲータ:TaskManagerの2つの追加メソッドでは、まだ説明していないCoherence CEの機能を使っています。それがアグリゲータです。


public int getActiveCount()
    {
    return tasks.aggregate(ACTIVE, Aggregators.count());
    }

public int getCompletedCount()
    {
    return tasks.aggregate(COMPLETED, Aggregators.count());
    }

Coherence CEのアグリゲータを使うと、クラスタ全体を対象にMapReduceスタイルの並列集計を行うことができます。

上記の例では、とても基本的なCountアグリゲータを使っています。このアグリゲータは指定されたフィルタに該当するエントリの数を返すだけです。しかし、他にも多くの組込みアグリゲータが搭載されており、特定の属性値の最大値や最小値の検索や平均値の計算、さらにはある属性でエントリをグループ化したり、エントリのグループごとに別々の集計を行ったりすることもできます。また、専用のカスタム・アグリゲータの実装も可能です。

アグリゲータは並列に実行されるので、非常に効率的です。また、正しく実装されていれば、ほぼ線形的にスケールアップすることができます。さらに、クラスタのメンバーが変更されたり、データのリバランスが行われたりすると、アグリゲータは自動的に再実行されます。そのため、クライアント・アプリケーションがその点を意識する必要はありません。

上記の例では、各メンバーが指定された条件を満たすローカルのエントリの数を算出し、クライアント(この場合は、gRPCプロキシ)で実行されているルート・アグリゲータに部分結果を返します。その後、ルート・アグリゲータが部分結果を結合して最終結果を計算します。
この例の最終結果はスカラー値ですが、実際はどんな結果でも構いません。

ところで、Coherence CEでは、Java 8で導入されたStream APIのカスタム実装も提供されていますが、この実装にはアグリゲータが使用されています。Oracle Coherence Remote(詳細はこちらのビデオをご覧ください)を使うと、カスタム・アグリゲータによってすべてのクラスタ・メンバーにストリーム・パイプライン定義が送信され、並列に実行されます。この場合、1つのJVMやいくつかのCPUコアではなく、おそらく数百のJVMと数千のコアで実行されます。

前回の記事で、REST APIでCoherence CEイベントを監視し、Web UIが受け取れるServer-Sent Events(SSE)に変換する必要があったように、ここでも同じようなことを行う必要があります。異なる点は、Coherence CEのMapEventsをSSEイベントに変換するのではなく、標準のCDIイベントに変換することです。こうすることで、JavaFXのUI実装は、完全にCoherence CEに依存しない状態を維持でき、発行されるCDIイベントを監視するだけで済みます。


@Inject
private Event<TaskEvent> taskEvent;

/**
 * Coherence の Map events を CDI イベントに変換
 */
void onTaskEvent(@Observes @MapName("tasks") MapEvent<String, Task> event)
    {
    taskEvent.fire(new TaskEvent(event.getOldValue(), event.getNewValue()));
    }

JavaFXのクライアント実装の説明は以上です。次は、新しいクライアントが期待どおりに動作し、curlを使わなくて済むようになるかどうかを確認しましょう。

 

クライアントの実行

アプリケーションの実行に必要なコードを本記事ですべて説明したわけではないため、GitHub上のサンプル・コードをまだ取得していない場合は、ここで取得してください。

Reactクライアントにアクセスするには、サーバーをビルドして実行する必要があります。このサーバーでは、フロントエンド・アプリケーションと、フロントエンドが使用するREST APIが提供されます。

前回の記事では、IDEでそのままサーバーを実行できました。しかし今回は、フロントエンドをビルドしてサーバーのJARファイルにパッケージングするために、Mavenを使ってサーバーをビルドする必要があります。最新バージョンのNode.jsとnpmをインストールしている場合、この操作はサーバー・ディレクトリでmvn installを実行するだけで簡単に実現できます。

まず、server/src/main/webディレクトリでnpm installを実行し、フロントエンドに必要な依存性をインストールする必要があります。これは一度だけ行う必要があります。

すると、以前と同じようにIDEでサーバーを実行できるようになります。または、コマンドラインからmvn exec:execを実行しても構いません。すべてがうまくいけば、サーバーの起動が表示され、数秒後には第1回の記事で見たものと同じHelidonのログ・メッセージが表示されるはずです。


2020.08.11 03:16:00 INFO io.helidon.microprofile.server.ServerCdiExtension Thread[main,5,main]: Server started on http://localhost:7001 (and all other host addresses) in 11967 milliseconds (since JVM startup).

これで、上記のログ・メッセージからわかるように、http://localhost:7001/にアクセスするだけで、Reactのフロントエンド(図4参照)にアクセスできるようになっています。

図4:React クライアントの UI

アプリケーションを触ったり、タスクをいくつか作ったりしてみてください。アプリケーションをもっとおもしろくしたい場合は、複数のブラウザ・ウィンドウでアプリケーションを開き、変更を行った際に、イベントを通してすべてのウィンドウの同期が保たれる様子を確認してください。

最後に、クライアント・ディレクトリでmvn installを実行し、続いてmvn javafx:runを実行して、JavaFXクライアントを起動します。図5に示すものと同様のUIが表示されるはずです。

図5:JavaFX クライアントの UI

すぐにわかることですが、タスク・リストに初期表示されているのは、先ほどReactクライアントを使って作成したタスクです。ここでも同じように、タスクをいくつか追加したり、既存のタスクを変更したりしてみてください。そして、変更を行った際に、両方のアプリケーションの同期が保たれる様子を確認してください。

自分でコードを実行したくない方は、こちらのビデオをご覧ください。

 

まとめ

長い記事になってしまいましたが、今回はここまでです。ローカルで実行できるTo Doリスト・アプリケーションを作ることができました。ただし、最初に言っておきますが、これはさほど便利なものではありません。

第3回となる最後の記事では、このちょっとしたデモ・アプリケーションをKubernetesクラスタにデプロイし、本番環境向けの高可用性アプリケーションにします。このアプリケーションは、簡単にスケールアウトできるだけでなく、PrometheusやGrafana、Jaegerを使って監視することができます
 

さらに詳しく

 

Aleks Seović

Aleks Seović(@aseovic):オラクルのアーキテクト。主要なインメモリ・データ・グリッド製品であるOracle Coherenceに携わるとともに、Helidonマイクロサービス・フレームワークにも貢献。最近では、HelidonのgRPCフレームワークや、CoherenceのCDIおよびEclipse MicroProfileサポートの設計と実装を主導した。現在は、Coherenceネイティブ・クライアント、GraphQLサポート、Spring統合の実装を率いている。2016年にオラクルに入社する前は、専門のコンサルタント会社を率いて、世界中の顧客がCoherenceを用いたミッション・クリティカルなアプリケーションを実装する作業をサポートしていた。『Oracle Coherence 3.5』(Packt Publishing、2010年)の著者であるほか、業界のカンファレンス、Javaや.NETユーザー・グループのイベント、Coherence SIGで頻繁に講演を行い、Coherenceの普及に努めている。