48 Developing Polyglot Coherence Applications
Using this functionality requires that you run Oracle Coherence with Oracle GraalVM Enterprise Edition. See Running Oracle WebLogic Server and Coherence on GraalVM Enterprise Edition.
Note:
To build and run the examples in this chapter, you must have Maven 3.8.6 or later installed.
Note:
The GraalVM Polyglot Runtime does not currently support virtual threads, so make sure that virtual threads are not enabled for the cache service for which you want to use polyglot features.
This chapter includes the following sections:
- Setting Up the Project
To demonstrate Coherence's polyglot capabilities, you will create a simple application that populates a Coherence map and then you'll use JavaScript to access and process the map entries you created. - Implementing JavaScript Classes
Now, you will implement JavaScript classes that you can use to access and update the entries in a Coherence map. - Installing and Using Dependencies
In this section, you'll see how you can use third-party dependencies when implementing your JavaScript classes, which often may be the reason to use JavaScript in the first place. - Building a Project
To make your JavaScript classes available to Coherence Polyglot Framework at runtime, you need to package them in a JAR file. - Using JavaScript Classes in a Java Application
In this section, you will complete the polyglot example by writing a Java application that uses the JavaScript classes that you created previously.
Parent topic: Developing and Running Polyglot Coherence Applications
Setting Up the Project
If you already have a Maven Java project for your server-side
classes that will be deployed to all Coherence storage members, then you can
simply create a src/main/js
directory and create your
JavaScript project within it.
src/main/js
directory:
- Change to your Maven project
directory.
cd coherence-js-app export PRJ_DIR=`pwd` mkdir -p src/main/js cd src/main/js export JS_DIR=`pwd`
This main project directory now can be referred to by the shell environment variable
${PRJ_DIR}
, and the JavaScript project directory can be referred to by using the${JS_DIR}
environment variable. - In the JavaScript project directory,
run:
npm init -y
Running this command will create a
package.json
file, which you will need to edit later. - Create aggregator module,
main.mjs
, that will be used to export all of the JavaScript classes defined in other modules.echo > main.mjs
- Modify the main attribute within
package.json
to point to the newly createdmain.mjs
module.{ "name": "coherence-js-app", "version": "1.0.0", "main": "main.mjs", … }
Parent topic: Developing Polyglot Coherence Applications
Implementing JavaScript Classes
Person
objects with attributes, such as firstName
,
lastName
, age
, and
gender
.public class Person { private String firstName; private String lastName; private int age; private String gender; // constructors and accessors omitted for brevity }
Now, you can implement JavaScript classes that will allow you to query, aggregate, and update.
This section includes the following topics:
Parent topic: Developing Polyglot Coherence Applications
Using Filters
NamedMap
provides the get()
and
put()
methods that allow you to access entries
based on the entries' keys. However, in many cases you may want to retrieve
entries based on attributes other than the key. Coherence defines a
Filter
interface for these situations.
Assume that you want to find all those persons who are in their
teens, in the age group 13-19
. One way to implement this is
to retrieve all entries and pick only those whose age is between 13 and 19,
but this method is inefficient. Coherence helps to move the predicate to the
storage nodes, which not only avoids huge data movement, but also enables
parallel execution of the predicate on the storage nodes.
Coherence provides a number of built in filters to access entries on the storage nodes. See Querying Data in a Cache.
This section includes the following topics:
Using a Filter Interface
The Filter
interface defines a single method
evaluate(object)
, which takes an object to evaluate
as an argument, and returns true
if the specified object
satisfies the criteria defined by the filter, or false
if
the specified object does not satisfy the criteria defined by the filter.
- Must define and export a class that has one method,
named
evaluate
, that takes one argument. - The
evaluate(obj)
must returntrue
ifobj
satisfies the criteria represented by the filter. Otherwise, it should returnfalse
.
Parent topic: Using Filters
Implementing a Filter
To write a filter that checks if the person is in his or her teens, do the following.
Change directory to ${JS_DIR}/src/main/js
.
Create the filters.mjs
file and add the following content:
export class IsTeen { // [1] evaluate(person) { // [2] return person.age >= 13 && person.age <= 19; } }
- [1] Defines and exports a class named
IsTeen
. - [2] Defines a method named
evaluate
that takes a single parameter. This method checks if theage
attribute is between 13 and 19.
Finally, re-export all classes defined in the filters.mjs
module from the
aggregator module, main.mjs
:
export * from "./filters.mjs"
Parent topic: Using Filters
Using EntryProcessors
Coherence provides built in EntryProcessors
which
help to perform parallel updates to cache entries. See Overview of Entry Processor Agents.
If you want to update cache entries (that are filled with
Person
objects as per the example used in this
chapter) such that all lastName
attributes are in
uppercase, one way to do this is to retrieve all entries, iterate over them
and update them one by one, and finally write them back into the cache.
This is an inefficient method. Coherence provides a more efficient approach of
shipping the processing logic where the data resides and thus eliminating
the need for data movement. If you need to perform parallel updates to cache
entries efficiently, you should use an EntryProcessor
.
This section includes the following topics:
Parent topic: Implementing JavaScript Classes
Using an EntryProcessor Interface
EntryProcessor
defines a mandatory method called
process(entry)
, which takes a map entry to process
as an argument and returns the result of the processing. To implement an
EntryProcessor
in your JavaScript, the class must:
- Define an exported class with one method named
process
that takes one argument (which will be the map entry). - If the
process(entry)
method mutates the map entry 's value, then it must update the entry explicitly.
Parent topic: Using EntryProcessors
Implementing an EntryProcessor
To write an EntryProcessor
that updates
lastName
to its uppercase value, create the
processors.mjs
file and add the following
content:
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] Defines and exports a class named
UpperCaseProcessor
. - [2] Defines a method called
process()
that takes a single parameter. TheNamedMap
entry that needs to be processed is accessible through theentry
argument that is passed to this method. - [3] Converts the person's
lastName
to uppercase. - [4] Updates the
entry
with the new value. - [5] Returns the updated (uppercase) last name as the result of the processor execution.
processors.mjs
module from the aggregator module,
main.mjs
:export * from "./ processors.mjs"
Parent topic: Using EntryProcessors
Using Aggregators
You can use a Coherence Aggregator
to retrive a
single aggregated result from a cache, based on certain criteria. For
example, retrieving the key of the oldest Person
in the
Cache
you created in the previous examples. You can
retrieve all entries and then find the Person
with the
maximum age, but you will be moving a lot data to the client.
Coherence defines an Aggregator
interface which lets you compute
partial results and then combine those partial results to get a single
aggregated result. See Performing Data Grid Aggregation.
This section includes the following topics:
Using an Aggregator Interface
The Aggregator
interface requires you to implement
the following methods:
accumulate(entry):
Executes in parallel across all members and accumulates a single entry into the partial result for that member. In this method, the partial result is computed by using one or more attributes fromentry
. This method will be called multiple times on anAggregator
, once for each entry that was selected for aggregation on a given cluster member.getPartialResult():
Returns the partial result of the parallel aggregation from each member.combine(partialResult):
Combines partial results returned from each cluster member into the final result. In this method, the partial result is computed by using one or more attributes fromentry
. This method will be called multiple times on the rootAggregator
instance, once for each cluster member's partial result.finalizeResult():
Calculates and returns the final result of the aggregation.
Parent topic: Using Aggregators
Writing an Aggregator
To define an Aggregator
that returns the key of the
oldest Person
, create the main.js
file and add the following
content:
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; } }
Finally, re-export the classes defined from the Aggregator
module, main.mjs
:
export * from "./aggregators.mjs"
Parent topic: Using Aggregators
Installing and Using Dependencies
You can use the standard approach of using npm
install
to install your dependencies and use the
import
statement function to consume them. For
example, to install the lodash-es
and
@paravano/utils
packages, use the following
commands:
cd ${JS_DIR} npm install -s lodash-es npm install -s @paravano/utils
After the dependencies are installed, you can implement an entry
processor that uses the now
function from the
lodash-es
package, and camel
function from the @paravano/utils
package:
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; } }
Parent topic: Developing Polyglot Coherence Applications
Building a Project
To make your JavaScript classes available to Coherence Polyglot Framework at runtime, you need to package them in a JAR file.
Coherence Polyglot Framework will automatically load all scripts with an
.mjs
extension from any scripts/js
directory in the class
path, so in the simplest scenario, when you have no external dependencies, you can simply
configure your Maven project to copy all of your source files to the correct location within
the target directory, which will ensure that they are packaged into the final JAR along with
all other classes and resources:
<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>
Note that if you do this, then you also need to redefine the default resource, so all the
files from src/main/resources
get copied to the output directory as well,
like they typically would.
However, when you have external dependencies, and often you likely will, you need to use a bundler to bundle both your source code and any code from the external dependencies it depends on, into the output directory.
The bundler we recommend for this task is esbuild, because it is fast and very simple to use. So now, you'll install it and use it to bundle your JavaScript project into the output directory:
- Install esbuild.
npm install –-save-dev esbuild
- Configure the build script within
package.json
to run esbuild against your main module.{ "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" } }
- Install Node.js and npm locally.
- Install dependencies by running
npm install
. - Bundle your scripts by running esbuild using the
npm run build
command, which was defined previously.
pom.xml
file:<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>
If you now run mvn install
in your project directory, you should
see that Node.js and npm are installed, and that your scripts and their dependencies are
bundled, using esbuild, into a single target/classes/scripts/js/main.mjs
file.
Now, you are ready to consume classes defined in, and exported from, that module in your Java application.
Parent topic: Developing Polyglot Coherence Applications
Using JavaScript Classes in a Java Application
This section includes the following topics:
Parent topic: Developing Polyglot Coherence Applications
Adding Runtime Dependencies
pom.xml
file:<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>
Parent topic: Using JavaScript Classes in a Java Application
Implementing the Java Application
You are now ready to implement the Java application that will start the Coherence cluster, populate the people map with a few entries, and use the JavaScript classes you created previously to query and modify entries in the people map.
Create a file called Main.java
within the main project containing the
following code:
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()); } } }
Parent topic: Using JavaScript Classes in a Java Application
Invoking JavaScript Objects from Java
As the last remaining step, you must implement the previous three methods with the empty bodies, which will use JavaScript classes that you implemented earlier.
This section includes the following topics:
- Invoking JavaScript Filters
- Making Parallel Updates Using JavaScript EntryProcessor
- Running the JavaScript Aggregator
Parent topic: Using JavaScript Classes in a Java Application
Invoking JavaScript Filters
A Filter
written in JavaScript can be instantiated by calling
the script()
method in the com.tangosol.util.Filters
utility
class. It is defined as:
public static <V> Filter<V> script(String language, String filterName, Object... args);
The first argument is used to specify the language in which the script is
implemented. For JavaScript, use js
. The second argument is used to specify
the exported
name of the Filter
, and the last, optional
argument, can be used to pass constructor arguments to the filter, in case it has any.
Add the following method to Main.java
:
private static void displayAllTeens(NamedMap<Integer, Person> people) { printHeader("displayAllTeens"); print(people.entrySet(Filters.script("js", "IsTeen"))); }
Parent topic: Invoking JavaScript Objects from Java
Making Parallel Updates Using JavaScript EntryProcessor
An EntryProcessor
written in JavaScript can be instantiated by
calling the script()
method in the
com.tangosol.util.Processors
class.
Add the following methods to Main.java
:
private static void convertLastNameToUppercase( NamedMap<Integer, Person> people) { printHeader("convertLastNameToUppercase"); people.invokeAll(Processors.script("js", "UpperCaseProcessor")); print(people); }
Parent topic: Invoking JavaScript Objects from Java
Running the JavaScript Aggregator
An Aggregator
written in JavaScript can be instantiated by
calling the script()
method in the
com.tangosol.util.Aggregators
class.
Use the OldestPerson
aggregator, along with the built in
filters, to find the oldest male and female persons.
Add the following to 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)); }
Now, you can run the Java application, and if everything is implemented correctly, see the following output:
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]
If you run into any issues, the example project for this chapter will be available on GitHub.
Parent topic: Invoking JavaScript Objects from Java