Tech Life

Ideaport Riga blog about mobile, web and integration solutions for enterprises

Java: using embedded Rhino JavaScript engine

Java supports a lot of scripting languages, and since Java 6 it supports JavaScript out of the box. JDK 6 and JDK 7 both have embedded Rhino JavaScript engine that was developed by Mozilla. In JDK 8 though, the Rhino engine was replaced with Nashorn. Being the Rhino successor that was rewritten from scratch to meet modern script engine expectations, it offers a better performance, but since Java 8 is still not so widely adopted in the enterprise environments, let us focus on Rhino for now. Rhino

You may be wondering, why you really need this? So here are the most common use case scenarios for JavaScript engine in your Java application:

  • The need to execute or change something in the application server without restarting it.
  • The need to run the same JavaScript files on the client side and on the server side that execute validation logic, for instance, to avoid code duplication.
  • The need to make some part of your application programmable or configurable through scripts to separate business logic from the application core, give more control over the application to other parties.

Obviously, that’s not all of the scenarios – there are a lot more ways how you can use it.

So let’s talk about how you can start using Rhino. Here’s a small example that covers all the basics. First of all, we will need a JavaScript code to execute:

result.put('result', fib(NUM_CONST).toString());
function fib(n) {
if(n <= 1) return n;
return fib(n-1) + fib(n-2);
}

This is a simple script that calculates a Fibonacci sequence number at the requested position. It can be used as a benchmark to test different Rhino configurations to squeeze the maximum performance out of it. The script input parameters are NUM_CONST, which is a constant value that will be passed from Java, and the result is a java object of class HashMap that will contain script execution result, which will be accessible from Java code.

Now that we have the script to run, let’s write some code in Java (using JDK 7) to execute it:

import sun.org.mozilla.javascript.internal.*;

import java.io.IOException;
import java.util.*;

import static java.nio.file.Files.readAllBytes;
import static java.nio.file.Paths.get;

public class Main {
private static final SandboxClassShutter sandboxClassShutter = new SandboxClassShutter();
private static ScriptableObject globalScope;

public static void main(String[] args) throws IOException {
Main main = new Main();
main.execute();
}

public void execute() throws IOException {
Context cx = Context.enter();
cx.setOptimizationLevel(9);
cx.setLanguageVersion(Context.VERSION_1_8);
cx.setClassShutter(sandboxClassShutter);
try {
Map result = new HashMap<>();
Scriptable currentScope = getNewScope(cx);
currentScope.put("result", currentScope, result);
Script script = cx.compileString(new String(readAllBytes(get("src/test.js"))), "my_script_id", 1, null);
script.exec(cx, currentScope);
System.out.println("Result: " + result.get("result"));
} finally {
Context.exit();
}
}

private Scriptable getNewScope(Context cx) {
//global scope lazy initialization
if (globalScope == null) {
globalScope = cx.initStandardObjects();
globalScope.put("NUM_CONST", globalScope, 20);
}
return cx.newObject(globalScope);
}

public static class SandboxClassShutter implements ClassShutter {
public boolean visibleToScripts(String fullClassName) {
return fullClassName.equals(HashMap.class.getName());
}
}
}

Now I will explain what each block of the code does. Let’s start with execute() method. First of all, we need to enter the Context by calling cx.enter() method. Context is the environment, in which the script will be executed. cx.enter() creates new context, which is associated with the thread that will be executing the script. You can change different context parameters to the desired values; in this example I have changed context optimization level to 9, which means that all available optimizations will be performed, nd those will be executed when the script is compiled. Optimization levels range from 1 to 9; note, however, that optimizations do not apply to interpreted scripts. If you set it to 0, no optimizations will be applied, and -1 forces to execute scripts in an interpretive mode.

Language level is set to JavaScript version 1.8. With these parameters I was able to get the best performance, although script compilation took more time, because of the performed optimizations. But this will not matter if you store compiled scripts in memory and reuse them or the script takes a lot more time to execute than compile.

Then I set my own implementation of ClassShutter that will prevent script from accessing forbidden Java classes. This implementation only allows the use of the HashMap class. It is useful if you want to make your JavaScript environment secure, i.e. to prevent tampering with your application from the scripts. Basically, you can implement a sandbox for JavaScript environment. After executing the script, don’t forget to exit the Context in the finally block.

After creating and configuring the Context, we need to initialize JavaScript scope. In Rhino, scopes can be reused by different threads, so it means that concurrency should be taken into consideration. We create a new scope in the getNewScope() method: as you can see, I have used lazy global scope initialization there to create a scope that will be used to create other scope instances. This is a recommended approach, since cx.initStandardObjects() is an expensive operation. The cx.initStandardObjects() method initializes the global scope that contains core JavaScript objects like Object and Function. You can store constant variables in the global scope, so that each scope instance that is created from it woud have access to them. For demonstration purposes, we put our own constant called NUM_CONST in the global scope, so that we can access it in the script later. After that we create another scope that will contain everything that our globalScope has to execute our script in it. We also put our result map in the new scope instance.

Now that we have the Context and Scriptable scope, we need to compile the JavaScript before executing it. As I have mentioned before, script compilation boosts performance. Our script is located in a src/test.js file so we read it as a String. You can store scripts in a database, files, or upload them into a servlet if you desire so. In the cx.compileString( )method, the first parameter is the script itself. The second parameter is the script name or ID that will be used to display debugging information in logs. The third parameter is the script line number, from which to start the execution, and the last parameter is security domain that specifies security information about the origin or owner of the script. We set the last parameter to null because we do not implement security features in this example.

The last step is executing the compiled script in our configured Context using our created scope that contains the result map and NUM_CONST. After executing the script, we can access the result from the map and print the line in a console. The output should be “Result: 6765”.

Have a question or need help working with something similar? Be sure to get in touch with us!


ak About the author: Aleksandrs Krasovskis has been with Idea Port Riga for almost 3 years now, he is a software developer, specializing mostly in Java, JavaScript and web application development. He has solid experience in working with PostgreSQL and mongoDB, having been involved in building various solutions from the ground up.

 

Java JavaScript

Subscribe for new posts!

Photo of Katerina Alfimova - System Analyst