48 多言語Coherenceアプリケーションの開発

この章では、サーバー側のJavaScriptを使用して共通のCoherenceサーバー側の操作を実装するアプリケーションを開発することによって、Coherenceの多言語機能について説明します

この機能を使用するには、Oracle GraalVM Enterprise EditionでOracle Coherenceを実行する必要があります。GraalVM Enterprise EditionでのOracle WebLogic ServerおよびCoherenceの実行を参照してください。

ノート:

この章の例をビルドして実行するには、Maven 3.8.6以上がインストールされている必要があります。

ノート:

GraalVM多言語ランタイムは現在仮想スレッドをサポートしていないため、多言語機能を使用するキャッシュ・サービスに対して仮想スレッドが有効になっていないことを確認してください。

この章の内容は次のとおりです。

プロジェクトの設定

Coherenceの多言語機能を示すために、Coherenceマップを移入する単純なアプリケーションを作成し、JavaScriptを使用して、作成したマップ・エントリにアクセスして処理します。

すべてのCoherence記憶域メンバーにデプロイされるサーバー側クラスのMaven Javaプロジェクトがすでにある場合は、src/main/jsディレクトリを作成して、その中にJavaScriptプロジェクトを作成できます。

それ以外の場合は、まずMavenプロジェクトを作成し、次にsrc/main/jsディレクトリの下にJavaScriptプロジェクトを作成する必要があります:
  1. Mavenプロジェクト・ディレクトリに移動します。
    cd coherence-js-app
    export PRJ_DIR=`pwd`
    mkdir -p src/main/js
    cd src/main/js
    export JS_DIR=`pwd`

    このメイン・プロジェクト・ディレクトリは、シェル環境変数${PRJ_DIR}で参照できるようになりました。JavaScriptプロジェクト・ディレクトリは、${JS_DIR}環境変数を使用して参照できます。

  2. JavaScriptプロジェクト・ディレクトリで、次のように実行します:
    npm init -y

    このコマンドを実行すると、package.jsonファイルが作成されます。このファイルは後で編集する必要があります。

  3. 他のモジュールに定義されているすべてのJavaScriptクラスをエクスポートするために使用されるアグリゲータ・モジュールmain.mjsを作成します。
    echo > main.mjs
  4. 新しく作成したmain.mjsモジュールを指すように、package.json内のメイン属性を変更します。
    {
      "name": "coherence-js-app",
      "version": "1.0.0",
      "main": "main.mjs",
      …
    }

JavaScriptクラスの実装

ここでは、Coherenceマップのエントリへのアクセスおよび更新に使用できるJavaScriptクラスを実装します。
わかりやすくするために、マップにfirstNamelastNameagegenderなどの属性を持つPersonオブジェクトが移入されているとします。
public class Person
    {
    private String firstName;
    private String lastName;
    private int age;
    private String gender;

    // constructors and accessors omitted for brevity
    }

これで、問合せ、集計および更新を可能にするJavaScriptクラスを実装できます。

この項には次のトピックが含まれます:

フィルタの使用

NamedMapには、エントリのキーに基づいてエントリにアクセスできるget()およびput()メソッドがあります。ただし、多くの場合、キー以外の属性に基づいてエントリを取得する必要があります。Coherenceでは、このような状況に対応するFilterインタフェースを定義しています。

たとえば、10代、つまり13-19歳の年齢層に属する人をすべて見つけるとします。これを実装する方法の1つは、すべてのエントリを取得して、年齢が13から19の間のエントリのみを選択することですが、この方法は非効率的です。Coherenceは、述語を記憶域ノードに移動する際に役立ちます。これにより、大量のデータ移動が回避されるだけでなく、記憶域ノードに対する述語のパラレル実行も可能になります。

Coherenceでは、記憶域ノードのエントリにアクセスするための組込みフィルタがいくつか提供されます。「キャッシュ内のデータの問合せ」を参照してください。

この項には次のトピックが含まれます:

フィルタ・インタフェースの使用

Filterインタフェースでは、1つのメソッドevaluate(object)が定義されます。このメソッドは、評価するオブジェクトを引数とし、指定されたオブジェクトがフィルタによって定義された条件を満たす場合はtrueを返し、指定されたオブジェクトがフィルタによって定義された条件を満たさない場合はfalseを返します。

フィルタは、次の条件に従う必要があります:
  1. 1つの引数を取るevaluateというメソッドを含むクラスを定義してエクスポートする必要があります。
  2. objがフィルタで表される条件を満たす場合、evaluate(obj)trueを返す必要があります。それ以外の場合は、falseを返す必要があります。
フィルタの実装

その人が10代であるかどうかを確認するフィルタを作成するには、次を実行します。

ディレクトリを${JS_DIR}/src/main/jsに変更します。filters.mjsファイルを作成し、次の内容を追加します:

export class IsTeen { // [1]
  evaluate(person) {  // [2]
    return person.age >= 13 && person.age <= 19;
  }
}
前述の例は、次のとおりです。
  • [1] IsTeenというクラスを定義し、エクスポートします。
  • [2] 1つのパラメータを取るevaluateというメソッドを定義します。このメソッドは、age属性が13から19の間にあるかどうかをチェックします。

最後に、filters.mjsモジュールに定義されているすべてのクラスをアグリゲータ・モジュールmain.mjsから再エクスポートします:

export * from "./filters.mjs"

EntryProcessorsの使用

Coherenceには、キャッシュ・エントリに対してパラレル更新を実行するのに役立つ組込みのEntryProcessorsが用意されています。「エントリ・プロセッサ・エージェントの概要」を参照してください。

すべてのlastName属性が大文字になるように、(この章で使用している例のようにPersonオブジェクトで占められた)キャッシュ・エントリを更新する場合、その方法の1つは、すべてのエントリを取得し、反復処理して1つずつ更新し、最終的にキャッシュに書き戻すことです。

これは非効率的な方法です。Coherenceでは、データが存在する処理ロジックをより効率的に送るアプローチが用意されているため、データ移動の必要性がなくなります。キャッシュ・エントリのパラレル更新をより効率的に実行する必要がある場合は、EntryProcessorを使用する必要があります。

この項には次のトピックが含まれます:

EntryProcessorインタフェースの使用
EntryProcessorは、処理するマップ・エントリを引数とし、処理結果を返すprocess(entry)という必須メソッドを定義します。JavaScriptでEntryProcessorを実装するには、クラスが次のようになっている必要があります。
  1. 1つの引数(マップ・エントリ)を取るprocessという名前のメソッドを含む、エクスポートするクラスを定義します。
  2. process(entry)メソッドでマップ・エントリの値を変更する場合は、エントリを明示的に更新する必要があります。
EntryProcessorの実装

lastNameを大文字の値に更新するEntryProcessorを記述するには、processors.mjsファイルを作成し、次の内容を追加します:

export class UpperCaseProcessor { // [1]
    process(entry) { // [2]
      let person = entry.value;
      person.lastName = person.lastName.toUpperCase(); // [3]
      entry.value = person; // [4]
      return person.lastName; // [5]
    }
}
前述の例は、次のとおりです。
  • [1] UpperCaseProcessorというクラスを定義し、エクスポートします。
  • [2] 1つのパラメータを取るprocess()というメソッドを定義します。処理が必要なNamedMapエントリは、このメソッドに渡されるentry引数を通じてアクセス可能です。
  • [3] その人のlastNameを大文字に変換します。
  • [4] entryを新しい値で更新します。
  • [5] 更新された(大文字の)姓をプロセッサの実行結果として返します。
最後に、processors.mjsモジュールに定義されているすべてのクラスをアグリゲータ・モジュール main.mjsから再エクスポートします:
export * from "./ processors.mjs"

Aggregatorの使用

Coherence Aggregatorを使用して、特定の基準に基づいて、キャッシュから単一の集計結果を取得できます。たとえば、前述の例で作成したCache内の最も古いPersonのキーを取得します。すべてのエントリを取得し、最も年齢の高いPersonを見つけることもできますが、大量のデータをクライアントに移動することになります。

Coherenceでは、部分的な結果を計算し、それらの部分的な結果を結合して単一の集計結果を取得できるAggregatorインタフェースを定義しています。データ・グリッドの集計の実行を参照してください。

この項には次のトピックが含まれます:

Aggregatorインタフェースの使用

Aggregatorインタフェースでは、次のメソッドを実装する必要があります:

  1. accumulate(entry): すべてのメンバーに対してパラレルに実行し、単一エントリをそのメンバーの部分的な結果に累計します。このメソッドでは、entryの1つ以上の属性を使用して部分的な結果が計算されます。このメソッドは、特定のクラスタ・メンバーでの集計用に選択されたエントリごとに1回ずつ、Aggregatorに対して複数回コールされます。
  2. getPartialResult(): 各メンバーからのパラレル集計の部分的な結果を返します。
  3. combine(partialResult): 各クラスタ・メンバーから返された部分的な結果を最終結果に結合します。このメソッドでは、entryの1つ以上の属性を使用して部分的な結果が計算されます。このメソッドは、各クラスタ・メンバーの部分的な結果ごとに1回ずつ、ルートAggregatorインスタンスに対して複数回コールされます。
  4. finalizeResult(): 集計の最終結果を計算し、返します。
Aggregatorの記述

最も古いPersonのキーを返すAggregatorを定義するには、main.jsファイルを作成して、次の内容を追加します:

export class OldestPerson {
  constructor() {
    this.key = -1;
    this.age = 0;
  }

  accumulate(entry) {
    // Compare this entry's age with the result computed so far.    
    if (entry.value.age > this.age) {
      this.key = entry.key;
      this.age = entry.value.age;
    }
    return true;
  }

  getPartialResult() {
    // Return the partial result accumulated / computed so far.
    return JSON.stringify({key:this.key, age:this.age});
  }

  combine(partialResult) {
    // Compute a (possibly) new result from the previously computed
    // partial result.
    let p = JSON.parse(partialResult);

    if (p.age > this.age) {
      this.key = p.key;
      this.age = p.age;
    }
    return true;
  }
  
  finalizeResult() {
    // Return the final computed result.
    return this.key;
  }
}

最後に、Aggregatorモジュールmain.mjsから定義されたクラスを再エクスポートします:

export * from "./aggregators.mjs"

依存関係のインストールおよび使用

この項では、JavaScriptクラスを実装する際にサード・パーティの依存関係を使用する方法を説明します。これが、そもそもJavaScriptを使用する理由であることがよくあります。

npm installを使用する標準アプローチを使用して依存関係をインストールし、import文の機能を使用してそれらを使用できます。たとえば、lodash-esおよび@paravano/utilsパッケージをインストールするには、次のコマンドを使用します:

cd ${JS_DIR}
npm install -s lodash-es
npm install -s @paravano/utils

依存関係のインストール後、lodash-esパッケージのnow関数と@paravano/utilsパッケージのcamel関数を使用するエントリ・プロセッサを実装できます:

import {now} from 'lodash-es';
import {camel} from "@paravano/utils";

export class CamelCase {
  process(entry) {
    console.log(`> CamelCase: entry=${entry} time=${now()}`)
    entry.value = camel(entry.value);
    return entry.value;
  }
}

プロジェクトのビルド

JavaScriptクラスを実行時にCoherence多言語フレームワークで使用できるようにするには、JARファイルにパッケージ化する必要があります。

Coherence多言語フレームワークは、クラス・パスの任意のscripts/jsディレクトリから.mjs拡張子を持つすべてのスクリプトを自動的にロードします。そのため、最も単純なシナリオでは、外部依存関係がない場合、すべてのソース・ファイルをターゲット・ディレクトリ内の正しい場所にコピーするようMavenプロジェクトを構成するだけです。これにより、それらのファイルが他のすべてのクラスおよびリソースとともに最終JARにパッケージ化されます:

<build>
  <resources>
    <resource>
      <directory>${project.basedir}/src/main/resources</directory>
    </resource>
    <resource>
      <directory>${project.basedir}/src/main/js</directory>
      <targetPath>${project.build.outputDirectory}/scripts/js</targetPath>
      <includes>
        <include>**/*.mjs</include>
      </includes>
    </resource>
  </resources>
</build>

これを行う場合は、デフォルト・リソースも再定義する必要があります。そうすると、src/main/resourcesのすべてのファイルも、通常どおり出力ディレクトリにコピーされます。

ただし、外部依存関係がある場合(多くの場合その可能性が高い)、ソース・コードと依存する外部依存関係のコードの両方を出力ディレクトリにバンドルするには、バンドラを使用する必要があります。

このタスクで推奨されるバンドラはesbuildです。これは、高速で使いやすいためです。次に、これをインストールして使用し、JavaScriptプロジェクトを出力ディレクトリにバンドルします:

  1. esbuildをインストールします。
    npm install –-save-dev esbuild
  2. package.json内のビルド・スクリプトを構成して、メイン・モジュールに対してesbuildを実行します。
    {
      "name": "coherence-js-app",
      "version": "1.0.0",
      "main": "main.mjs",
      "scripts": {
        "build": "esbuild main.mjs --bundle --format=esm --charset=utf8 --outdir=../../../target/classes/scripts/js/ --out-extension:.js=.mjs"
      },
      "devDependencies": {
        "esbuild": "^0.24.0"
      }
    }
最後に、バンドラとサードパーティの依存関係を使用する場合は、優れたフロントエンドMavenプラグインを構成して次のことを行う必要があります:
  1. Node.jsおよびnpmをローカルにインストールします。
  2. npm installを実行して依存関係をインストールします。
  3. 前に定義したnpm run buildコマンドを使用してesbuildを実行して、スクリプトをバンドルします。
このすべてを簡単に実行するには、次のプラグイン構成をpom.xmlファイルに追加します:
<plugin>
  <groupId>com.github.eirslett</groupId>
  <artifactId>frontend-maven-plugin</artifactId>
  <executions>
    <execution>
      <id>install node and npm</id>
      <goals>
        <goal>install-node-and-npm</goal>
      </goals>
    </execution>
    <execution>
      <id>npm install</id>
      <goals>
        <goal>npm</goal>
      </goals>
      <configuration>
        <arguments>install</arguments>
      </configuration>
    </execution>
    <execution>
      <id>npm run build</id>
      <goals>
        <goal>npm</goal>
      </goals>
      <configuration>
        <arguments>run build</arguments>
      </configuration>
    </execution>
  </executions>
  <configuration>
    <nodeVersion>v20.17.0</nodeVersion>
    <installDirectory>target</installDirectory>
    <workingDirectory>src/main/js</workingDirectory>
  </configuration>
</plugin>

プロジェクト・ディレクトリでmvn installを実行すると、Node.jsおよびnpmがインストールされ、スクリプトとその依存関係がesbuildを使用して1つのtarget/classes/scripts/js/main.mjsファイルにバンドルされていることがわかります。

これで、Javaアプリケーションでそのモジュールに定義され、そのモジュールからエクスポートされたクラスを使用する準備ができました。

JavaアプリケーションでのJavaScriptクラスの使用

この項では、以前に作成したJavaScriptクラスを使用するJavaアプリケーションを記述して、多言語の例を完了します。

この項には次のトピックが含まれます:

ランタイム依存関係の追加

Javaアプリケーションから以前に作成したJavaScriptクラスを使用するには、Coherence自体、およびGraalVM多言語ランタイムおよびJavaScript言語実装に対する依存関係をpom.xmlファイルに追加する必要があります:
<dependencies>
  <dependency>
    <groupId>com.oracle.coherence</groupId>
    <artifactId>coherence</artifactId>
    <version14.1.2-0-0</version>
  </dependency>

  <!-- GraalVM Polyglot support -->
  <dependency>
    <groupId>org.graalvm.polyglot</groupId>
    <artifactId>polyglot</artifactId>
    <version>23.1.4</version>
  </dependency>
  <dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js-language</artifactId>
    <version>23.1.4</version>
  </dependency>
</dependencies>

Javaアプリケーションの実装

これで、Coherenceクラスタを起動し、ピープル・マップにいくつかのエントリを移入し、前に作成したJavaScriptクラスを使用して、ピープル・マップのエントリを問い合せて変更するJavaアプリケーションを実装する準備ができました。

次のコードを含むMain.javaというファイルをメイン・プロジェクト内に作成します:

package com.oracle.coherence.example.js;

import com.tangosol.net.Coherence;
import com.tangosol.net.NamedMap;

import com.tangosol.util.Aggregators;
import com.tangosol.util.Filters;
import com.tangosol.util.Processors;

import com.tangosol.util.filter.AlwaysFilter;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class Main
    {
    public static void main(String[] args)
        {
        System.setProperty("coherence.log.level", "1");
        
        try (Coherence coherence = Coherence.clusterMember().start().join())
            {
            NamedMap<Integer, Person> people = coherence.getSession().getMap("people");
            populatePeople(people);

            displayAllTeens(people);
            convertLastNameToUppercase(people);
            displayOldestManAndWoman(people);
            }
        }

    private static void populatePeople(NamedMap<Integer, Person> people)
        {
        printHeader("populatePeople");
        
        Map<Integer, Person> map = new HashMap<>();
        map.put(1, new Person("Ashley", "Jackson", "F", 84));
        map.put(2, new Person("John", "Campbell", "M", 36));
        map.put(3, new Person("Jeffry", "Trayton", "M", 95));
        map.put(4, new Person("Florence", "Campbell", "F", 35));
        map.put(5, new Person("Kevin", "Kelvin", "M", 15));
        map.put(6, new Person("Jane", "Doe", "F", 17));

        people.putAll(map);
        print(people);
        }

    private static void displayAllTeens(NamedMap<Integer, Person> people)
        {
        }

    private static void convertLastNameToUppercase(NamedMap<Integer, Person> people)
        {
        }

    private static void displayOldestManAndWoman(NamedMap<Integer, Person> people)
        {
        }

    private static void printHeader(String header)
        {
        System.out.println();
        System.out.println(header);
        System.out.println("-".repeat(header.length()));
        }

    private static void print(NamedMap<Integer, Person> map)
        {
        print(map.entrySet(AlwaysFilter.INSTANCE));
        }

    private static void print(Set<Map.Entry<Integer, Person>> entrySet)
        {
        TreeMap<Integer, Person> map = new TreeMap<>();
        for (Map.Entry<Integer, Person> e : entrySet)
            {
            map.put(e.getKey(), e.getValue());
            }
        for (Map.Entry<Integer, Person> e : map.entrySet())
            {
            System.out.printf("%d: %s%n", e.getKey(), e.getValue());
            }
        }
    }

JavaからのJavaScriptオブジェクトの起動

最後のステップとして、空の本体を使用して前の3つのメソッドを実装する必要があります。これには、前に実装したJavaScriptクラスを使用します。

この項には次のトピックが含まれます:

JavaScriptフィルタの起動

JavaScriptで記述されたFilterは、com.tangosol.util.Filtersユーティリティ・クラスでscript()メソッドをコールすることでインスタンス化できます。次のように定義されます。

public static <V> Filter<V> script(String language,
                                   String filterName,
                                   Object... args);

最初の引数は、スクリプトが実装されている言語を指定するために使用されます。JavaScriptの場合は、jsを使用します。2番目の引数はFilterexported名を指定するために使用し、最後の引数(オプション)はフィルタにコンストラクタ引数(ある場合)を渡すために使用できます。

Main.javaに次のメソッドを追加します:

private static void displayAllTeens(NamedMap<Integer, Person> people)
    {
    printHeader("displayAllTeens");

    print(people.entrySet(Filters.script("js", "IsTeen")));
    }
JavaScript EntryProcessorを使用したパラレル更新の実行

JavaScriptで記述されたEntryProcessorは、com.tangosol.util.Processorsクラスのscript()メソッドを呼び出すことでインスタンス化できます。

Main.javaに次のメソッドを追加します:

private static void convertLastNameToUppercase(
                                 NamedMap<Integer, Person> people)
    {
    printHeader("convertLastNameToUppercase");

    people.invokeAll(Processors.script("js", "UpperCaseProcessor"));
    print(people);
    }
JavaScriptアグリゲータの実行

JavaScriptで記述されたAggregatorは、com.tangosol.util.Aggregatorsクラスのscript()メソッドを呼び出すことでインスタンス化できます。

OldestPersonアグリゲータを組込みフィルタとともに使用して、最も年齢の高い男性と女性を見つけます。

Main.javaに次を追加します:

private static void displayOldestManAndWoman(NamedMap<Integer, Person> people)
    {
    printHeader("displayOldestManAndWoman");

    Integer oldestManId   = people.aggregate(
                                  Filters.equal(Person::getGender, "M"),
                                  Aggregators.script("js", "OldestPerson"));
    Integer oldestWomanId = people.aggregate(
                                  Filters.equal(Person::getGender, "F"),
                                  Aggregators.script("js", "OldestPerson"));

    System.out.printf("%d: %s%n", oldestManId, people.get(oldestManId));
    System.out.printf("%d: %s%n", oldestWomanId, people.get(oldestWomanId));
    }

これで、Javaアプリケーションを実行できます。すべてが正しく実装されている場合は、次の出力が表示されます:

Oracle Coherence Version 14.1.2.0.0 (dev-aseovic) Build 0
 Grid Edition: Development mode
Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.


populatePeople
--------------
1: Person[lastName=Jackson, firstName=Ashley, age=84, gender=F]
2: Person[lastName=Campbell, firstName=John, age=36, gender=M]
3: Person[lastName=Trayton, firstName=Jeffry, age=95, gender=M]
4: Person[lastName=Campbell, firstName=Florence, age=35, gender=F]
5: Person[lastName=Kelvin, firstName=Kevin, age=15, gender=M]
6: Person[lastName=Doe, firstName=Jane, age=17, gender=F]

displayAllTeens
---------------
5: Person[lastName=Kelvin, firstName=Kevin, age=15, gender=M]
6: Person[lastName=Doe, firstName=Jane, age=17, gender=F]

convertLastNameToUppercase
--------------------------
1: Person[lastName=JACKSON, firstName=Ashley, age=84, gender=F]
2: Person[lastName=CAMPBELL, firstName=John, age=36, gender=M]
3: Person[lastName=TRAYTON, firstName=Jeffry, age=95, gender=M]
4: Person[lastName=CAMPBELL, firstName=Florence, age=35, gender=F]
5: Person[lastName=KELVIN, firstName=Kevin, age=15, gender=M]
6: Person[lastName=DOE, firstName=Jane, age=17, gender=F]

displayOldestManAndWoman
------------------------
3: Person[lastName=TRAYTON, firstName=Jeffry, age=95, gender=M]
1: Person[lastName=JACKSON, firstName=Ashley, age=84, gender=F]

問題が発生した場合は、この章のプロジェクト例をGitHubで確認できます。