Scripting with Java – Improving Approachability

Historically Java has not often been thought of as a scripting language. This belief stems from concerns of Java being a compiled language, being “too verbose”, and having poor startup performance.

Recent changes to the JDK and the Java ecosystem however have begun to address these concerns, which we will explore in this article series. In this first article we will focus on how a change introduced in Java 11 can allow Java to behave like a script language in certain cases. The second and third articles will focus on reducing language formality and improving startup performance respectively.

Why Script with Java?

There might the question of why write scripts with Java at all? There are a couple of answers to this question. The first, if your primary role is a Java developer, writing scripts in Java means you can immediately bring all the experience and knowledge as a Java developer with you when writing a script to simplify or automate a task.

The second is that Java offers an improved feature set, tooling, and structure when compared to scripting languages. While for very simple scripts this might not be relevant, or in the case of structure, a small burden, as a script increases in size, complexity, and particularly importance within your organization, the additional feature set, tooling, and structure that Java offers can be extremely beneficial.

Single Source File Execution

One barrier to using Java as a scripting language is that it’s a compiled language. Before Java can be executed, it needs to be compiled into bytecode. While this is still true, JEP 330, part of JDK 11, allows for single .java source file to be directly compilation and executed in a single command by the java launcher.

Script writing is often a tight development loop of write, execute, write, execute, and the additional step of compiling disrupts this development flow. By combining compile and execution into a single step by invoking the java launcher, this makes Java more approachable as a language for writing scripts.

There are several ways to use this feature, let’s take a look at each of them.

Executing with the java Launcher

The most straightforward way of executing a Java source code file is from the command line with the java launcher. From the directory where the source file is located, the code can be executed simply with:

java [source code file] [arguments]

In a “Hello Name” code example, executing it would look like this:

java HelloJava.java Billy

And produce this:

Hello Billy

Executing using shebang

Running a single Java source file can be further refined with a shebang #!.

At the top of the source file add a #! line that points to the bin directory of your JDK installation and the argument --source version, like below:

#!/path/to/your/bin/java --source 16

public class HelloJava {

    public static void main(String[] args) {
        System.out.println("Hello " + args[0]);
    }

}

Then modify the file to be executable:

chmod +x

And remove the .java file extension

This allows the Java file to be executed like a normal bash script:

./HelloJava Billy

Note: Shebang only works with Unix like systems. For use on a Windows OS either a tool like cygwin will need to be used, or Windows Subsystem for Linux.

Adding source file to PATH

For further convenience a Java source file, configured like the above with a shebang, can be added to a system’s PATH to allow for easy execution from any location on a system. The exact steps for adding a file to your system’s PATH will vary depending upon your OS, so here is a handy link on how to update your PATH for the OS you are using: What are PATH and other environment variables, and how can I set or use them?

Odds and Ends

Most Java developers are very familiar working with larger applications utilizing frameworks, build tools, and packaging applications in JARs or WARs. However when writing and executing Java applications as a script from the command-line there are a couple of things to remember.

Single Source File, Multiple Classes

Typically Java developers define only a single top level class within a source file for readability and consistency purposes, however defining multiple classes within a single source code file is entirely valid and can be done like in the below example:

public class MultipleClassesInSameFile {
    public static void main(String[] args) {

        System.out.println(GenerateMessage.generateMessage());
        System.out.println(AnotherMessage.generateAnotherMessage());
    }


}

class GenerateMessage {
    static String generateMessage() {
        return "Here is one message";
    }
}

class AnotherMessage {
    static String generateAnotherMessage() {
        return "Here is another message";
    }
}

When executed:

java MultipleClassesInSameFile.java 

Produces the following output:

Here is one message
Here is another message

Just keep in mind that while multiple classes can be located in the same Java source file, when compiled each class will have it’s own dedicated class file. So the above code example, when compiled will produce the following files:

MultipleClassesInSameFile.class
GenerateMessage.class
AnotherMessage.class

Note: This also applies to inner classes, a class defined within another class, which will have a class file name like this: OuterClassName$InnerClassName.class.

This will have relevance in the third article in the series when we look at improving performance of scripts written in Java.

Configuring classpath

When referencing classes that are not part of the JDK, you will need to configure the classpath to include the location of these classes. In the below example of using the RandomUtils class from the Apache Common Utils library:

import org.apache.commons.lang3.RandomUtils;

public class GetRandomNunber {

    public static void main(String[] args) {
        System.out.println(RandomUtils.nextInt());
    }

}

While the code is contained within a single file, attempting to execute with the java launcher will cause the following error:

GetRandomNumber.java:1: error: package org.apache.commons.lang3 does not exist
import org.apache.commons.lang3.RandomUtils;
                               ^
GetRandomNumber.java:6: error: cannot find symbol
        System.out.println(RandomUtils.nextInt());
                           ^
  symbol:   variable RandomUtils
  location: class GetRandomNumber
2 errors
error: compilation failed

Adding the commons-lang3-3.12.0.jar jar to the classpath resolves the error:

java -cp /path/to/commons-lang3-3.12.0.jar GetRandomNumber.java 

The classpath, along with other relevant JVM args, can also be defined in the shebang line:

#!/path/to/your/bin/java --source 16 -cp /path/to/commons-lang3-3.12.0.jar

Conclusion

Being able to directly execute a single source code file helps to make Java more approachable for scripting. No build tools, beyond the JDK itself, are needed, and the single step to execute the script helps with a rapid development cycle commonly desired when writing scripts.

In the next article we will take a look at how recent changes have reduced the formality and “verbosity” of the Java language, and how this helps when writing scripts.

The code for for article can be found on my GitHub repo here: https://github.com/wkorando/scripting-with-java/tree/article-i

One thought on “Scripting with Java – Improving Approachability

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s