Introduction
As a developer, you sometimes need to write scripts. If your primary expertise is in Java, you might have considered writing scripts in Java instead of Bash or Python. However, if you've tried this, you quickly realized it's not as straightforward as it seems due to Java's verbosity. In this article, I'll explain why scripting with Java now is possible and, more importantly, practical. I'll also introduce a small utility that allows you to write Java scripts that are simple and powerful.
1. It's Possible
Writing scripts in Java has been possible since Java 11. JEP 330: Launch Single-File Source-Code Programs introduced the ability to run single-file Java scripts without requiring explicit compilation. This feature also allowed adding a shebang to the beginning of the file, enabling scripts to be run directly from the command line.
Even though Java 11 made shebangs possible, nobody started writing scripts in Java because it was cumbersome. Just look at this 'Hello World' example:
#!/usr/bin/env java --source 11
public class Script {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
This leads us logically to the next part of the article.
2. Now It's Simpler
Starting with Java 22 (in preview mode), JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) allows us to omit the class declaration and reduce the main
function declaration from public static void main(String[] args)
to simply void main()
. This is a significant improvement.
#!/usr/bin/env java --source 22 --enable-preview
void main() {
System.out.println("Hello World");
}
It will become even simpler in the third iteration of this JEP. Java 23, with JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview), will allow writing print(obj)
, println(obj)
, and readln(obj)
instead of System.out.println(obj)
, thanks to the automatic import of the java.io.IO
package.
3. But It's Still Not Practical
Yeah, the newer versions of Java have reduced much of the verbosity, so writing a 'Hello World' script become easier. The problem becomes apparent when trying to do something more complex. Consider the following example:
This is a mess. 11 lines of code in 6 lines of code in Example 1 (HTTP request)
Let's say you want to make an HTTP request and print the result. Here's how it looks:
#!/usr/bin/env java --source 22 --enable-preview
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
void main() throws Exception {
var url = new URL("https://httpbin.org/get");
var con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
var in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
var content = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
}
in.close();
con.disconnect();
System.out.println(content);
}
main()
, just to make an HTTP request and print the result, + import lines. Using HttpClient
introduced in Java 11 doesn't simplify it much:
#!/usr/bin/env java --source 22 --enable-preview
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
void main() throws Exception {
var httpClient = HttpClient.newHttpClient();
var response = httpClient.send(HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.build(),
HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
main()
, + import lines.
Example 2 (Terminal command)
Here's another example, invoking a terminal command:
#!/usr/bin/env java --source 22 --enable-preview
import java.io.BufferedReader;
import java.io.InputStreamReader;
void main() throws Exception {
var process = Runtime.getRuntime().exec("ls -lah");
var reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitCode = process.waitFor();
System.out.println("Exited with code: " + exitCode);
}
Example 3 (Files IO)
Or, working with files... Have you ever tried to delete a directory with all its contents? You'd struggle with walk()
and nested try-catch
blocks for handling numerous checked exceptions.
All this shows that while Java is powerful, it's not suitable for scripting. But what if I told you it could be?
The problem is that standard mechanisms don't provide default behavior. That's a pity because it simplifies life. Think about why Spring Boot starters became so popular. It includes default behavior. For cases that require detailed configuration, you can always define it, but for most scripting tasks, it's not necessary.
That's why I created a utility that allows you to write Java scripts concisely.
4. Introducing Scripting Utils for Java
Scripting Utils - a single Java file containing several useful wrapper classes with default behavior. Static objects of these wrappers are declared for quick access to functions in this file.
GitHub: https://github.com/AnatoliyKozlov/scripting-utils
To see how convenient this is, let's revisit the examples above using Scripting Utils.
#!/usr/bin/env java --source 22 --enable-preview --class-path /Users/toliyansky/scripting-utils
import static scripting.Utils.*;
void main() {
var response = http.get("https://httpbin.org/get");
log.info(response.body());
}
As we can see, we need to add --class-path /Users/toliyansky/scripting-utils
to the shebang line and a static import import static scripting.Utils.*;
, then we can leverage the full power of Scripting Utils.
Just two lines for an HTTP request and logging the response.
Other examples:
#!/usr/bin/env java --source 22 --enable-preview --class-path /Users/toliyansky/scripting-utils
import static scripting.Utils.*;
void main() {
var terminalResponse = terminal.execute("ls -lah", 100);
log.info(terminalResponse.output);
}
Just two lines for executing a command and logging the response.
Scripting Utils includes wrappers for:
-
http
for HTTP requests -
terminal
for terminal commands -
file
for file operations -
log
for logging -
thread
for threading
Let's look at an example that utilizes the advantages of Scripting Utils.
Imagine our script needs to read a file containing lines in the format <name> <URL>
. For each line, it should make an HTTP request and save the result to a file named <name>
. As a bonus, let's do this in parallel, because we want to leverage Java's strengths over Bash or Python.
#!/usr/bin/env java --source 22 --enable-preview --class-path /Users/toliyansky/scripting-utils
import static scripting.Utils.*;
void main() {
var linksFilePath = "links.txt";
if (!file.exists(linksFilePath)) {
log.error("File not found: " + linksFilePath);
return;
}
file.readAllLines(linksFilePath)
.forEach(line -> thread.run(() -> {
var data = line.split(" ");
var fileName = data[0];
var url = data[1];
var response = http.get(url);
if (response.statusCode() == 200) {
file.rewrite(fileName, response.body());
log.info("File " + fileName + " updated from " + url);
} else {
log.warn("File " + fileName + " was not updated. Http code: " + response.statusCode());
}
}));
thread.sleepSeconds(5);
var terminalResponse = terminal.execute("ls -lah", 100);
log.info(terminalResponse.output);
}
Only 20 lines of code in main()
that do tons of work. It uses the files
, http
, log
, thread
, and terminal
modules.
Can you imagine the monstrosity this would be without Scripting Utils? Or in Bash 😄? Or in Python?
The downside is that Scripting Utils needs to be installed. The project repository has a one-line command that downloads Utils.java
, places it in a specific directory, and compiles it. This only needs to be done once. After that, you can use it in your scripts.
Conclusion
As you can see, Java is actively working towards simplifying the writing of simple programs, including scripts. The numerous JEPs I've mentioned above, and others like JEP 458: Launch Multi-File Source-Code Programs, attest to this. However, even with these simplifications, Java remains not the most convenient language for scripting. With the advent of Scripting Utils, it has become practical as well.
Top comments (7)
A more universal way to use shebang is to specify
/usr/bin/env java
, instead of explicitly specifying the path to the interpreter. Depending on the distribution, it can be located either in/usr/bin
or/bin
or/usr/local/bin
. Specifying env searches the PATH for an interpreter.See stackoverflow.com/questions/437930...
You're right, it makes sense.
I think that I will edit the article and examples in the repository taking this into account.
#!/usr/bin/java
... I learn something new every day, I had no idea that this was a thing :) Thanks!
Great! looks like i am going to retire my python scripts.
Sounds really good. I've looked in GitHub but haven't found the license description. Is it free to use in commercial cases? Do you count to publish it as a free software? Thanks in advance
Hi, yeah, of course, it will be under a free license. I will add it a little bit later.
Nice article.
Just know that tools as jbang or JeKa provide better alternative as you can mention any Maven dependency in the source code (as com.google.guava:guava:33.3.1-jre ) to be automatically included in the classpath.
Moreover, JeKa provides a mean to execute regular methods (not only
public static void main
) since java8. It also contains similar classes for easily script http, file & string manipulation, and so on. You can use it as a tool or a single jar library.You are welcome to try or extend it.
Disclamer: I'm the author of JeKa.