6

Background:

Since Nashorn is being dropped in JDK15, I am looking for an alternative for an application I am working on. I am currently only using for the dynamic execution of some user-definable formatting snippets within a java swing desktop application.

I don't particularly want to add another library dependency to my app (like rhino). I would be willing to use nashorn as an additional dependency if it is available (this would save me from having to rewrite code, and ensure compatibility with existing js snippets). I haven't seen that it is available anywhere except as something that was related to minecraft.

I won't switch to Graal vm.

The problem:

I was considering using JShell (though not javascript, much of the formatting code is very similar), but the performance is abysmal the way I am calling it:

try(JShell js = JShell.create())
{
    js.eval("public int add(int a, int b) { return a + b; }");
    for(int i = 0; i < 100; i++)
    {
        List<SnippetEvent> eval = js.eval("add(5,6)");
        eval.forEach(se -> {
            System.out.println(se.value());
        });
    }
}

The for loop in the that code is taking ~6 seconds to run (compared to ~11 microseconds in nashorn). This will not be fast enough for my application.

  • Is there a way to get the class bytecode back out of JSell so I can use reflection to execute the method directly instead of calling 'eval' again?

  • Is there a way to get a 'method handle' to a method I create in JShell?

  • Is there any way of making a function where the behavior is defined in JShell, but can be called with high performance from 'normal' java?

azro
  • 53,056
  • 7
  • 34
  • 70
Skcussm
  • 658
  • 1
  • 5
  • 20
  • While the accepted answer is brilliant and performant, I'm wondering why you say that Nashorn as a dependency is not available https://stackoverflow.com/questions/65265629/how-to-use-nashorn-in-java-15-and-later – Mike Kim Mar 29 '23 at 06:58
  • 1
    @MikeKim When I originally posted this question, the Nashorn package had only been available as part of the JDK. It wasn't until shortly after this that the maintainers created a standalone version of Nashorn for Java 15+. – Skcussm Mar 31 '23 at 14:24
  • lol I didn't realie this was 3 years old – Mike Kim Apr 01 '23 at 19:27

1 Answers1

3

I stumbled across the same performance issue today and I think I found a solution. It boils down to the following four steps:

  1. starting the JShell inside the currently running JVM using the LocalExecutionControlProvider such that they share the same class loader
  2. defining a static field inside a class that serves as a "shared variable" between the JShell and our "normal" Java code
  3. overwriting the static field inside the JShell with a newly constructed lambda function
  4. storing the reference to the constructed function in a variable inside our "normal" Java code (necessary if we want to construct multiple functions via this approach)

The following code snippet implements this approach for your example. On my machine, it takes approx. 350ms for the initial function construction via JShell and approx. 50ms for the 10_000 subsequent function calls.

package app;

import jdk.jshell.JShell;
import jdk.jshell.execution.LocalExecutionControlProvider;
import org.junit.jupiter.api.Test;

import java.util.function.BiFunction;

public class Debug {

    public static BiFunction<Integer, Integer, Integer> function = null;

    @Test
    public void debug() {
        JShell jShell = JShell.builder()
                .in(System.in).out(System.out).err(System.err)
                .executionEngine(new LocalExecutionControlProvider(), null)
                .build();
        jShell.eval("app.Debug.function = (a,b) -> a+b;");
        BiFunction<Integer, Integer, Integer> theFunction = Debug.function;
        for (int a = 0; a < 100; a++) {
            for (int b = 0; b < 100; b++) {
                assert theFunction.apply(a, b) == a + b;
            }
        }
    }
}
jboockmann
  • 1,011
  • 2
  • 11
  • 27
  • 1
    Excellent. I'm using java 19, and on my machine, I had to bump up the loop counts to 1000x1000 to get a value using System.currentTimeMillis(). So, the 1 million calls took ~40 milliseconds which comes out to about 4 microseconds per 100 calls to compare to my original post. Obviously, not the best way to assess performance, but this is great! Thank you! – Skcussm Mar 23 '23 at 15:48