JGrab - run Java code fast, from source, with a little Rust help

Post date: May 21, 2017 11:19:51 AM

One of the main problems Java has, in my opinion, is how hard it is to run a simple Java class without the help of a complex build system.

If you don't need any external dependencies, it may be easy enough to run javac Hello.java, followed by java Hello (but even this can get tiring quickly). But if you need dependencies, then it becomes completely impractical to download the dependencies by hand, and then type the full classpath every time you need to run the code.

Not to mention the performance of code run in this way is pretty bad (due to the JVM startup and warmup times), even with the great improvements seen in the latest Java versions.

For this reason, even veteran Java developers often choose to write tiny applications in JavaScript or Python, for example, or even bash on a bad day. These scripting languages let you write a file and run it, with decent performance, without fuss... and installing dependencies with npm (for NodeJS) or pip (for Python) is just a simple command away.

But using these languages comes with its own problems if you use Java as your main language: unfamiliar syntax and libraries, often lower quality libraries than what we're used in the Java world, low performance for longer-running tasks, often complex ecosystems once you start trying to use more advanced things (especially in the JS world), lack of static type checking causing trivial errors to only be caught at runtime, sometimes.

Each reason may be weak on its own, but put together they really add up and, at least for me, make it hard to justify using these languages for anything but the most trivial utilities.

But the Java world offers basically no alternatives (except perhaps Groovy, which solves most, but not all of these problems).

That's why I created JGrab!

JGrab lets you run your Java source file quickly and with 0 setup. If you need to use dependencies, just add them to the Java file itself and JGrab will download and cache it for the next runs.

As explained in the JGrab README page, you can download and install JGrab on most OSs with the following command:

curl https://raw.githubusercontent.com/renatoathaydes/jgrab/master/releases/install.sh -sSf | sh

If you're on Unix-like systems (or Windows using a bash system), create a link to the JGrab executable to make it easy to run it from anywhere:

sudo ln -s $HOME/.jgrab/jgrab-client /usr/local/bin/jgrab

And that's it! You're all set up.

Using JGrab

To try it out, you can run a simple Java expression:

jgrab -e 2 + 2

The first time you run JGrab, it will automatically start the JGrab Daemon, which will be used in to compile/run code in subsequent runs.

Or a statement (statements are things that do not return any value, as opposed to expressions) by terminating it with a semi-colon:

jgrab -e 'System.out.println("Hello world!");'

To run a Java class (must either be Runnable or have a traditional main method), just pass its path to JGrab.

For example, create the following file in the local directory:

Hello.java

public class Hello implements Runnable {

    public void run() {

        System.out.println( "Hello JGrab" );

    }

}

Then, run it with:

jgrab Hello.java

You can also pipe source code directly to JGrab:

cat Hello.java | jgrab

If you want to use some dependency, declare it as a #jgrab groupdId:artifactId:[version] comment directive anywhere in the source.

For example, to use Guava:

UsesGuava.java

 // #jgrab com.google.guava:guava:19.0

import com.google.common.collect.ImmutableMap;

import java.util.Map;

public class UsesGuava {


    public static void main(String[] args) {

        Map<String, Integer> items = ImmutableMap.of("coin", 3, "glass", 4, "pencil", 1);

        items.entrySet()

                .stream()

                .forEach(System.out::println);

    }

}

You can run the file with JGrab, as usual:

jgrab UsesGuava.java

Notice that JGrab will download all transitive dependencies the first time you run this file...

subsequent runs will use the cached jars, so will run much faster!

JGrab Performance

To get an idea JGrab's performance compared to running a compiled Java class using the java command, here's the results I got on my machine using Java 8 update 131, as measured by using the shell time command:

JGrab command:

jgrab UsesGuava.java

Javac + Java command:

javac -cp ~/.ivy2/cache/com.google.guava/guava/bundles/guava-19.0.jar UsesGuava.java

java -cp  ~/.ivy2/cache/com.google.guava/guava/bundles/guava-19.0.jar:. UsesGuava

So, performance-wise, JGrab does not suffer much from having to compile the source before running it, if at all (of course, this will depend on how long the Java compiler takes to generate the bytecode, but from experience, the compiler is really fast even for bigger files). And the simplicity of using JGrab VS javac+java is pretty obvious!

I hope to improve JGrab performance further in the future by re-using the ClassLoader used to execute each source (so that not all classes need to be loaded on each run).

How does JGrab work?

JGrab uses a native client written in Rust to send code to execute to the JGrab Daemon, written in Java.

I should mention that to build the Rust binaries for all different operating systems, I used the japaric/trust template, which makes it easy to leverage GitHub, Travis CI and AppVeyor to build, test and distribute the executables. The install script was copied and adapted from the rustup project (the script detects which OS, architecture, bitness a system has, then automatically selects the appropriate binary to download).

The Java daemon is hosted on JCenter and uses Apache Ivy to resolve dependencies, so it works really well with Maven repositories.

An in-memory Java compiler I wrote for the OSGiaaS project is used to actually compile the Java source. This compiler was originally written for the OSGiaaS Java REPL and is very fast and reliable because it never writes anything to disk and leverages the JavaCompiler mechanism (which means you must have tools.jar, distributed with the Java SDK, in the classpath). This allows JGrab to support both Java 8 and Java 9 seamlessly (all that matters is which Java version you use to run JGrab).

The JGrab client sends code to the JGrab daemon via a socket on 127.0.0.1:5002. It is not meant for remote code execution, though it would probably work if you exposed this port on the network (which I do not recommend at all!). By design, JGrab only handles a single program at a time, so multi-user setups are not supported.

Conclusion

JGrab offers a new alternative to running Java code simply and easily from the command-line. It is part of my long-term crusade against bash programming!

I had already created an alternative to using bash, awk and company on the shell that allows invoking code from a shell in almost any JVM language (including Java, Groovy, Scala, Clojure and Frege)... with JGrab, I hope to make it easy to replace the cryptic languages of times past with proper Java utilities which are debuggable and readable.

JGrab has also been a great exercise to get my feet wet with Rust, and though I found that learning it was quite hard, it was fun to learn really interesting low-level language concepts as well as a quite advanced type-system. I am happy with the results!