One and Done: Embrace single-file JVM apps for speedy development

One and Done: Embrace single-file JVM apps for speedy development

Introduction

Besides traditional build tools like Maven, Gradle, and SBT, developers might desire quicker code-to-execution cycles, as seen in straightforward scripting situations. Although Java 9 introduced JShell and JEP 330 (Launch Single-File Source-Code Programs), neither option supports downloading and caching dependencies. Alternative solutions include the Groovy language, known for its scripting capabilities and Grapes, which enables the swift addition of Maven repository dependencies to the classpath. Other notable options are Kscript for Kotlin and Ammonite-REPL for Scala.

In the given context, JBang and Scala-CLI offer more than just scripting for Java, Scala, Groovy, or Kotlin. They enable quick starts with minimal setup, download dependencies from Maven, install the required JDK version, and work with single-file JVM apps or single-module projects. They are suitable for serverless lambdas, basic web applications, and utilizing frameworks such as Spring Boot, Quarkus, ZIO, and more.

Installation

Installing both JBang and Scala-CLI is simple. Choose your preferred platform: Windows, MacOS, or Linux, and proceed with the installation using SDKMan, Homebrew, Chocolatey, Scoop, or manually through cURL.

Creating a single-file JVM application

JBang

JBang is a command-line tool that simplifies running Java applications without complex build systems or project setups.

Below is a Hello World Quarkus app, easily compiled, run, and packaged for deployment.

//usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17
//DEPS io.quarkus.platform:quarkus-bom:3.0.1.Final@pom
//DEPS io.quarkus:quarkus-resteasy-reactive
//JAVAC_OPTIONS -parameters
//JAVA_OPTIONS -Djava.util.logging.manager=org.jboss.logmanager.LogManager

import io.quarkus.runtime.Quarkus;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.jboss.logging.Logger;

@Path("/hello")
@ApplicationScoped
public class MyApp {
    @GET
    public String sayHello() {
        return "Hello world!";
    }

    public static void main(String[] args) {
        Quarkus.run(args);
    }
}

For more information on the Javac and Java options used in the example above, please refer to this article.

Running the application

jbang run MyApp.java

The command above runs the app, fetching dependencies and installing the JDK if necessary.

Run in your browser: http://localhost:8080/hello.

Packaging the application

jbang export fatjar MyApp.java

The command packages the app as a JAR.

Scala-CLI

Similar to JBang, Scala-CLI is an excellent command-line tool for education, scripting, prototyping, and experimentation. It is not only suitable for playgrounds but also ideal for single-module projects such as serverless lambdas, web application microservices, and more. Primarily, it is a tool to interact with the Scala language; however, it also supports running Java code, so nothing prevents you from using it for that purpose.

It is worth mentioning that, currently, Scala-CLI is in the process of becoming the official Scala runner.

Here is the equivalent ZIO app from the previous chapter.

//> using jvm "17"
//> using dep "dev.zio::zio-http:3.0.0-RC1"
//> using scala "3.2.2"

import zio.*
import zio.http.*

object MyApp extends ZIOAppDefault:

  val app: App[Any] = 
    Http.collect[Request] {
      case Method.GET -> !! / "hello" => Response.text("Hello world!")
    }

  override val run =
    Server.serve(app).provide(Server.default)

If necessary, you can configure it as a Unix shebang by adding a line that specifies the interpreter for executing the script, e.g.: #!/usr/bin/env scala-cli .

Running the application

scala-cli run MyApp.scala

The command runs the app, fetching dependencies and installing JDK if necessary.

Run in your browser: http://localhost:8080/hello.

Packaging the application

scala-cli --power package MyApp.scala --assembly --preamble=false

The command packages the app as a JAR.

You might have observed some extra flags. The power option enables advanced features; however, it's possible to enable all features and activate them globally.

The preamble setting determines whether to include a bash/bat preamble in the assembly JAR.

Building a Docker image

In the case of JBang, you can easily build a Docker image by creating a simple Dockerfile, an example of which can be found here. With Scala-CLI, it's even simpler; just use the command-line option --docker.

Example:

scala-cli --power package --docker MyApp.scala --docker-image-repository hello-docker

Such a Docker image can be run with: docker run hello-docker:latest .

Packaging the application as a platform-specific executable

If you have the native-image binary installed and GRAALVM_HOME configured, you can build and run the native executable using the command:

jbang --native MyApp.java

In the case of Scala-CLI, it's slightly easier, as there are no prerequisites.

Simply run:

scala-cli --power package MyApp.scala --native-image

The Scala CLI automatically downloads and unpacks a GraalVM distribution.

Of course, just like any other Native Image application, you must provide a reflect-config.json file if necessary.

A simpler alternative for creating platform-specific executables is available with Scala Native, which removes the need for Native Image. However, this option won't be suitable if you're using Java dependencies or Scala ones without native target support. Also, check out the other available targets you can use without complex configurations simply by utilizing command-line options or directives in the file.

Single-module projects

As mentioned it's also a great choice for simple applications like microservices or serverless lambda functions.

JBang did not support multiple source files for quite some time, but now it is listed as one of its features. From its early days, Scala-CLI has offered support for this feature. I will concentrate on this in a separate article. For now, you can explore an example project scaffold from the Tapir project initializer, where Scala-CLI shows as an option alongside SBT.

IDE integration

In the case of Scala-CLI, to initialize IDE integration (IntelliJ or VSCode), run the command scala-cli setup-ide in the project directory. This command generates all the necessary configurations for IDE support, enabling the option to import the project. On the other hand, if one has used basic commands such as: run, compile, or test, there is no need to execute the setup-ide command.

I wish IntelliJ simply offered an option to "import the project as" without requiring the execution of an additional shell command. Perhaps the future will introduce a plugin, similar to the one available for JBang. The extension for VSCode also exists.

Conclusion

The article above gives an overview of unique tools that streamline rapid interaction with JVM-based languages, removing the necessity for preconfiguring a working environment. These solutions excel in educational contexts, swift testing or prototyping, experimentation, or error reproduction associated with particular language versions and dependencies. Moreover, they can be accessible alternatives to sophisticated build tools in production use, especially for simple applications.