PolarSPARC

Introduction to Scripting in Java


Bhaskar S 05/28/2021 (UPDATED)


Overview

A Business application comprises of core application logic plus some business rules. The core logic does not change that often and remains static over time, while the business rules tend to change quite frequently. Let me give you an example of a hypothetical Bank to illustrate this scenario. Consider a simple Banking application, which allows one to open a Certificate of Deposit (CD). In this simple Banking application, a CD of $1000 for a year yields an interest of 2.25%, a CD of $5000 for a year yields an interest of 2.5%, and a CD of $10000 and above for a year yields an interest of 2.75%. These are the business rules of the Banking application. All the other procedures of creating an account for the CD etc., are the core logic of the Banking application.

As the Bank offers new promotions, the business rules will have to change to accommodate those new promotions. If the business rules are coded as Java class(es), then they will have to change every time the business rules change. This results in a tight coupling between the business rules and the core application logic. To make the Banking application more flexible, we need to externalize the business rules from the core application logic so they can be changed independent of the core application logic. So, what do we do here ? One could suggest using an open source Rules Engine like JBoss Drools or a commercial Rules Engine such as IBM JRules. But, the business rules in our simple Banking application are quite simple. It would be an over-kill to use a full-fledged Rules Engine for this purpose. This is where the Scripting API in Java comes to the rescue.

Many of us have worked with some type of scripting language, such as Javascript, Perl, Python, etc., in our development project(s). Scripting languages are interpreted in nature. We write logic in a script file and it is executed line by line by the corresponding language interpreter. Also, Scripting languages use Dynamic Type system. In that, one does not have to define a variable of a specific type upfront before using it. The type is automatically determined based on the context when they are used. As a result, Scripting languages make it easier and faster to write and test code.

For our simple Banking application, we could write the business rules as a script using a Scripting Language of our choice and execute the script dynamically at run-time from within our core application written in Java. This results in extensibility and flexibility of our business application. Does this sound exciting !!!

Setup

The setup will be on a Ubuntu 20.04 LTS based Linux desktop. Ensure at least Java 11 or above is installed and setup. Also, ensure Apache Maven is installed and setup.

To setup the Java directory structure for the demonstrations in this part, execute the following commands:

$ cd $HOME

$ mkdir -p $HOME/java/Scripting

$ cd $HOME/java/Scripting

$ mkdir -p src/main/java src/main/resources target

$ mkdir -p src/main/java/com/polarsparc/scripting

$ mkdir -p src/main/resources/scripts


The following is the listing for the Maven project file pom.xml that will be used:


pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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.polarsparc.scripting</groupId>
    <artifactId>Scripting</artifactId>
    <version>1.0</version>

    <properties>
        <java.version>11</java.version>
        <slf4j.version>1.7.30</slf4j.version>
        <nashorn.version>15.2</nashorn.version>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <fork>true</fork>
                        <meminitial>128m</meminitial>
                        <maxmem>512m</maxmem>
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

    <dependencies>
        <dependency>
            <groupId>org.openjdk.nashorn</groupId>
            <artifactId>nashorn-core</artifactId>
            <version>${nashorn.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
    </dependencies>
</project>

!!! ATTENTION !!!

Since Java 11, the built-in JavaScript scripting engine Nashorn has been removed !!! However, it is available as a package in OpenJDK

The following is the listing for the slf4j-simple logger properties file simplelogger.properties located in the directory src/main/resources:


simplelogger.properties
#
### SLF4J Simple Logger properties
#

org.slf4j.simpleLogger.defaultLogLevel=info
org.slf4j.simpleLogger.showDateTime=true
org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS
org.slf4j.simpleLogger.showThreadName=true

Without much further ado, let us jump into a simple example to illustrate this powerful capability of Scripting support in Java.

Hands-on Scripting in Java

The following is the code in Javascript src/main/resources/First.js to print a given name certain number of times:


Listing.1
print('Print Hello ' + name + ' ' + num + ' times');
for (var i = 0; i < num; i++) {
  print(' Hello ' + name + ' ('+ (i+1) +')');
}

The following is the Java code that will execute the above shown Javascript code dynamically at run-time:


Listing.2
/*
 * Name:   First
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.io.*;
import javax.script.*;

public class First {
  private static final Logger LOGGER = LoggerFactory.getLogger(First.class);

  public static void main(String[] args) {
    if (args.length != 3) {
      System.out.println("Usage: java com.polarsparc.scripting.First <script> <name> <number>");
      System.exit(1);
    }
    
    String name = args[1];
    
    int num = 0;
    
    try {
      num = Integer.parseInt(args[2]);
    }
    catch (Exception ex) {
      LOGGER.error("Exception", ex);
    }
    
    if (num <= 0) {
      num = 5;
    }
    
    ScriptEngineManager manager = new ScriptEngineManager();

    List>ScriptEngineFactory> factories = manager.getEngineFactories();
    factories.forEach(factory -> {
      LOGGER.info("Engine name: {}, Version: {}, Language: {}, Language version: {}",
          factory.getEngineName(),
          factory.getEngineVersion(),
          factory.getLanguageName(),
          factory.getLanguageVersion());

      factory.getNames().forEach(alias -> LOGGER.info("Engine alias: {}", alias));
    });

    ScriptEngine engine = manager.getEngineByExtension("js");
    if (engine == null) {
      LOGGER.error("Could not find engine - Nashorn");
    }
    else {
      Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
      bindings.put("name", name);
      bindings.put("num", num);

      try {
        File script = new File(Objects.requireNonNull(First.class.getClassLoader()
            .getResource(args[0])).getFile());
        engine.eval(new FileReader(script));
      } catch (Throwable ex) {
        LOGGER.error("Exception", ex);
      }
    }
  }
}

Java introduced a standardized way to integrate with the various scripting languages as defined in JSR-223 through the Scripting API defined in the package javax.script.

ScriptEngineManager is a concrete class and uses service provider discovery mechanism to find all the classes in the current ClassLoader that implement the interface ScriptEngineFactory.

An implementation of ScriptEngineFactory exposes various meta information such as, name, version, language, etc., of the corresponding ScriptEngine.

ScriptEngine is the main interface that works with the underlying scripting language interpreter to execute the specified script code.

An instance of Bindings is an implementation of java.util.Map, which allows a Java program to bind object values to script variables so they can be used by the underlying scripting engine while executing the script. There are two scopes of binding script variables:

To execute the code from Listing.2, open a terminal window and run the following commands:

$ cd $HOME/java/Scripting

$ mvn exec:java -Dexec.mainClass="com.polarsparc.scripting.First" -Dexec.args="scripts/First.js Alice 10"

The following would be the typical output:

Output.1

[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.polarsparc.scripting:Scripting >-----------------
[INFO] Building Scripting 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ Scripting ---
2021-05-28 19:54:52:961 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine name: Oracle Nashorn, Version: 15.2, Language: ECMAScript, Language version: ECMA - 262 Edition 5.1
2021-05-28 19:54:52:963 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: nashorn
2021-05-28 19:54:52:964 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: Nashorn
2021-05-28 19:54:52:964 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: js
2021-05-28 19:54:52:965 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: JS
2021-05-28 19:54:52:965 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: JavaScript
2021-05-28 19:54:52:966 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: javascript
2021-05-28 19:54:52:966 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: ECMAScript
2021-05-28 19:54:52:967 [com.polarsparc.scripting.First.main()] INFO com.polarsparc.scripting.First - Engine alias: ecmascript
Print Hello Alice 10 times
  Hello Alice (1)
  Hello Alice (2)
  Hello Alice (3)
  Hello Alice (4)
  Hello Alice (5)
  Hello Alice (6)
  Hello Alice (7)
  Hello Alice (8)
  Hello Alice (9)
  Hello Alice (10)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.958 s
[INFO] Finished at: 2021-05-28T19:54:53-04:00
[INFO] ------------------------------------------------------------------------

The following Java code illustrates the behavior of ENGINE_SCOPE and GLOBAL_SCOPE scopes for script variable binding:


Listing.3
/*
 * Name:   Second
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import javax.script.*;

public class Second {
  private static final Logger LOGGER = LoggerFactory.getLogger(Second.class);

  public static void main(String[] args) {
    Random random = new Random();
    
    ScriptEngineManager manager = new ScriptEngineManager();
    
    ScriptEngine engineOne = manager.getEngineByExtension("js");
    ScriptEngine engineTwo = manager.getEngineByExtension("js");
    if (engineOne == null || engineTwo == null) {
      LOGGER.error("Could not find engine - Nashorn");
    }
    else {
      Bindings bindings = engineOne.getBindings(ScriptContext.ENGINE_SCOPE);
      bindings.put("num", random.nextInt(1000));

      LOGGER.info("Executing using engine-1 (1) ... ");

      try {
        engineOne.eval("print('num = ' + num);");
      }
      catch (Throwable ex) {
        LOGGER.error("Exception", ex);
      }

      LOGGER.info("Executing using engine-2 (1) ... ");

      try {
        engineTwo.eval("print('num = ' + num);"); // Will throw exception
      }
      catch (Throwable ex) {
        LOGGER.error("Exception", ex);
      }

      bindings = engineTwo.getBindings(ScriptContext.GLOBAL_SCOPE);
      bindings.put("val", random.nextInt(1000));

      LOGGER.info("Executing using engine-1 (2) ... ");

      try {
        engineOne.eval("print('val = ' + val);");
      }
      catch (Throwable ex) {
        LOGGER.error("Exception", ex);
      }

      LOGGER.info("Executing using engine-2 (2) ... ");

      try {
        engineTwo.eval("print('val = ' + val);");
      }
      catch (Throwable ex) {
        LOGGER.error("Exception", ex);
      }
    }
  }
}

To execute the code from Listing.3, open a terminal window and run the following commands:

$ cd $HOME/java/Scripting

$ mvn exec:java -Dexec.mainClass="com.polarsparc.scripting.Second"

The following would be the typical output:

Output.2

[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.polarsparc.scripting:Scripting >-----------------
[INFO] Building Scripting 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ Scripting ---
2021-05-28 20:03:31:662 [com.polarsparc.scripting.Second.main()] INFO com.polarsparc.scripting.Second - Executing using engine-1 (1) ... 
num = 713
2021-05-28 20:03:31:782 [com.polarsparc.scripting.Second.main()] INFO com.polarsparc.scripting.Second - Executing using engine-2 (1) ... 
2021-05-28 20:03:31:792 [com.polarsparc.scripting.Second.main()] ERROR com.polarsparc.scripting.Second - Exception
javax.script.ScriptException: ReferenceError: "num" is not defined in <eval> at line number 1
        at org.openjdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:463)
        at org.openjdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:447)
        at org.openjdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:399)
        at org.openjdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:395)
        at org.openjdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:151)
        at java.scripting/javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264)
        at com.polarsparc.scripting.Second.main(Second.java:45)
        at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:254)
        at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: <eval>:1 ReferenceError: "num" is not defined
        at org.openjdk.nashorn.internal.runtime.ECMAErrors.error(ECMAErrors.java:57)
        at org.openjdk.nashorn.internal.runtime.ECMAErrors.referenceError(ECMAErrors.java:318)
        at org.openjdk.nashorn.internal.runtime.ECMAErrors.referenceError(ECMAErrors.java:290)
        at org.openjdk.nashorn.internal.objects.Global.__noSuchProperty__(Global.java:1616)
        at org.openjdk.nashorn.internal.scripts.Script$1$\^eval\_/0x0000000800dd9840.:program(<eval>:1)
        at org.openjdk.nashorn.internal.runtime.ScriptFunctionData.invoke(ScriptFunctionData.java:646)
        at org.openjdk.nashorn.internal.runtime.ScriptFunction.invoke(ScriptFunction.java:513)
        at org.openjdk.nashorn.internal.runtime.ScriptRuntime.apply(ScriptRuntime.java:520)
        at org.openjdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:442)
        ... 7 more
2021-05-28 20:03:31:798 [com.polarsparc.scripting.Second.main()] INFO com.polarsparc.scripting.Second - Executing using engine-1 (2) ... 
val = 29
2021-05-28 20:03:31:805 [com.polarsparc.scripting.Second.main()] INFO com.polarsparc.scripting.Second - Executing using engine-2 (2) ... 
val = 29
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.944 s
[INFO] Finished at: 2021-05-28T20:03:31-04:00
[INFO] ------------------------------------------------------------------------

The result clearly indicates that the script variable 'num' does not exist in ScriptEngine engineTwo.

Now, that we are familiar with the Scripting API in Java, let us look at the example of our simple Banking application. The following Java code determines the interest rate given the CD amount:


Listing.4
/*
 * Name:   Third
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.Objects;
import javax.script.*;

public class Third {
  private static final Logger LOGGER = LoggerFactory.getLogger(Third.class);

  public static void main(String[] args) {
    if (args.length != 1) {
      System.out.println("Usage: java com.polarsparc.scripting.Third <script>");
      System.exit(1);
    }
    
    try {
      File script = new File(Objects.requireNonNull(Third.class.getClassLoader()
          .getResource(args[0])).getFile());

      BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
      
      boolean exit = false;
      while (! exit) {
        System.out.println("1. Open New CD");
        System.out.println("2. Quit");
        
        String choice = input.readLine();
        if (choice.equals("1")) {
          System.out.print("CD Amount: ");
          String amtStr = input.readLine();
          
          double amount = Double.parseDouble(amtStr);
          
          double rate = executeBusinessRules(script, amount);
          
          LOGGER.info("Interest rate: {}", rate);
        } else if (choice.equals("2")) {
          exit = true;
        }
      }
    }
    catch (Exception ex) {
      LOGGER.error("Exception", ex);
    }
  }
  
  public static double executeBusinessRules(File script, double amount) throws Exception {
    double rate = 0.0;
    
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByExtension("js");
    if (engine == null) {
      LOGGER.error("Could not find engine - Nashorn");
    }
    else {
      Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
      bindings.put("amount", amount);
      
      engine.eval(new FileReader(script));
      
      rate = (Double) bindings.get("rate");
    }
    
    return rate;
  }
}

The following is the Javascript code src/main/resources/Third.js that implements the business rules for our simple Banking application:


Listing.5
rate = 0.0;
if (amount >= 1000.0 && amount < 5000.0) {
    rate = 2.25;
}
else if (amount >= 5000.0 && amount < 10000.0) {
    rate = 2.5;
}
else if (amount >= 10000.0) {
    rate = 2.75;
}

To execute the code from Listing.4, open a terminal window and run the following commands:

$ cd $HOME/java/Scripting

$ mvn exec:java -Dexec.mainClass="com.polarsparc.scripting.Third" -Dexec.args="scripts/Third.js"

The following would be the interaction and the corresponding output:

Output.3

[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.polarsparc.scripting:Scripting >-----------------
[INFO] Building Scripting 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ Scripting ---
1. Open New CD
2. Quit
1
CD Amount: 1000
2021-05-28 20:25:21:363 [com.polarsparc.scripting.Third.main()] INFO com.polarsparc.scripting.Third - Interest rate: 2.25
1. Open New CD
2. Quit
1
CD Amount: 6000
2021-05-28 20:25:28:186 [com.polarsparc.scripting.Third.main()] INFO com.polarsparc.scripting.Third - Interest rate: 2.5
1. Open New CD
2. Quit
1
CD Amount: 12000
2021-05-28 20:25:35:748 [com.polarsparc.scripting.Third.main()] INFO com.polarsparc.scripting.Third - Interest rate: 2.75
1. Open New CD
2. Quit

As the application Third is waiting on further input from the user, let us make the change to the business rules to handle the new promotion offer of increasing the interest rate by 0.25% across the board. The following shows the new business rules for our simple Banking application:


Listing.6
rate = 0.0;
if (amount >= 1000.0 && amount < 5000.0) {
    rate = 2.5;
}
else if (amount >= 5000.0 && amount < 10000.0) {
    rate = 2.75;
}
else if (amount >= 10000.0) {
    rate = 3.0;
}

The following would be the interaction and the corresponding output after the new changes in effect without the application restart:

Output.4

1
CD Amount: 1000
2021-05-28 20:26:04:131 [com.polarsparc.scripting.Third.main()] INFO com.polarsparc.scripting.Third - Interest rate: 2.5
1. Open New CD
2. Quit
1
CD Amount: 6000
2021-05-28 20:26:11:767 [com.polarsparc.scripting.Third.main()] INFO com.polarsparc.scripting.Third - Interest rate: 2.75
1. Open New CD
2. Quit
1
CD Amount: 12000
2021-05-28 20:26:18:714 [com.polarsparc.scripting.Third.main()] INFO com.polarsparc.scripting.Third - Interest rate: 3.0
1. Open New CD
2. Quit
2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:06 min
[INFO] Finished at: 2021-05-28T20:26:20-04:00
[INFO] ------------------------------------------------------------------------

Now, let us refactor the Javascript code and move the logic to compute the interest rate into a function. The following is the Javascript code src/main/resources/Fourth.js that implements the business rules for our simple Banking application as a function:


Listing.7
function computeRate(amount) {
  rate = 2.5;
  if (amount >= 5000.0 && amount < 10000.0) {
      rate = 2.5;
  }
  else if (amount >= 10000.0) {
      rate = 2.75;
  }
  return rate;
}

The following Java code determines the interest rate for a given the CD amount by invoking the Javascript function:


Listing.8
/*
 * Name:   Fourth
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.File;
import java.io.FileReader;
import java.util.Objects;

public class Fourth {
    private static final Logger LOGGER = LoggerFactory.getLogger(Fourth.class);

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java com.polarsparc.scripting.Fourth <script> <amount>");
            System.exit(1);
        }

        try {
            File script = new File(Objects.requireNonNull(Fourth.class.getClassLoader()
                    .getResource(args[0])).getFile());

            double amount = Double.parseDouble(args[1]);

            ScriptEngineManager manager = new ScriptEngineManager();
            ScriptEngine engine = manager.getEngineByExtension("js");
            if (engine == null) {
                LOGGER.error("Could not find engine - Nashorn");
            }
            else {
                engine.eval(new FileReader(script));

                Invocable invocable = (Invocable) engine;

                Object rate = invocable.invokeFunction("computeRate", amount);

                LOGGER.info("Amount: {}, Rate [{}]: {}", amount, rate.getClass().getName(), rate);
            }
        }
        catch (Exception ex) {
            LOGGER.error("Exception", ex);
        }
    }
}

Invocable is a optional interface implemented by ScriptEngines whose methods allow the invocation of functions in the scripts. The method invokeFunction​(String name, Object... args) allows one to call the function defined in the scripts.

To execute the code from Listing.8, open a terminal window and run the following commands:

$ cd $HOME/java/Scripting

$ mvn exec:java -Dexec.mainClass="com.polarsparc.scripting.Fourth" -Dexec.args="scripts/Fourth.js 6000"

The following would be the typical output:

Output.5

[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.polarsparc.scripting:Scripting >-----------------
[INFO] Building Scripting 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ Scripting ---
2021-05-28 20:32:47:202 [com.polarsparc.scripting.Fourth.main()] INFO com.polarsparc.scripting.Fourth - Amount: 6000.0, Rate [java.lang.Double]: 2.5
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.980 s
[INFO] Finished at: 2021-05-28T20:32:47-04:00
[INFO] ------------------------------------------------------------------------

We will now further refactor the Javascript code to have the function compute the interest rate as well as the processing fees for the given amount. The return value will be a java.util.HashMap that holds the interest rate and the processing fees. The following is the Javascript code src/main/resources/Fifth.js that implements the desired business rules for our simple Banking application and returns a java.util.HashMap:


Listing.9
function computeRateAndFees(amount) {
  var HashMap = Java.type("java.util.HashMap");
  var map = new HashMap;

  rate = 2.5;
  fees = 1000.0;
  if (amount >= 5000.0 && amount < 10000.0) {
      rate = 2.5;
      fees = 750;
  }
  else if (amount >= 10000.0) {
      rate = 2.75;
      fees = 500;
  }

  map.put("rate", rate);
  map.put("fees", fees);

  return map;
}

JavaScript call Java.type(type) allows a script to reference and use Java types.

The following Java code determines the interest rate and procesing fees for a given the CD amount by invoking the Javascript function, which returns the desired result as a java.util.HashMap:


Listing.10
/*
 * Name:   Fifth
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.File;
import java.io.FileReader;
import java.util.Objects;

public class Fifth {
    private static final Logger LOGGER = LoggerFactory.getLogger(Fifth.class);

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java com.polarsparc.scripting.Fifth <script> <amount>");
            System.exit(1);
        }

        try {
            File script = new File(Objects.requireNonNull(Fifth.class.getClassLoader()
                    .getResource(args[0])).getFile());

            double amount = Double.parseDouble(args[1]);

            ScriptEngineManager manager = new ScriptEngineManager();
            ScriptEngine engine = manager.getEngineByExtension("js");
            if (engine == null) {
                LOGGER.error("Could not find engine - Nashorn");
            }
            else {
                engine.eval(new FileReader(script));

                Invocable invocable = (Invocable) engine;

                Object rateFees = invocable.invokeFunction("computeRateAndFees", amount);

                LOGGER.info("Amount: {}, Rate & Fees [{}]: {}",
                        amount, rateFees.getClass().getName(), rateFees.toString());
            }
        }
        catch (Exception ex) {
            LOGGER.error("Exception", ex);
        }
    }
}

To execute the code from Listing.10, open a terminal window and run the following commands:

$ cd $HOME/java/Scripting

$ mvn exec:java -Dexec.mainClass="com.polarsparc.scripting.Fifth" -Dexec.args="scripts/Fifth.js 15000"

The following would be the typical output:

Output.6

[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.polarsparc.scripting:Scripting >-----------------
[INFO] Building Scripting 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ Scripting ---
2021-05-28 20:41:32:373 [com.polarsparc.scripting.Fifth.main()] INFO com.polarsparc.scripting.Fifth - Amount: 15000.0, Rate & Fees [java.util.HashMap]: {fees=500, rate=2.75}
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.055 s
[INFO] Finished at: 2021-05-28T20:41:32-04:00
[INFO] ------------------------------------------------------------------------

We desire to create a Java POJO in the Java application and have the JavaScript code update certain fields in the POJO.

The following is the Java POJO that holds the amount, the interest rate, and the processing fees:


Listing.11
/*
 * Name:   SixthHolder
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

public class SixthHolder {
    private double amount = 0.0;
    private double rate = 0.0;
    private double fees = 0.0;

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    public double getRate() {
        return rate;
    }

    public void setRate(double rate) {
        this.rate = rate;
    }

    public double getFees() {
        return fees;
    }

    public void setFees(double fees) {
        this.fees = fees;
    }

    @Override
    public String toString() {
        return "SixthHolder{" +
                "amount=" + amount +
                ", rate=" + rate +
                ", fees=" + fees +
                '}';
    }
}

The following Javascript code defines the function to compute the interest rate as well as the processing fees for the given amount. The input to and output from the function is the Java POJO SixthHolder:


Listing.12
function computeRateAndFees(holder) {
  holder.rate = 2.5;
  holder.fees = 1000.0;

  if (holder.amount >= 5000.0 && holder.amount < 10000.0) {
      holder.rate = 2.5;
      holder.fees = 750;
  }
  else if (holder.amount >= 10000.0) {
      holder.rate = 2.75;
      holder.fees = 500;
  }

  return holder;
}

The following Java code determines the interest rate and procesing fees for a given the CD amount by invoking the Javascript function, which passes in and receives the Java POJO SixthHolder:


Listing.13
/*
 * Name:   Sixth
 * Author: Bhaskar S
 * Date:   05/28/2021
 * Blog:   https://www.polarsparc.com
 */

package com.polarsparc.scripting;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.io.File;
import java.io.FileReader;
import java.util.Objects;

public class Sixth {
    private static final Logger LOGGER = LoggerFactory.getLogger(Sixth.class);

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java com.polarsparc.scripting.Sixth <script> <amount>");
            System.exit(1);
        }

        try {
            File script = new File(Objects.requireNonNull(Sixth.class.getClassLoader()
                    .getResource(args[0])).getFile());

            SixthHolder holder = new SixthHolder();
            holder.setAmount(Double.parseDouble(args[1]));

            ScriptEngineManager manager = new ScriptEngineManager();
            ScriptEngine engine = manager.getEngineByExtension("js");
            if (engine == null) {
                LOGGER.error("Could not find engine - Nashorn");
            }
            else {
                engine.eval(new FileReader(script));

                Invocable invocable = (Invocable) engine;

                Object rateFees = invocable.invokeFunction("computeRateAndFees", holder);

                LOGGER.info("Amount: {}, Rate & Fees [{}]: {}",
                        holder.getAmount(), rateFees.getClass().getName(), rateFees.toString());
            }
        }
        catch (Exception ex) {
            LOGGER.error("Exception", ex);
        }
    }
}

To execute the code from Listing.13, open a terminal window and run the following commands:

$ cd $HOME/java/Scripting

$ mvn exec:java -Dexec.mainClass="com.polarsparc.scripting.Sixth" -Dexec.args="scripts/Sixth.js 7500"

The following would be the typical output:

Output.7

[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< com.polarsparc.scripting:Scripting >-----------------
[INFO] Building Scripting 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ Scripting ---
2021-05-28 20:56:27:176 [com.polarsparc.scripting.Sixth.main()] INFO com.polarsparc.scripting.Sixth - Amount: 7500.0, Rate & Fees [com.polarsparc.scripting.SixthHolder]: SixthHolder{amount=7500.0, rate=2.5, fees=750.0}
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.983 s
[INFO] Finished at: 2021-05-28T20:56:27-04:00
[INFO] ------------------------------------------------------------------------

Excited ??? Well, thats it folks !!!

References

Nashorn User's Guide

GitHub - Source Code



© PolarSPARC