Writing scripts in Java 11 and beyond


Many Java users hate it at times for being too verbose. Many of us have started using other languages like Kotlin or Scala for their terseness and expressiveness. One of the feature that Java programmers like about these modern languages is their ability to do quick experimentation. There are times you do not want to create a project in your IDE, configure build tool, create package structure and then write the code. There is a lot of yak shaving involved before you can automate that one thing that is nagging you. You just want to write a script and move on.

Before JDK 11, Java language did not give you the means to achieve this. Since Java 11, you can write single source file scripts to automate tasks. You do not have to compile the Java file before executing it. You write your Java file and then directly execute it.

This week at work I had to write a script to move Git repositories from one Github organizations to the other. I quickly wrote a bash script that iterated over the list of repos and made a cURL request to Github API to transfer the repo from one org to another. I could have used Kotlin, Scala, Python, and many other languages to write that script as well.

Over the weekend I was watching a talk titled Life After Java 8 by Gil Tene where he talked about JEP 330 – Launch Single-File Source-Code Programs. I was pleasantly surprised that this feature existed since Java 11. The problem in Java ecosystem is that most developers have not moved beyond JDK 8. So, most developers are unaware of the many great features that have come since JDK 8.

The single source file scripts are a good way to learn and apply some of the new features to solve the problems at hand.

Coming back to the problem let’s start by writing a simple Java program that takes three arguments –

  • fromOrg: Github organization to move your repo from
  • repo: Name of the repo
  • toOrg: Github organisation to move your repo to

Create a Java file RepoMover.java and copy and paste the following code.

public class RepoMover {

    public static void main(String[] args) throws Exception{
        if(args.length != 3){
            throw new IllegalArgumentException("Please specify fromOrg, repoName, and toOrg");
        }
        var fromOrg = args[0];
        var repo = args[1];
        var toOrg = args[2];
          System.out.println(String.format("Moving %s/%s to %s/%s", fromOrg, repo, toOrg, repo));
    }
}

The code shown above uses JDK 10 local variable syntax. We have removed types of the variables – fromOrg, repo, and toOrg. Compiler will automatically figure out the left hand side type by looking at the right hand.

I hate the fact that in 2020 Java still does not have a good String interpolation story.

To run the above program we need to have JDK 11 installed on our machine.

I use Jabba, a Java Version Manager inspired by nvm (Node.js) to install and manage different JDKs on my machine. You can refer to installation instructions on the Jabba Github repo to install it.

Once you installed you can install JDK 11 by running following command. I am using Zulu JDK 11 distribution.

$ jabba install zulu@1.11.0-9

You don’t have to type $. $ signifies command prompt.

Once installed you can enable it by executing following command.

$ jabba use zulu@1.11.0-9

After this, if you run java -version you will get following output.

openjdk version "11.0.9" 2020-10-20 LTS
OpenJDK Runtime Environment Zulu11.43+21-CA (build 11.0.9+11-LTS)
OpenJDK 64-Bit Server VM Zulu11.43+21-CA (build 11.0.9+11-LTS, mixed mode)

Let’s run our program now.

$ java RepoMover.java

You will be greeted with the following error

Exception in thread "main" java.lang.IllegalArgumentException: You should specify fromOrg, repoName, and toOrg
        at RepoMover.main(RepoMover.java:5)

We need to pass the arguments to our program.

$ java RepoMover.java org1 repo1 org2

The output as expected is shown below.

Moving org1/repo1 to org2/repo1

Now, we will use the Github REST API to transfer a repo from one organization to the another.

As mentioned in the documentation we need to make a POST call to the /repos/{owner}/{repo}/transfer with the JSON payload as shown below.

{"new_owner":"new_owner"}

Java 11 added a new and better HTTP client API that we can use to make the POST request. You can learn more about it here.

It is similar to OkHttp API and use fluent builder API. It is pleasant to use.

You start by creating the client.

var client = HttpClient.newHttpClient();

Next, you create the request object as shown below.

var request = HttpRequest.newBuilder()
            .uri(URI.create(String.format("https://api.github.com/repos/%s/%s/transfer", fromOrg, repo)))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer " + ghAccessToken)
            .POST(HttpRequest.BodyPublishers.ofString(String.format("{\"new_owner\": \"%s\"}", toOrg)))
            .build();

And finally you make the request.

var response = client.send(request, HttpResponse.BodyHandlers.ofString());

The full code with import statements is shown below.

import java.net.URI;
import java.net.http.*;

public class RepoMover {

    public static void main(String[] args) throws Exception{
        if(args.length != 3){
            throw new IllegalArgumentException("Please specify fromOrg, repoName, and toOrg");
        }
        var fromOrg = args[0];
        var repo = args[1];
        var toOrg = args[2];
        var ghAccessToken = System.getenv("GITHUB_ACCESS_TOKEN");
        System.out.println(String.format("Moving %s/%s to %s/%s", fromOrg, repo, toOrg, repo));
        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder()
            .uri(URI.create(String.format("https://api.github.com/repos/%s/%s/transfer", fromOrg, repo)))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer " + ghAccessToken)
            .POST(HttpRequest.BodyPublishers.ofString(String.format("{\"new_owner\": \"%s\"}", toOrg)))
            .build();
        var response = client.send(request, HttpResponse.BodyHandlers.ofString());
        var statusCode = response.statusCode();
        System.out.println("Status code: " + statusCode);
        if(statusCode != 202){
            System.err.println("Failed to transfer repo. Status code is "+ statusCode);
            System.out.println("Response Body: " + response.body());
        }

    }
}

To use the Github API you have to create a personal access token and provide that in the header. You can read more about it in the documentation.

Now, if you execute the code it will transfer the repo from one org to the other.

$ java RepoMover.java

One thing that I don’t like in the above solution is that we have to name our file RepoMover.java and that I can’t execute it like a normal script.

I want my script to be called repo-mover and I want to execute it like ./repo-mover org1 repo1 org2

Luckily we can do that using shebang.

Rename your file to repo-mover and then add the shebang as shown below.

#!/Users/shekhargulati/.jabba/jdk/zulu@1.11.0-9/Contents/Home/bin/java --source 11

import java.net.URI;
import java.net.http.*;

public class RepoMover {
// Rest of the code is same
}

It is mandatory to specify the --source 11. Without it, it will not work.

Now, you will be able to run it like an actual script.

./repo-mover org1 repo1 org2

That’s it for this blog.

2 thoughts on “Writing scripts in Java 11 and beyond”

  1. Hey, thanks for the write up! So instead of the JVM compiling to bytecode prior to execution here the JVM acts as an interpreter, running the code directly, like a Python script? Cheers 🙂

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 )

Twitter picture

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

Facebook photo

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

Connecting to %s

%d bloggers like this: