Developing Java Applications with Scala-CLI

Learn how to leverage Scala-CLI to build, compile, and run pure Java applications efficiently, using the power of the Scala ecosystem.

Introduction

Scala-CLI is a relatively new tool that simplifies learning Scala and building simple applications. I wrote a detailed blog about the features of Scala-CLI to build applications in Scala.

However, it is also possible to write pure Java applications using this same tool. In this tutorial, let's have a quick look at how we can write pure Java code and use Scala-CLI to build and execute it.

Set-Up

We can install the Scala-CLI application using any of the methods mentioned in the official documentation.

Once installed, verify that it is working fine by using the command:

scala-cli

If it is installed successfully, running the above command starts a Scala REPL as below:

We can exit the REPL for now as we are not going to use the REPL directly.

We can also verify the installation just by checking the version using the command:

scala-cli --version

This prints the Scala-CLI version along with the Scala version as:

For this article, I am going to use VS Code. Please install the vs code extension "Metals" for the Scala-CLI to provide highlight, navigation and auto complete features. Also, there is a chance that other Java extensions might conflict with the metals extension and might cause incorrect errors in the IDE. In my case, I had the vs code suggested extension "Extension Pack for Java" which caused issue for me, so I disabled it.

Basic Usage

Now that the setup is ready, let's write a simple Java class and test the execution. We can create a new directory and use a simple text editor to create a simple Java Hello World application.

I created the directory scala-cli-java to keep the file HelloWorld.java using the content:

 class HelloWorld {
    public static void main(String args[]) {
        System.out.println("Hello, Java World, from Scala-CLI!");
    }
}

We can run this file by using the command:

scala-cli HelloWorld.java

This executes the Java code and prints the message to the console. We don't need to set up JDK to run this code. Scala-CLI automatically downloads the JDK and uses it to run it.

It's also good to run the command in the working directory:

scala-cli setup-ide .

This generates the necessary metadata for the Metals plugin to provide code completion and navigation features. Once it's is run, we can open the directory in editor such as VS Code.

Using Specific Java Version

In the previous code, we never set up or mentioned the JDK version anywhere. ScalaCLI uses the default version(now 17) to build the app.

However, we can very easily specify the required Java version in the code itself, using a special syntax called directives. Directives in Scala-CLI are special lines that start with //> and we can use them to define meta information for the build. The directives must be placed before any Java code including import and package statements.

Using the directive, we can set a specific Java version:

//> using jvm 17

This uses the specified Java version to build the code. Just by specifying the number 17, it uses the latest OpenJDK(Temurin/Adopt) version. We can also specify a different JVM distribution if we want:

//> using jvm zulu:17

This downloads and uses Zulu JDK v17 for compiling and running the code.

The list of JDKs are provided by the tool Coursier, which is used by the Scala-CLI under the hood for managing JVMs.

Using Third-party Dependencies

We can also use the dependencies from MavenCentral in the Scala-CLI app. It uses the directive dep to define the dependency:

//> using dep com.google.code.gson:gson:2.8.9

This adds the gson dependency to the project. We can similarly provide any Maven dependencies using the format GroupId:ArtifactId:Version. Similarly, if we want to use commons-lang3 from apache-commons, we can use the directive as //> using dep org.apache.commons:commons-lang3:3.14.0

Now, we can import the necessary classes and use them.

We can write a more complicated Java application that uses the REST invocation and JSON processing using Scala-CLI. Let's paste the below content into a file called JavaAppWithDep.java:


//> using jvm 17
//> using dep com.google.code.gson:gson:2.8.9
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.util.List;
import com.google.gson.JsonDeserializer;
import com.google.gson.GsonBuilder;

record CountryRecord(String name, String code, String capital, List<String> languages) {}
public class JavaAppWithDep {
    public static void main(String args[]) throws Exception {
        JsonDeserializer<CountryRecord> deserializer = (json, typeOfT, context) -> {
            var jsonObject = json.getAsJsonObject();
            String name = jsonObject.get("name").getAsString();
            String code = jsonObject.get("code").getAsString();
            String capital = jsonObject.get("capital").getAsString();
            List<String> languages = context.deserialize(jsonObject.get("languages"), new TypeToken<List<String>>() {
            }.getType());
            return new CountryRecord(name, code, capital, languages);
        };

        HttpClient httpClient = HttpClient.newHttpClient();
        URI uri = URI.create("https://raw.githubusercontent.com/yadavan88/blog-code-samples/main/countries.json");
        HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(uri)
                .build();
        HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(CountryRecord.class, deserializer)
                .create();
        List<CountryRecord> countries = gson.fromJson(httpResponse.body(), new TypeToken<List<CountryRecord>>() {
        }.getType());
        countries.forEach(System.out::println);
    }
}

This code above downloads a JSON file from a GitHub URL, parses it using GSON, converts it into a record, and prints the output to the console.

We can execute it by using the command as before:

scala-cli JavaAppWithDep.java

When we execute it, we get the output as:

We can see that Scala-CLI even suggested updating the library dependencies based on the availability from Maven Central, which is really awesome :)

Setting Java Options

Similar to dependencies, we can even configure the Java options using directives:

//> using javaProp user=yadu
//> using javaOpt -Xmx2g, -Dkey=myvalue
public class JavaOptions {
    public static void main(String[] args) {
        var key = System.getProperty("key");
        var user = System.getProperty("user");
        System.out.println("Value for key: " + key);
        System.out.println("Value for user: " + user);
    }
}

The directive above sets the Java option for Xmx and passes a custom argument key to the program. Similarly, we can also pass Java properties to the program by using javaProp.

There are more such properties and settings. You may refer to this page for more details.

Packaging

Scala-CLI also lets you create packages of your classes easily. Let's package the Java app we wrote before that downloads the JSON and parses it using GSON:

scala-cli --power package JavaAppWithDep.java -o javaApp --assembly

This creates an executable application with the name as javaApp. The flag --assembly informs Scala-CLI to build an assembly jar(not a library jar). It is important to use the flag --power as this task is considered a power user command.

This creates the jar and an executable script so that we can run the app by using the command:

./javaApp

When I execute this, I get the same output as before:

Instead of creating a wrapper script, if we want only to create an executable jar, we can add the flag --preamble=false:

scala-cli --power package JavaAppWithDep.java -o javaApp.jar --assembly --preamble=false

Here I renamed the output javaApp.jar and also added the passed preamble as false. Now we need to run the jar using the command:

java -jar javaApp.jar

There are more options to build as a docker image, graalvm app, scala-native app, OS-specific formats such as deb, and more. You may find those details here.

Conclusion

In this article, we explored how Scala-CLI can be utilized to build Java applications. Scala-CLI is versatile, capable of building both Scala and Java apps, and brings a rich feature set to Java codebases. With Scala-CLI, you can leverage features such as packaging, switching between different JVM versions, passing Java options, and more. For smaller applications, you can bypass traditional build tools like Maven and create applications with just a single Java file.

However, since it is not Java-centric, features like code completion and navigation might not work as expected.

The sample code used in this article can be accessed on GitHub.