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