An easy way to run Java classes from the command line.
The idea came from this neat
Cloudflare blog post
about binfmt_misc
for Go-lang. After seeing that Kotlin is
also ready for scripting,
I wanted to run my Kotlin scripts like ./hello.kt
without having to
compile them first to include all their dependencies in a single
binary. So I wrote krun.
Then I thought... why not Java classes also?
Let's say you have a Java class hello.java
:
public class Main {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Notice the class name inside the file is Main
. This constraint makes it easier
to generate the executable .jar
file, where your class will end up as a Main.class
file in the root package.
After installing jrun
in your PATH
, you can do this:
jrun hello.java
If you register jrun
with binfmt_misc
then you can also do this:
./hello.java
The first time jrun
encounters a script, it compiles it with javac
.
Each subsequent time jrun
encounters the same script, it skips compiling
it and runs the .jar
file that was compiled the first time. The "same"
script is defined by the SHA-256 digest of the script file, so the time stamp,
filename, and path are irrelevant.
The compiled .jar
files are stored in a ~/.jrun
directory. You can change
this by exporting the variable JRUN_CACHE
with the path to a different location
that is writable.
To avoid repetition you can create a file ~/.jrunrc
with the variable like this:
JRUN_CACHE=/var/lib/jrun
If the Java class imports classes that are not part of the JRE, you'll need to set the class path for compiling and running the class.
You can set the class path by creating an Ivy module file. Continuing the
hello.java
example, let's say you have another file hello.ivy.xml
:
<?xml version="1.0"?>
<ivy-module version="2.0">
<info organisation="jrun" module="slf4j"/>
<dependencies>
<dependency org="org.slf4j" name="slf4j-api" rev="1.7.25" />
<dependency org="ch.qos.logback" name="logback-classic" rev="1.2.3" />
</dependencies>
</ivy-module>
Java classes typically depend on other classes. A specially-formatted comment
in the .java
file helps jrun
set the class path in the manifest of the
compiled .jar
file. Here is hello.java
again with dependencies declared
in hello.ivy.xml
:
// @ivy hello.ivy.xml
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main {
final private static Logger LOG = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
LOG.info("Hello World!");
}
}
Ivy downloads artifacts into ~/.ivy2/cache/
or whatever location you configure
in the XML file.
To automatically download dependencies that are not already in the cache,
Ivy will connect to remote repositories. If you have an HTTP or HTTPS proxy
you can configure it in ~/.jrunrc
like this:
HTTP_PROXY_HOST=http-proxy.example.com
HTTP_PROXY_PORT=8080
HTTPS_PROXY_HOST=https-proxy.example.com
HTTPS_PROXY_PORT=8443
If these variables are defined and there is no ~/.m2/settings.xml file, the
jrun
script will create it automatically with the values from these variables.
The following programs need to be in the PATH
:
- java
- javac
- jar
- ant
- mvn
And the following standard utilities also need to be in the PATH
:
- mktemp
- sed
- echo
- mv
- rm
- mkdir
- head
- tail
- fold
To register jrun
with binfmt_misc
in a Docker container,
you must create the container with the --privileged
option.
git clone https://github.com/jbuhacoff/java-jrun.git
( cd java-jrun && make install )
tar xzf jrun-0.1.tar.gz
( cd jrun-0.1 && make install )
There's one test script that demonstrates jrun
with the cache:
make test
This command will create the .tar.gz
for distribution:
make package
Using jrun
adds very little overhead compared to using java -jar ...
directly for a
pre-compiled .jar
file. When a new or edited .java
file is compiled, if new
dependencies are declared they will be downloaded automatically. That delay may be
significant but it's essentially the same time you would have waited for that to
happen while building a .jar
the first time.
Here are some samples from my laptop, informally:
hello.java
first time:
time jrun hello.java
Hello World!
real 0m0.858s
user 0m1.144s
sys 0m0.076s
hello.java
second time:
time jrun hello.java
Hello World!
real 0m0.106s
user 0m0.092s
sys 0m0.008s
hello-ivy-convention.java
first time:
time jrun hello-ivy-convention.java
11:50:50.757 [main] INFO Main - Hello World!
real 0m2.232s
user 0m3.508s
sys 0m0.116s
hello-ivy-convention.java
second time:
time jrun hello-ivy-convention.java
11:50:51.010 [main] INFO Main - Hello World!
real 0m0.241s
user 0m0.268s
sys 0m0.024s
hello-ivy-comment.java
first time:
time jrun hello-ivy-comment.java
11:50:53.214 [main] INFO Main - Hello World!
real 0m2.215s
user 0m3.340s
sys 0m0.172s
hello-ivy-comment.java
second time:
time jrun hello-ivy-comment.java
11:50:53.418 [main] INFO Main - Hello World!
real 0m0.193s
user 0m0.232s
sys 0m0.016s
The times vary, on my system the "real" time is sometimes +/- 0.010s from this typical measurement.