Skip to main content

GraalVM and NodeJs Interoperability

Pushing the envelop even further, I'll attempt to fuse parts from the previous articles into a single executable - specifically I will use the javascript handler functions from Java router-like functions.

Create an empty Java application#

Use you favorite method (IDE or terminal) to create a new java project. I will illustrate here using gradle

initialize new project using gradle
mkdir java-calc-srvcd java-calc-srv
gradle init evaluating another scriptevaluating init sript
Select type of project to generate:  1: basic  2: application  3: library  4: Gradle pluginEnter selection (default: basic) [1..4] 2
Select implementation language:  1: C++  2: Groovy  3: Java  4: Kotlin  5: Scala  6: SwiftEnter selection (default: Java) [1..6] 3
Split functionality across multiple subprojects?:  1: no - only one application project  2: yes - application and library projectsEnter selection (default: no - only one application project) [1..2] 1
Select build script DSL:  1: Groovy  2: KotlinEnter selection (default: Groovy) [1..2] 1
Select test framework:  1: JUnit 4  2: TestNG  3: Spock  4: JUnit JupiterEnter selection (default: JUnit 4) [1..4] 4
Project name (default: java-calc-svc):Source package (default: java.calc.svc): works.hop.calc.svc
> Task :initGet more help with your project: https://docs.gradle.org/6.8.3/samples/sample_building_java_applications.html
BUILD SUCCESSFUL in 1m 42s2 actionable tasks: 2 executed
gradle wrapper
BUILD SUCCESSFUL in 1s1 actionable task: 1 up-to-date [44ms]

Let's add some jetty dependencies

implementation group: 'org.eclipse.jetty', name: 'jetty-server', version: '11.0.6'implementation group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '11.0.6'

For logging purposes, let's get one more concern out of the way

implementation group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.8.0-beta4'

Add a log4j properties file

src/main/resurce/log4j.properties
# Root logger optionlog4j.rootLogger=INFO, stdout
# Direct log messages to stdoutlog4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.Target=System.outlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1} - %m%n

With the dependencies in placee, let's create a basic servlet-based application. Be advised that the code used here is for illustration purposes only, and is definately not production-calibre whatsoever. Create an App class

public class App {
    private static final Server server = new Server();    private static final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    public static void initServer() {        ServerConnector http = new ServerConnector(server);        http.setHost("localhost");        http.setPort(8080);        http.setIdleTimeout(30000);        server.addConnector(http);    }
    public static void main(String[] args) throws Exception {        initServer();        context.setContextPath("/");        server.setHandler(context);
        //add a servlet handler        context.addServlet(new ServletHolder("Hello Servlet", new HttpServlet(){            @Override            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {                resp.setStatus(200);                PrintWriter out = resp.getWriter();                out.println("Hello World servlet");            }        }), "/");
        server.start();        server.join();    }}

Start the applicaiton and verify that it works

$ curl -X GET   http://localhost:8080/  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                 Dload  Upload   Total   Spent    Left  Speed100    21  100    21    0     0     93      0 --:--:-- --:--:-- --:--:--    93Hello World servlet

Upon examining the program closer, you will observer that most of the program actually constitutes the plumbing around the servlet handler. So if this can be refactored away, then we'd have a smaller surface area of moving parts. Let's try that

public class AppV2 {
    private static final Server server = new Server();    private static final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    private static void initServer() {        ServerConnector http = new ServerConnector(server);        http.setHost("localhost");        http.setPort(8080);        http.setIdleTimeout(30000);        server.addConnector(http);    }
    private static void initContextHandler() {        context.setContextPath("/");        server.setHandler(context);    }
    private static void register(HttpServlet servlet, String pathSpec) {        context.addServlet(new ServletHolder(servlet), pathSpec);    }
    private static void addHandler(String pathSpec, BiConsumer<HttpServletRequest, HttpServletResponse> handler) {        register(new HttpServlet() {            @Override            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {                handler.accept(req, resp);            }        }, pathSpec);    }
    private static void start() throws Exception {        server.start();        server.join();    }
    public static void main(String[] args) throws Exception {        initServer();        initContextHandler();
        addHandler("/", (req, resp) -> {            resp.setStatus(200);            PrintWriter out;            try {                out = resp.getWriter();                out.println("Hello World servlet");            } catch (IOException e) {                e.printStackTrace();                System.exit(1);            }        });
        addHandler("/time", (req, resp) -> {            resp.setStatus(200);            PrintWriter out;            try {                out = resp.getWriter();                out.println(LocalDate.now().toString());            } catch (IOException e) {                e.printStackTrace();                System.exit(1);            }        });
        start();    }}

With this refactoring attempt, the program continues to work the same way as it did before;

curl -X GET   http://localhost:8080/time  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                 Dload  Upload   Total   Spent    Left  Speed100    12  100    12    0     0     51      0 --:--:-- --:--:-- --:--:--    502021-08-06

But there is also one additional benefit - the responsibility of adding a handler to the server has been decoupled from the rest of the code

private static void addHandler(String pathSpec, BiConsumer<HttpServletRequest, HttpServletResponse> handler)

In this simplified scenario, the addHandler method simply passes on the request and response parameters to the handler supplied. This concept can be extended to also accept an additional argument - the HTTP method verb, or even going further and creating additional interfaces to abstract away the HttpServletRequest and HttpServletResponse classes, as illustrated here.

private static void addHandler(String httpMethod, String pathSpec, BiConsumer<Request, Response> handler)
// additional interfacespublic interface Request {
    byte[] body();
    String method();
    Map<String, String> headers();
    Map<String, List<String>> params();
    <T>T param(String key);}
public interface Response {
    HttpServletResponse response();
    Response status(int code);
    int status();
    Response contentType(String type);
    String contentType();
    Gson gson();
    void send(String payload);
    void json(Object payload);
    void error(String message);
    Promise onComplete();}
public interface Promise {
    <T> void resolve(CompletableFuture<T> future);
    void reject(Throwable throwable);
    boolean done();}

For the sake of simplicity and for staying focused, I will stick with the GET http method alone for now. Now that adding a handler is relatively easy, it's a good time to try using a Javascript handler.

Just to make sure you are set up correctly, verify that you are using GraalVM for your java compiler, and run the sample code below.

public static void main(String[] args) throws Exception {    ScriptEngine eng = new ScriptEngineManager().getEngineByName("graal.js");    Object fn = eng.eval("(function() { return Date.now(); })");    Invocable inv = (Invocable) eng;    Object result = inv.invokeMethod(fn, "call", fn);    System.out.println(result);}

You should get a valid result printed to the console, for instance

1.628296792256E12

With that out of the way, let's tweak the Application class a little bit by encapsulating the fields, and making the functions publically accessible.

public class AppV3 {
    private final Server server = new Server();    private final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
    public void initServer() {        ServerConnector http = new ServerConnector(server);        http.setHost("localhost");        http.setPort(8080);        http.setIdleTimeout(30000);        server.addConnector(http);    }
    public void initContextHandler() {        context.setContextPath("/");        server.setHandler(context);    }
    private void register(HttpServlet servlet, String pathSpec) {        context.addServlet(new ServletHolder(servlet), pathSpec);    }
    public void addHandler(String pathSpec, BiConsumer<HttpServletRequest, HttpServletResponse> handler) {        register(new HttpServlet() {            @Override            protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {                handler.accept(req, resp);            }        }, pathSpec);    }
    public void start() throws Exception {        server.start();        server.join();    }}

Create a javascript file in the resource folder to run the application

src/main/resources/server.js
let app = new AppV3()
//initialize appapp.initServer();app.initContextHandler();
//add handlerapp.addHandler("/", function (req, resp)  {    resp.setStatus(200);    let out;    try {        out = resp.getWriter();        out.println(Date.now());    } catch (e) {        e.printStackTrace();    }});
//start the serverapp.start();

Add a main method in the AppV3 class to start up using GraalVM

public static void main(String[] args) throws Exception {    ScriptEngine eng = new ScriptEngineManager().getEngineByName("graal.js");    Bindings bindings = eng.getBindings(ScriptContext.ENGINE_SCOPE);    bindings.put("polyglot.js.allowHostAccess", true);    bindings.put("polyglot.js.allowHostClassLookup", (Predicate<String>) s -> true);    bindings.put("AppV3", AppV3.class);    Object fn = eng.eval(new InputStreamReader(Objects.requireNonNull(AppV3.class.getResourceAsStream("/server.js"))));    Invocable inv = (Invocable) eng;    Object result = inv.invokeMethod(fn, "call", fn);    System.out.println(result);}

Attemp 1

The application should start successfully. That's some major progress. Now use curl to access the registered / handler

curl -X GET   http://localhost:8080/time  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                 Dload  Upload   Total   Spent    Left  Speed  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--    

Oh no! The response has an error. On examining the error, you will see this:

<tr><th>CAUSED BY:</th><td>Attached Guest Language Frames (1)</td></tr></table><h3>Caused by:</h3><pre>java.lang.IllegalStateException: Multi threaded access requested by thread Thread[qtp1043317832-26,5,main] but is not allowed for language(s) js.

This is the one caveat you will have to be aware of. Since Javascript is single-threaded in nature, it is not possible to execute it in a multi-threaded context, like in a Java application server. So this approach will not work. However, all is not lost. There exists a different way you can use to dice the issue - having javascript get executed with its own context so that it will be sufficiently isolated in the calling thread. Let give this a try

info

Version 2 of the same attempt

I think it's worth notig here that you can also attempt the same thing by executing the server.js file directly using GraalVM's nodejs implementation. But to do this, you would need to handle some additional concerns

  1. Create a fatjar of your project so that all necessary dependencies are bundled together
  2. Add reference to the Application class in the javascript file
  3. Use special swicthes in the command line to enable interopeability

Building a fat-jar#

For gradle, this is pretty straight-forward. For a simple use case like this one, simply add the following to the gradle.build file

jar {    from {        configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) }    }}

For other build tools (like maven), there must be plugins that will achieve the same result

Add reference to Java class in JS file#

In the handler.js file, you will need to add a reference to the AppV3 class in java for interoperability. Change the beginning of the file like show here

let App = Java.type("works.hop.calc.svc.AppV3")let app = new App()... // the rest stays the same

Verify the bundle content#

To check that the jar artifact contains all dependencies nessasry to distribute it, run the following command (assuming the bundle name is app.jar). This should print to the console a list of files existing inside the jar

jar tf app\build\libs\app.jar

Command-line options for interoperability#

Fire up the server using GraalVM's nodejs

>%GRAALVM_HOME%\bin\node.cmd --jvm --vm.cp=app\build\libs\app.jar app\src\main\resources\server.js

This should once again start the server successfully, but you will still run into the same issue as before.

Attemp 2

Add a modified version of the addHandler function to the AppV3 class, and call it addJsHandler for convenience

public void addJsHandler(String pathSpec, String jsHandler) throws IOException {
    String literalFunction;    try (BufferedReader reader = new BufferedReader(            new InputStreamReader(Objects.requireNonNull(AppV3.class.getResourceAsStream(String.format("/%s.js", jsHandler))))    )) {        literalFunction = reader.lines().collect(Collectors.joining());    }
    register(new HttpServlet() {        @Override        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {            try (Context context = Context.newBuilder("js")                    .allowHostAccess(HostAccess.ALL)                    //allows access to all Java classes                    .allowHostClassLookup(className -> true)                    .build()) {                Value value = context.eval("js", literalFunction);                value.execute(req, resp);            }        }    }, pathSpec);}

Using this approach will require a new dependency, so add this to your gradle dependencies

implementation group: 'org.graalvm.js', name: 'js', version: '21.2.0'

You will also create a modified version of the previous handler.js file

"src/main/resources/handler.js
(function handle(req, resp)  {    resp.setStatus(200);    let out;    try {        out = resp.getWriter();        out.println(Date.now());    } catch (e) {        e.printStackTrace();    }});

Now adjust the main method in AppV3 class to use this new approach

public static void main(String[] args) throws Exception {    AppV3 app = new AppV3();    app.initServer();    app.initContextHandler();    app.addJsHandler("/time", "handler");    app.start();}

The server starts successfully again. Execute a curl request to the /time handler. This time, everything works as you's expect.

curl -X GET   http://localhost:8080/time  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current                                 Dload  Upload   Total   Spent    Left  Speed100    15  100    15    0     0     11      0  0:00:01  0:00:01 --:--:--    111628302442840

This exercise illustrated just how awesome working with GraalVM really is, and how working with both Javascript and Java in the same code really becomes a breeze.