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”