Running JavaScript in JVM with Nashorn Successor in Java

Introduction

With the deprecation of Nashorn in JDK 11, new approaches for running JavaScript on the JVM have emerged. The primary successors are GraalVM's JavaScript engine and alternative solutions that provide seamless JavaScript integration with Java applications.

GraalVM JavaScript Engine

Setting Up GraalVM JavaScript

// Maven dependencies for GraalVM JavaScript
/*
<dependency>
<groupId>org.graalvm.polyglot</groupId>
<artifactId>polyglot</artifactId>
<version>23.0.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>23.0.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>23.0.0</version>
</dependency>
*/
public class GraalVMSetup {
public void checkGraalVMAvailability() {
try {
Context context = Context.create("js");
System.out.println("GraalVM JavaScript engine is available");
context.close();
} catch (Exception e) {
System.err.println("GraalVM JavaScript engine not available: " + e.getMessage());
}
}
}

Basic JavaScript Execution

Simple Script Execution

import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;
public class BasicJavaScriptExecution {
public void executeSimpleScript() {
try (Context context = Context.create("js")) {
// Execute simple JavaScript
Value result = context.eval("js", "1 + 2 * 3");
System.out.println("Result: " + result.asInt()); // Result: 7
// Execute JavaScript functions
context.eval("js", "function greet(name) { return 'Hello, ' + name; }");
Value greetFunction = context.getBindings("js").getMember("greet");
Value greeting = greetFunction.execute("World");
System.out.println(greeting.asString()); // Hello, World
}
}
public void executeScriptWithVariables() {
try (Context context = Context.create("js")) {
// Set variables in JavaScript context
context.getBindings("js").putMember("x", 10);
context.getBindings("js").putMember("y", 20);
Value result = context.eval("js", "x + y");
System.out.println("x + y = " + result.asInt()); // x + y = 30
// Modify variables from JavaScript
context.eval("js", "z = x * y;");
Value z = context.getBindings("js").getMember("z");
System.out.println("z = " + z.asInt()); // z = 200
}
}
public void executeFileScript(String filePath) {
try (Context context = Context.create("js")) {
// Read and execute JavaScript from file
String script = new String(Files.readAllBytes(Paths.get(filePath)));
Value result = context.eval("js", script);
System.out.println("Script result: " + result);
} catch (IOException e) {
System.err.println("Error reading script file: " + e.getMessage());
}
}
}

Java-JavaScript Interoperability

Calling Java from JavaScript

public class JavaToJavaScriptInterop {
public static class UserService {
public String getUserName(int userId) {
return "User_" + userId;
}
public boolean isUserActive(int userId) {
return userId % 2 == 0;
}
public List<String> getUserRoles(int userId) {
return Arrays.asList("user", "reader");
}
}
public void exposeJavaToJavaScript() {
try (Context context = Context.create("js")) {
// Expose Java object to JavaScript
UserService userService = new UserService();
context.getBindings("js").putMember("userService", userService);
// Call Java methods from JavaScript
String script = """
var userName = userService.getUserName(123);
var isActive = userService.isUserActive(123);
var roles = userService.getUserRoles(123);
console.log('User: ' + userName);
console.log('Active: ' + isActive);
console.log('Roles: ' + roles);
// Return result to Java
{
name: userName,
active: isActive,
roles: roles
};
""";
Value result = context.eval("js", script);
System.out.println("User data: " + result);
}
}
public void useProxyObjects() {
try (Context context = Context.create("js")) {
// Create proxy objects for more control
ProxyObject userProxy = ProxyObject.fromMap(Map.of(
"name", "John Doe",
"age", 30,
"email", "[email protected]"
));
context.getBindings("js").putMember("user", userProxy);
String script = """
console.log('User: ' + user.name);
console.log('Age: ' + user.age);
user.age = 31;  // Modify property
user.country = 'USA';  // Add new property
""";
context.eval("js", script);
// Access modified object from Java
Value user = context.getBindings("js").getMember("user");
System.out.println("Updated age: " + user.getMember("age").asInt());
System.out.println("New country: " + user.getMember("country").asString());
}
}
}

Calling JavaScript from Java

public class JavaScriptToJavaInterop {
public void callJavaScriptFunctions() {
try (Context context = Context.create("js")) {
// Define JavaScript functions
String script = """
function calculateArea(width, height) {
return width * height;
}
function formatUser(user) {
return user.name + ' (' + user.email + ')';
}
function processArray(numbers) {
return numbers.map(n => n * 2).filter(n => n > 10);
}
""";
context.eval("js", script);
// Call JavaScript functions from Java
Value calculateArea = context.getBindings("js").getMember("calculateArea");
Value area = calculateArea.execute(5, 10);
System.out.println("Area: " + area.asInt()); // Area: 50
// Pass Java objects to JavaScript
Map<String, Object> user = Map.of("name", "Alice", "email", "[email protected]");
Value formatUser = context.getBindings("js").getMember("formatUser");
Value formatted = formatUser.execute(user);
System.out.println("Formatted user: " + formatted.asString());
// Pass arrays between Java and JavaScript
Value processArray = context.getBindings("js").getMember("processArray");
Value processed = processArray.execute(new int[]{1, 5, 8, 12, 3});
System.out.println("Processed array: " + processed);
}
}
public void useJavaScriptLibraries() {
try (Context context = Context.create("js")) {
// Load and use JavaScript libraries
String lodashScript = """
// Simplified lodash-like functions
const _ = {
chunk: function(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
},
groupBy: function(array, key) {
return array.reduce((groups, item) => {
const group = item[key];
if (!groups[group]) groups[group] = [];
groups[group].push(item);
return groups;
}, {});
}
};
""";
context.eval("js", lodashScript);
// Use JavaScript library functions from Java
Value chunkFunction = context.getBindings("js").getMember("_").getMember("chunk");
Value chunks = chunkFunction.execute(new int[]{1, 2, 3, 4, 5, 6}, 2);
System.out.println("Chunks: " + chunks);
}
}
}

Advanced GraalVM Features

Performance Optimization

public class GraalVMOptimizations {
public void enableOptimizations() {
Context.Builder contextBuilder = Context.newBuilder("js")
.option("js.ecmascript-version", "2023")  // Use latest ECMAScript
.option("js.experimental-foreign-object-prototype", "true")  // Better interop
.allowExperimentalOptions(true)
.allowAllAccess(true);  // Allow full access between Java and JS
try (Context context = contextBuilder.build()) {
// Optimized context for performance
String complexScript = """
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// This will be optimized by GraalVM
fibonacci(35);
""";
long startTime = System.nanoTime();
Value result = context.eval("js", complexScript);
long endTime = System.nanoTime();
System.out.println("Fibonacci result: " + result.asInt());
System.out.println("Execution time: " + (endTime - startTime) / 1_000_000 + "ms");
}
}
public void useEngineCaching() {
// Cache compiled scripts for better performance
Map<String, Source> scriptCache = new ConcurrentHashMap<>();
public Value executeCachedScript(Context context, String scriptId, String scriptCode) {
Source source = scriptCache.computeIfAbsent(scriptId, id -> 
Source.newBuilder("js", scriptCode, scriptId).build());
return context.eval(source);
}
public void demonstrateCaching() {
try (Context context = Context.create("js")) {
String script = "Math.pow(2, 10)";
// First execution - compiles and caches
long start1 = System.nanoTime();
Value result1 = executeCachedScript(context, "powerScript", script);
long end1 = System.nanoTime();
// Second execution - uses cached version
long start2 = System.nanoTime();
Value result2 = executeCachedScript(context, "powerScript", script);
long end2 = System.nanoTime();
System.out.println("First execution: " + (end1 - start1) / 1000 + "µs");
System.out.println("Second execution: " + (end2 - start2) / 1000 + "µs");
System.out.println("Result: " + result1.asInt());
}
}
}
}

Multi-Language Integration

public class MultiLanguageIntegration {
public void mixJavaScriptWithOtherLanguages() {
try (Context context = Context.newBuilder()
.allowAllAccess(true)
.build()) {
// Use JavaScript with other GraalVM languages
String multiLangScript = """
// JavaScript code
function processData(data) {
// Call R for statistical analysis
const rResult = Polyglot.eval('R', 
'mean(c(' + data.join(',') + '))');
// Call Python for ML processing
const pythonResult = Polyglot.eval('python', 
'import numpy as np; np.array([' + data.join(',') + ']).std()');
return {
mean: rResult,
stdDev: pythonResult
};
}
processData([1, 2, 3, 4, 5]);
""";
Value result = context.eval("js", multiLangScript);
System.out.println("Multi-language result: " + result);
}
}
public void useBuildersForComplexScripts() {
Source.Builder scriptBuilder = Source.newBuilder("js", 
"""
function complexCalculation(input) {
let result = 0;
for (let i = 0; i < input.length; i++) {
result += input[i] * Math.sin(i * Math.PI / 180);
}
return result;
}
complexCalculation(data);
""", "complexCalculation.js");
try (Context context = Context.create("js")) {
Source source = scriptBuilder.build();
context.getBindings("js").putMember("data", new double[]{10, 20, 30, 40, 50});
Value result = context.eval(source);
System.out.println("Complex calculation result: " + result.asDouble());
} catch (IOException e) {
System.err.println("Error building source: " + e.getMessage());
}
}
}

Alternative Approaches

Using ScriptEngineManager (Compatibility Layer)

public class ScriptEngineCompatibility {
public void useScriptEngineAPI() {
try {
// Create script engine for JavaScript
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("graal.js");
if (engine == null) {
System.err.println("Graal.js engine not available");
return;
}
// Traditional ScriptEngine API usage
engine.put("name", "World");
Object result = engine.eval("'Hello, ' + name + '!'");
System.out.println(result); // Hello, World!
// Invoke JavaScript functions
engine.eval("function multiply(a, b) { return a * b; }");
Invocable invocable = (Invocable) engine;
Object multiplyResult = invocable.invokeFunction("multiply", 6, 7);
System.out.println("6 * 7 = " + multiplyResult); // 42
} catch (ScriptException | NoSuchMethodException e) {
System.err.println("Script execution error: " + e.getMessage());
}
}
public void nashornCompatibilityLayer() {
try {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("graal.js");
// Enable Nashorn compatibility mode
if (engine instanceof GraalScriptEngine) {
GraalScriptEngine graalEngine = (GraalScriptEngine) engine;
graalEngine.eval("load('nashorn:mozilla_compat.js');");
}
// Use some Nashorn-specific features
engine.eval("""
// Nashorn compatibility
var ArrayList = Java.type('java.util.ArrayList');
var list = new ArrayList();
list.add('Hello');
list.add('World');
list;
""");
Object result = engine.eval("list");
System.out.println("Java list from JavaScript: " + result);
} catch (ScriptException e) {
System.err.println("Compatibility error: " + e.getMessage());
}
}
}

Spring Boot Integration

@Component
public class JavaScriptService {
private final Context context;
@Autowired
public JavaScriptService(UserRepository userRepository) {
this.context = Context.newBuilder("js")
.allowAllAccess(true)
.build();
// Expose Spring beans to JavaScript
context.getBindings("js").putMember("userRepository", userRepository);
context.getBindings("js").putMember("javaScriptService", this);
}
public Object executeBusinessRule(String ruleScript, Map<String, Object> parameters) {
try {
// Set parameters in JavaScript context
parameters.forEach((key, value) -> 
context.getBindings("js").putMember(key, value));
// Execute business rule
Value result = context.eval("js", ruleScript);
return result.isNull() ? null : result.as(Object.class);
} catch (PolyglotException e) {
throw new JavaScriptExecutionException("Failed to execute JavaScript rule", e);
}
}
public List<User> filterUsersWithJavaScript(List<User> users, String filterScript) {
try {
context.getBindings("js").putMember("users", users);
String script = """
const filteredUsers = users.filter(user => %s);
javaScriptService.convertToJavaList(filteredUsers);
""".formatted(filterScript);
Value result = context.eval("js", script);
return result.as(new TypeLiteral<List<User>>() {});
} catch (PolyglotException e) {
throw new JavaScriptExecutionException("Failed to filter users", e);
}
}
// Called from JavaScript to convert results
public List<User> convertToJavaList(Value jsArray) {
List<User> result = new ArrayList<>();
for (long i = 0; i < jsArray.getArraySize(); i++) {
Value userValue = jsArray.getArrayElement(i);
User user = userValue.as(User.class);
result.add(user);
}
return result;
}
@PreDestroy
public void cleanup() {
if (context != null) {
context.close();
}
}
}
@RestController
public class JavaScriptRuleController {
@Autowired
private JavaScriptService jsService;
@PostMapping("/api/rules/execute")
public ResponseEntity<?> executeRule(@RequestBody RuleExecutionRequest request) {
try {
Object result = jsService.executeBusinessRule(
request.getScript(), 
request.getParameters()
);
return ResponseEntity.ok(Map.of("result", result));
} catch (JavaScriptExecutionException e) {
return ResponseEntity.badRequest()
.body(Map.of("error", e.getMessage()));
}
}
}

Error Handling and Debugging

Comprehensive Error Handling

public class JavaScriptErrorHandling {
public void executeWithErrorHandling() {
try (Context context = Context.create("js")) {
String problematicScript = """
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
divide(10, 0);
""";
try {
Value result = context.eval("js", problematicScript);
System.out.println("Result: " + result);
} catch (PolyglotException e) {
System.err.println("JavaScript error: " + e.getMessage());
System.err.println("Source location: " + e.getSourceLocation());
System.err.println("Stack trace: " + Arrays.toString(e.getStackTrace()));
}
}
}
public void addErrorListener() {
try (Context context = Context.newBuilder("js")
.onPolyglotException((exception) -> {
System.err.println("Polyglot exception occurred:");
System.err.println("  Message: " + exception.getMessage());
System.err.println("  Language: " + exception.getLanguage());
if (exception.getSourceLocation() != null) {
System.err.println("  Location: " + exception.getSourceLocation());
}
return null; // Continue execution
})
.build()) {
String script = """
console.log('This will execute');
throw new Error('This is a test error');
console.log('This will not execute');
""";
context.eval("js", script);
}
}
public void validateScriptSyntax(String script) {
try (Context context = Context.create("js")) {
// Parse without executing to check syntax
Source source = Source.newBuilder("js", script, "validation.js").build();
source.getCharacters(); // This will throw if syntax is invalid
System.out.println("Script syntax is valid");
} catch (IOException e) {
System.err.println("Invalid script syntax: " + e.getMessage());
}
}
}

Real-World Use Cases

Dynamic Business Rules Engine

public class BusinessRulesEngine {
private final Map<String, Source> ruleCache = new ConcurrentHashMap<>();
private final Context context;
public BusinessRulesEngine() {
this.context = Context.newBuilder("js")
.allowAllAccess(true)
.build();
// Load utility functions
loadUtilityFunctions();
}
private void loadUtilityFunctions() {
String utils = """
function isWeekend(date) {
const day = date.getDay();
return day === 0 || day === 6;
}
function calculateDiscount(amount, percentage) {
return amount * (percentage / 100);
}
function isEligibleForPromotion(user, orderAmount) {
return user.memberLevel === 'GOLD' && orderAmount > 100;
}
""";
context.eval("js", utils);
}
public void registerRule(String ruleId, String ruleScript) {
Source source = Source.newBuilder("js", ruleScript, ruleId + ".js").build();
ruleCache.put(ruleId, source);
}
public Object executeRule(String ruleId, Map<String, Object> contextVars) {
Source source = ruleCache.get(ruleId);
if (source == null) {
throw new IllegalArgumentException("Rule not found: " + ruleId);
}
try {
// Set context variables
contextVars.forEach((key, value) -> 
context.getBindings("js").putMember(key, value));
return context.eval(source).as(Object.class);
} catch (PolyglotException e) {
throw new RuleExecutionException("Failed to execute rule: " + ruleId, e);
}
}
// Example usage
public void demonstrateRuleEngine() {
// Register pricing rule
registerRule("discountRule", """
function applyDiscount(order) {
let discount = 0;
if (isEligibleForPromotion(order.user, order.amount)) {
discount = calculateDiscount(order.amount, 10);
}
if (order.amount > 500) {
discount += calculateDiscount(order.amount, 5);
}
return {
originalAmount: order.amount,
discount: discount,
finalAmount: order.amount - discount
};
}
applyDiscount(order);
""");
// Execute rule
Map<String, Object> order = Map.of(
"amount", 600.0,
"user", Map.of("memberLevel", "GOLD", "name", "John Doe")
);
Object result = executeRule("discountRule", Map.of("order", order));
System.out.println("Discount result: " + result);
}
}

Template Rendering Engine

public class JavaScriptTemplateEngine {
private final Context context;
public JavaScriptTemplateEngine() {
this.context = Context.newBuilder("js")
.allowAllAccess(true)
.build();
// Load template engine library (simplified)
loadTemplateLibrary();
}
private void loadTemplateLibrary() {
String templateLib = """
function renderTemplate(template, data) {
return template.replace(/\\{\\{([^}]+)\\}\\}/g, function(match, key) {
return data[key.trim()] || '';
});
}
function renderLoopTemplate(template, items, itemName) {
const loopPattern = new RegExp(
`{% for ${itemName} in items %}(.*?){% endfor %}`, 
'gs'
);
return template.replace(loopPattern, function(match, content) {
return items.map(item => 
renderTemplate(content, {[itemName]: item})
).join('');
});
}
""";
context.eval("js", templateLib);
}
public String renderTemplate(String template, Map<String, Object> data) {
try {
context.getBindings("js").putMember("template", template);
context.getBindings("js").putMember("data", data);
Value result = context.eval("js", 
"renderTemplate(template, data)");
return result.asString();
} catch (PolyglotException e) {
throw new TemplateRenderingException("Failed to render template", e);
}
}
public String renderWithLoop(String template, List<Object> items, String itemName) {
try {
context.getBindings("js").putMember("template", template);
context.getBindings("js").putMember("items", items);
context.getBindings("js").putMember("itemName", itemName);
Value result = context.eval("js", 
"renderLoopTemplate(template, items, itemName)");
return result.asString();
} catch (PolyglotException e) {
throw new TemplateRenderingException("Failed to render template with loop", e);
}
}
}

Performance Best Practices

public class JavaScriptPerformanceTips {
public void performanceBestPractices() {
// 1. Reuse Context objects
try (Context context = Context.create("js")) {
// Multiple evaluations in same context
for (int i = 0; i < 100; i++) {
context.eval("js", "Math.sqrt(" + i + ")");
}
}
// 2. Use Value objects efficiently
try (Context context = Context.create("js")) {
Value array = context.eval("js", "[1, 2, 3, 4, 5]");
// Prefer Value methods over converting to Java objects
long size = array.getArraySize();
for (long i = 0; i < size; i++) {
Value element = array.getArrayElement(i);
System.out.println(element.asInt());
}
}
// 3. Batch operations
try (Context context = Context.create("js")) {
String batchScript = """
function processBatch(items) {
return items.map(item => ({
id: item.id,
processed: item.value * 2,
timestamp: Date.now()
}));
}
""";
context.eval("js", batchScript);
Value processFunction = context.getBindings("js").getMember("processBatch");
List<Map<String, Object>> batchData = createBatchData(1000);
Value result = processFunction.execute(batchData);
// Process result without converting to Java immediately
}
}
private List<Map<String, Object>> createBatchData(int size) {
List<Map<String, Object>> data = new ArrayList<>();
for (int i = 0; i < size; i++) {
data.add(Map.of("id", i, "value", i * 10));
}
return data;
}
// 4. Use proper memory management
public void memoryManagement() {
Context context = Context.newBuilder("js")
.option("js.experimental-foreign-object-prototype", "true")
.build();
try {
// Large data processing
Value largeArray = context.eval("js", "new Array(1000000).fill(0).map((_, i) => i)");
// Process in chunks to avoid memory issues
long chunkSize = 10000;
long arraySize = largeArray.getArraySize();
for (long i = 0; i < arraySize; i += chunkSize) {
Value chunk = context.eval("js", 
"largeArray.slice(" + i + ", " + (i + chunkSize) + ")");
processChunk(chunk);
}
} finally {
context.close();
}
}
private void processChunk(Value chunk) {
// Process chunk of data
System.out.println("Processing chunk of size: " + chunk.getArraySize());
}
}

Conclusion

GraalVM's JavaScript engine provides a robust, high-performance replacement for Nashorn with several advantages:

  1. Better Performance - Advanced JIT compilation and optimizations
  2. Modern ECMAScript - Full support for ES2023+ features
  3. Seamless Interop - Excellent Java-JavaScript interoperability
  4. Multi-Language - Integration with other GraalVM languages
  5. Enterprise Ready - Production-ready with good tooling and monitoring

By following these patterns and best practices, you can successfully migrate from Nashorn and leverage the full power of modern JavaScript execution on the JVM while maintaining compatibility with existing codebases.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper