JJBridge is a multi-library project which brings JavaScript execution capabilities to Java.
JJBridge Api defines a standard set of interface for accessing a JavaScript engine from Java.
Add this to your pom.xml:
<dependency>
<groupId>srl.forge</groupId>
<artifactId>jjbridge-api</artifactId>
</dependency>
In order to actually execute JavaScript you should also add a JJBridge Engine library.
Here is a list of the engines currently available for JJBridge:
- JJBridge V8 Engine (Available for Linux, Windows, macOS and Android)
The full javadoc is available at https://www.javadoc.io/doc/srl.forge/jjbridge-api/latest/index.html.
All JavaScript code must run inside a JSRuntime
instance. Here is an example:
class Example {
private static JSEngine engine = new V8Engine(); // or other available engine
public static void main(String[] args) {
try (JSRuntime runtime = engine.newRuntime()) {
// Do JavaScript things here...
String script =
"const foo = '25'\n" +
"const bar = 33\n" +
"const mult = (a, b) => `${a} x ${b} = ${a * b}`\n" +
"mult(foo, bar)";
JSReference resultJSRef = runtime.executeScript(script);
JSString resultJSString = runtime.resolveReference(resultJSRef);
String result = resultJSString.getValue();
System.out.println(result); // Prints "25 x 33 = 825"
} catch (RuntimeException e) {
// handle errors here
}
}
}
A JSRuntime
is an instance of a JavaScript engine context. It is the single point from which you can execute scripts
and access JavaScript objects.
All interactions between JavaScript and Java exploit instances of JSReference
. A JSReference
is a lightweight
pointer to a JavaScript object which can be passed in and out of the runtime when needed. References are created as a
result of a script execution, as a result of a JavaScript function invocation, accessing a JavaScript object field or
even directly from the runtime using the newReference(JSType type)
method.
If you need to access the actual value pointed by a reference, you must resolve it using the
resolveReference(JSReference reference)
method of the runtime. When resolving a reference, you must specify the
expected JSValue
sub-type. JSValue
sub-types are the following:
JSUndefined
: maps JavaScriptundefined
.JSNull
: maps JavaScriptnull
.JSBoolean
: maps a JavaScript boolean; allows get/set of the value.JSNumber
: maps a JavaScript number to a Java double; allows get/set of the value and provides additional utilities for long values.JSString
: maps a JavaScript string; allows get/set of the value.JSObject
: maps a JavaScript object; allows get/set of properties/methods.JSDate
: maps a JavaScript date; allows get/set of the value and properties/methods.JSArray
: maps a JavaScript array; allows get/set of properties/methods and elements.JSFunction
: maps a JavaScript function; allows get/set of properties/methods, invocation (both as normal function and as constructor) and change of the actual code to execute.JSExternal
: this is a special type which allows the runtime to store references to Java objects in JavaScript objects.
Using the wrong type may throw an exception.
Working with primitive types (booleans, numbers, strings) is pretty straightforward. You can only get and set the value. Dates are objects (because you can access fields and methods), but they also allow getting and setting the value just like primitive types.
Here is an example with booleans:
JSReference ref = runtime.newReference(JSType.Boolean);
JSBoolean jsBool = runtime.resolveReference(ref);
jsBool.setValue(true);
Boolean b = jsBool.getValue(); // b is true
jsBool.setValue(false);
b = jsBool.getValue(); // b is false
And now an example with numbers:
JSReference ref = runtime.newReference(JSType.Number);
JSNumber jsNum = runtime.resolveReference(ref);
jsNum.setValue(420.5);
Double i = jsInt.getValue(); // i is 420.5
jsInt.setValue(-10000009);
i = jsInt.getValue(); // i is -10000009
Working with objects is all about getting and setting properties. Properties can be fields of any type but also methods (which are nothing but functions).
Here is an example:
JSReference ref = runtime.newReference(JSType.Object);
JSObject<?> jsObj = runtime.resolveReference(ref);
JSReference field1ref = runtime.newReference(JSType.String);
((JSString) runtime.resolveReference(field1ref)).setValue("foo bar baz");
jsObj.set("field1", field1ref);
// now jsObj is {field1: "foo bar baz"}
jsObj.set("field2", runtime.newReference(JSType.Null));
// now jsObj is {field1: "foo bar baz", field2: null}
jsObj.set("field3", runtime.executeScript("({inner: 8})"));
// now jsObj is {field1: "foo bar baz", field2: null, field3: {inner: 8}}
JSReference field3ref = jsObj.get("field3");
JSObject<?> field3 = runtime.resolveReference(field3ref);
JSNumber inner = runtime.resolveReference(field3.get("inner"));
Long i = inner.getLongValue() // i is 8
Arrays are much like objects, but instead of accessing a property by name, you can access an item by index. Array are also objects, which means you can access all array properties.
Here is an example:
JSReference ref = runtime.newReference(JSType.Array);
JSArray<?> jsArray = runtime.resolveReference(ref);
int size = jsArray.size(); // size is 0
jsArray.set(0, runtime.newReference(JSType.Null));
jsArray.set(1, runtime.newReference(JSType.Null));
jsArray.set(2, runtime.executeScript("11**4"));
size = jsArray.size(); // size is 3
JSNumber inner = runtime.resolveReference(jsArray.get(2));
Long i = inner.getLongValue() // i is 14641
You can get a function either from a script or accessing an object property. Depending on the function, you can call it normally or as a constructor. Functions are also objects, which means you can access all function properties.
Here is an example:
JSReference ref = runtime.executeScript("(a, b) => a + b");
JSFunction<?> jsFunction = runtime.resolveReference(ref);
JSReference stringRef = runtime.newReference(JSType.String);
((JSString) runtime.resolveReference(stringRef)).setValue("Toc");
JSReference resultRef = jsFunction.invoke(ref, stringRef, stringRef);
String resultString = ((JSString) runtime.resolveReference(resultRef)).getValue(); // resultString is TocToc
JSReference intRef = runtime.newReference(JSType.Number);
((JSNumber) runtime.resolveReference(intRef)).setLongValue(12L);
resultRef = jsFunction.invoke(ref, intRef, intRef);
Long resultInt = ((JSNumber) runtime.resolveReference(resultRef)).getLongValue(); // resultInt is 24
You can also create a JavaScript function which will execute Java code when invoked:
JSReference ref = runtime.newReference(JSType.Function);
JSFunction<?> jsFunction = runtime.resolveReference(ref);
jsFunction.setFunction((JSReference... args) -> {
String arg0 = ((JSString) runtime.resolveReference(args[0])).getValue();
String arg1 = ((JSString) runtime.resolveReference(args[1])).getValue();
String result = someObject.someFunction(arg0, arg1); // Do something with the args
JSReference resultRef = runtime.newReference(JSType.String);
((JSString) runtime.resolveReference(resultRef)).setValue(result);
return resultRef; // TIP: if the function is supposed to be void then just return undefined.
});
The external type is a special type useful to store native Java data inside a JavaScript object for later retrieval. From a JavaScript point of view, external values are indistinguishable from objects, but from a Java point of view they store additional data.
The usage is pretty much like a primitive type:
JSReference ref = runtime.newReference(JSType.External);
JSExternal<HashMap<String, int[]>> jsExternal = runtime.resolveReference(ref);
HashMap<String, int[]> hashMap = new HashMap<>();
hashMap.put("asd", new int[] {1, 2});
jsExternal.setValue(hashMap);
hashMap = null;
// A reference to the hash map is still present inside the external object, thus we can retrieve it
hashMap = jsExternal.getValue();
int[] array = hashMap.get("asd"); // array is [1, 2]
As you have seen from the examples above, to run a script you just need to use runtime.executeScript(script)
passing
the JavaScript code as a String
. There is a useful variant of this method which also allows to assign a file name to
the script; this is useful if you have to debug many scripts in the inspector!
The result of a script execution is the last expression of the script. For example if the script is:
const defaultName = 'world'
const greet = function(name = defaultName) {
return {greeting: `Hello ${name}!`}
}
greet('Alice')
The result will be the value of the last expression greet('Alice')
which is the resulting object
{greeting: "Hello Alice!"}
.
When you run multiple scripts keep in mind they are isolated, which means a variable defined in one script is not available in a different script. If you want to share values between scripts you should expose them as references which will be passed from Java.
The only exception are the global variables of the runtime. A global variable is just a property of the global
object which is made accessible to the scripts as a predefined variable. To get the global object use
runtime.globalObject()
, then add each variable as a property to this object. Remember to define your global variables
before running a script which use them!
Here is an example:
JSReference globRef = runtime.newReference(JSType.Number);
((JSNumber) runtime.resolveReference(globRef)).setLongValue(21);
runtime.globalObject().set("myGlobVar", globRef);
JSReference resultRef = runtime.executeScript("27 + (myGlobVar * 2)");
Long result = ((JSNumber) runtime.resolveReference(resultRef)).getLongValue(); // result is 69
It is also possible to use different runtime instances at once. Runtime instances are completely isolated which means they don't share memory (even global variables are different). Never pass/resolve a reference coming from a runtime instance with a different one!; this could lead to an undefined behaviour, most likely an exception to be thrown.
JJBridge allows you to debug JavaScript code via the Chrome DevTools Inspector protocol. To enable debugging via the inspector you have to follow these steps:
- Create an inspector and attach it to a runtime:
// ... JSInspector inspector = engine.newInspector(9090); // use a free port on your machine try (JSRuntime runtime = engine.newRuntime()) { inspector.attach(runtime); // Do JavaScript things here... } // ...
- Run the app.
- Connect to the inspector at
chrome-devtools://devtools/bundled/inspector.html?v8only=true&ws=IP:PORT
where:IP
is the IP address of the machine running the code;PORT
is the port passed to the inspector (9090 in the example up here).
- Debug 🎉
Before releasing your app it is recommended to remove all inspector-related code to avoid undesired access to the JavaScript code shipped with it.
See the LICENSE file for license rights and limitations (MIT).