Meta

Follow Me

Scripting Java 11, Shebang And All

There are several reasons why writing scripts in Java seems to be a bad idea, chief among them that it’s always a two step process to run a source file: It first has to be compiled (with
javac) before it can be executed (with
java). Enter Java 11, which, for a single source file, blends these two steps into one:

1

java HelloJavaScripts.java

Yes, you saw that right: The JVM accepts a source file and executes it. More than that, you can even define proper scripts, with shebang and everything, that you can execute like this:

Single-Source-File Execution

Executing a single source file is straightforward. Simply write a self-contained class with a
main method …

1

2

3

4

5

6

7

publicclassHelloJavaScripts{

publicstaticvoidmain(String[]args){

System.out.println("Hello, Java scripts!");

}

}

… and pass that file to
java:

1

2

$java HelloJavaScripts.java

>Hello,Java scripts!

There you go, you’ve just executed your first Java script! (Pun fully intended.) And with that you can already start experimenting, but, as usual, there are a few details that you should know. Let’s discuss them next before coming to what may be Java 11’s hidden killer feature.

Prerequisites

First of all, the JVM can only compile source files if the jdk.compiler module is present. That’s the case for all full JDKs, but if you’re building your own images with
jlink, you may well end up without it. In that case, you’ll see this error message:

1

2

3

4

Error:AJNI error has occurred,

please check your installation andtryagain

Exception inthread"main"java.lang.InternalError:

Module jdk.compilernotinboot Layer

There’s no way to fix this – you simply have to use a runtime image with that module.

On To The Details!

Once you start using single-source-file programs, you’ll quickly end up in situations where it helps or is even required to explicitly inform the JVM that it’s supposed to execute a source file and which Java version to compile it against. You do that with the
--source option:

1

$java--source11HelloJavaScripts.java

This is particularly interesting in conjunction with preview features (where specifying the source is a requirement anyways):

1

2

3

# running my switch expression demo with Java 12;

# more on that: https://www.youtube.com/watch?v=1znHEf3oSNI

$java--source12--enable-preview Switch.java

You can name the file any way you want

Another situation where
--source needs to be added is if the source file name does not end in
.java. Yes, you got that right, the
.java suffix is not mandatory! In fact, you can name the file any way you want:

1

$java--source11hello-java-scripts

Besides
--source and
--enable-preview, the JVM processes many other command line options like
--class-path,
--module-path, and those to hack the JPMS. If you end up using a lot of flags, you can put them into a so-called @-file and reference that:

1

2

3

4

5

6

# content of file `args`:

--source12

--enable-preview

--class-path'deps/*'

# use that file

$java@args HelloJavaScripts.java

Whatever command line flags you add, as you would expect, all arguments after the file name are passed to the program’s
main method:

1

2

$java--source11Greetings.javahello java scripts

# main receives [ "hello", "java", "scripts" ]

Last and maybe even least, the compiled source will be executed in the unnamed module of a class loader specifically created for it alone. That class loader’s parent is the application class loader (which loads the class path content), which means the main class has access to all dependencies on the class path.

Since the application class loader can’t also access the "main class" loader (no circular class loader dependencies allowed), the inverse is not true and classes from the class path won’t have access to the main class. I know, I know, sadly we can’t execute our Spring/Hibernate applications from a single source file. 😭

But Why?!!

You may wonder what this feature is good for. I mean, if you can’t even write a web-backend with it…

Its primary use case is similar to jshell, Java’s REPL: You can use it to run quick experiments, particularly in environments without an IDE. But unlike with jshell, you’ll be able to enjoy syntax highlighting (assuming you have access to something more advanced than Notepad), the lack of which renders the REPL unusable to me.

Although, if you’re like me, you have at least three IDE instances running at all times anyways and starting an experiment requires nothing more than Alt-Tabbing to one of them, letting it spew out a
main or test method, and off you go. So for me, experimentation is not an important use case for single-source-file execution.

But if you occasionally write a demo or two, turning each into a single self-contained and executable file will make it easier for your audience. Sharing a single file is simpler than distributing a few of them and your audience can decide whether they want to fire up an IDE or simply throw the file at the JVM. That may make it easier for them to get started – particularly if they’re Java beginners.

One detail that may end up driving that use case are incubator modules and preview features. These are mechanisms that the JDK team can use to let us experiment with not-yet-finalized APIs and syntax. Unlocking these features requires special compiler and JVM commands and putting them into the right places in an IDE can be tedious and fragile. As we have seen above, adding them to
java is much simpler:

1

$java--source12--enable-preview Switch.java

There’s one other way to use single-source-files, though, and it will knock your socks off! (If you’re on Linux or macOS.)

Java Scripts With Shebang

You can add a shebang to source files

We’ve already seen most of the ingredients for scripts above but one is still missing: the shebang. The big news is, you can add it to Java source files and the JVM will ignore it when compiling the source!

Here’s the file
hello-java-scripts:

1

2

3

4

5

6

7

8

#!/opt/jdk-11/bin/java--source11

publicclassHelloJavaScripts{

publicstaticvoidmain(String[]args){

System.out.println("Hello, Java scripts!");

}

}

If the file is executable (with
chmod+xhello-java-scripts), you can run it with
./hello-java-scripts or, if it’s on your
PATH, even with
hello-java-scripts. Arguments following the script’s name are naturally passed on to the
main method.

If you need to add further compiler or JVM flags, you can either put them into the source file after the
--source option or fall back to explicitly launching the JVM as usual:

FYI: Before passing the file to the compiler, the JVM will replace the shebang line with an empty one. This keeps the compiler from barfing while preserving line numbers, which is handy for fixing compile errors.

"Are You Serious?!"

Fair question. As I see it, there are three criticisms of writing scripts with Java:

compilation and execution is a two-step process

Java’s programming model is not conducive to quick results

the JVM is slow to boot

As we’ve discussed at length, the first bullet is no longer true. The third is definitely true, although it would be interesting to see whether we could add Graal native images to the mix. The second bullet feels true, but I’m not sure whether that’s actually still the case.

Echo – The Java Way

The Linux command line has a very simple tool called
echo that simply prints to the terminal whatever you pass to it:

1

2

$echo"Hello, world"

>Hello,world

That’s too boring, even for Java, so let’s try something a little more advanced. In Linux you can "pipe" the result of one command into the next. This doesn’t actually work with
echo (it doesn’t read from
stdin), but let’s do it in our Java variant nonetheless:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

#!/opt/jdk-11/bin/java--source11

// [... imports ...]

publicclassEcho{

publicstaticvoidmain(String[]args)throwsIOException{

varlines=readInput();

lines.forEach(System.out::println);

}

privatestaticStream<String>readInput()throwsIOException{

varreader=newBufferedReader(

newInputStreamReader(System.in));

if(!reader.ready())

returnStream.empty();

else

returnreader.lines();

}

}

As you can see,
readInput() tries to read from
System.in, which is connected to
stdin. The
if checks whether there is any input at all because if not,
reader.lines() will block until that changes – we want to avoid that. (I know, I know, awkward Java in action…)

Putting this into a file
echo and making it executable allows piping text into it – the content of
haiku.txt, for example:

1

2

3

4

5

$cathaiku.txt|./echo

# this is the unaltered content of haiku.txt

>worker bees can leave

>even drones can fly away

>the queen istheir slave

Now, thanks to streams, it’s easy to throw a few more complex operations at the input. Adding command line options for sorting and making lines unique, for example, are one-liners:

Reflection

Because it’s often needed, you should by default add
--source, though. For example when experimenting with preview features:

write an experimental class with a
main method

run it with
java--source12--enable-preview Switch.java

Finally, consider scripting with Java:

write a self-contained source file with a
main method

add a shebang with the path to your Java install as first line, for example
#!/opt/jdk-11/bin/java--source11

name the file any way you want, for example just
script

make it executable with
chmod+xscript

run it with
./script

Remember that you can add all kinds of command line flags either to
java or to the shebang line (in that case, they have to come after
--source). Options following the source file name are passed to
main.