lama-space logolama‑space
← Back to Tech Blog
Tech Blog

JBang: Turning Java Into a Scripting Language

JBang: Turning Java Into a Scripting Language

For most of its life, Java has been the wrong tool for small jobs. If you needed to fetch a URL, transform some JSON, or run a quick database cleanup, reaching for Java meant creating a directory, writing a pom.xml or build.gradle, declaring dependencies, compiling, and only then running something. By the time the ceremony was over, you could have written the same thing in Python or Bash three times.

That friction is exactly what pushed teams toward shell scripts and Python glue code even in Java shops — with the predictable cost of losing access to the domain models, drivers, and libraries their production code already used.

JBang removes the ceremony. It lets you run a single .java file directly, resolve dependencies from a comment line, and even download the right JDK on demand. The result is Java that behaves like a scripting language while keeping everything that makes Java worth using: static typing, mature libraries, and real tooling.

This article walks from a first script up to production-grade CI/CD usage.

What JBang Actually Does

At its core, JBang does a few things that traditional Java tooling makes hard:

  • Runs single files. jbang script.java compiles and runs in one step. No project, no build file.
  • Resolves dependencies inline. A //DEPS group:artifact:version comment pulls dependencies straight from Maven Central.
  • Manages the JDK for you. If the Java version a script asks for isn't installed, JBang downloads and uses it. Your machine doesn't need Java pre-installed at all.
  • Integrates with editors. jbang edit opens the script in a temporary project so your IDE gives you full autocomplete and navigation.
  • Compiles to native binaries. With GraalVM, a script can become a standalone executable with near-instant startup.

The mental model is simple: the dependency and runtime configuration that normally lives in a build file moves into comment directives at the top of the source file.

The 60-Second Setup

Install JBang with whichever tool you already have:

# SDKMAN (recommended if you manage JDKs already)
sdk install jbang
# Homebrew (macOS / Linux)
brew install jbangdev/tap/jbang
# Or the portable install script
curl -Ls https://sh.jbang.dev | bash -s - app setup

Then scaffold your first script:

jbang init hello.java
jbang hello.java

That's the whole loop: create, run. No directory structure to memorize.

Your First Real Script: An API Fetcher

Here's a self-contained script that pulls JSON from a public API and prints a field from it. Notice there is no build file anywhere — the JDK version and the dependency are declared right in the source.

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 21
//DEPS com.fasterxml.jackson.core:jackson-databind:2.17.0
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class FetchRate {
public static void main(String... args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.coinbase.com/v2/prices/BTC-USD/spot"))
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(response.body());
String amount = root.path("data").path("amount").asText();
System.out.println("Current Bitcoin price (USD): $" + amount);
}
}

Run it:

jbang FetchRate.java

On first run, JBang downloads JDK 21 if you don't have it, fetches Jackson from Maven Central, caches both, compiles, and runs. Subsequent runs use the cache and start almost immediately.

The first line deserves a note. That ///usr/bin/env jbang ... shebang lets you mark the file executable (chmod +x FetchRate.java) and run it directly as ./FetchRate.java — the file is the program.

The Directive Cheat Sheet

Everything JBang needs lives in comment directives at the top of the file:

  • //JAVA 21 — pin the Java version. JBang provisions it if missing. //JAVA 21+ means "21 or newer."
  • //DEPS group:artifact:version — one dependency per line, standard Maven coordinates.
  • //SOURCES OtherFile.java — pull in additional local source files.
  • //FILES resource.txt — bundle non-Java resources.
  • //JAVAC_OPTIONS and //JAVA_OPTIONS — pass flags to the compiler or runtime.
  • //MExtension and //REPOS — add non-Central Maven repositories when you need them.

This is the whole configuration surface for most scripts, and it stays readable because it sits next to the code it configures.

Where JBang Earns Its Place

JBang is not a replacement for your build system on a real application. It's the right tool when a full project would be overkill:

  • DevOps and automation — health checks, one-off migrations, report generators that need real database drivers.
  • Prototyping libraries — try a new Maven dependency in ten lines before committing to it in a project.
  • CI/CD utilities — pipeline steps that need more than Bash can safely express.
  • Distributable CLI tools — with Picocli plus native compilation, ship a single binary.

The common thread: you want production-grade libraries and type safety, but you don't want a repository, a build, or a deployment for something that runs in a few seconds.

Professional Use-Case: Replacing a Brittle Pipeline Script

Here's where the value becomes concrete. Consider a nightly maintenance job that purges soft-deleted records. The two traditional options are both bad:

  • A Bash or Python script has no access to your existing Java domain models, connection-pool configuration, or internal libraries. It reimplements logic that already exists, and it fails in ways that are hard to test.
  • A dedicated Java microservice or Maven module is far too much machinery for a job that runs one query. It adds build time, repository bloat, and a deployment lifecycle for something disposable.

JBang gives you a third path: a single type-safe file that imports the same production-grade libraries your services use — a real connection pool, the real JDBC driver — and returns proper exit codes so CI can react correctly.

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 21
//DEPS com.zaxxer:HikariCP:5.1.0
//DEPS org.postgresql:postgresql:42.7.2
//DEPS info.picocli:picocli:4.7.5
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.concurrent.Callable;
@Command(name = "db-cleanup", mixinStandardHelpOptions = true, version = "1.0",
description = "Purges soft-deleted records older than a threshold in days.")
public class DbMaintenance implements Callable<Integer> {
@Option(names = {"-d", "--days"}, description = "Age threshold in days", defaultValue = "30")
private int days;
@Option(names = {"--url"}, description = "Database JDBC URL", required = true)
private String dbUrl;
public static void main(String... args) {
int exitCode = new CommandLine(new DbMaintenance()).execute(args);
System.exit(exitCode);
}
@Override
public Integer call() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(dbUrl);
config.setUsername(System.getenv("DB_USER"));
config.setPassword(System.getenv("DB_PASSWORD"));
config.setMaximumPoolSize(2);
String sql = "DELETE FROM audit_logs WHERE deleted_at < NOW() - (INTERVAL '1 day' * ?)";
try (HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, days);
int rowsDeleted = stmt.executeUpdate();
System.out.printf("SUCCESS: purged %d stale audit records older than %d days.%n",
rowsDeleted, days);
return 0;
} catch (Exception e) {
System.err.println("FAILURE: maintenance job failed: " + e.getMessage());
return 1; // Non-zero exit code fails the CI step immediately.
}
}
}

Two details make this production-appropriate rather than a toy. The query is parameterized, so the day threshold can't turn into an injection vector. And the exit code is meaningful — CI treats a non-zero return as a failed step, so a broken maintenance run surfaces instead of passing silently.

Wiring It Into GitHub Actions

The pipeline needs no Java setup and no build step. The official action runs the script directly:

name: Database Maintenance
on:
schedule:
- cron: '0 2 * * *' # Nightly at 02:00 UTC
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jbangdev/jbang-action@v0.119.0
with:
script: .github/scripts/DbMaintenance.java
args: "--url=jdbc:postgresql://db.internal:5432/prod --days=60"
env:
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}

Secrets stay in the environment, never on the command line. The script is versioned alongside the workflow, reviewed like any other code, and easy to run locally with the exact same command the pipeline uses — which is precisely what a Bash script can't offer.

Production Best Practices and Caveats

JBang scales up gracefully, but a few habits keep it reliable:

  • Pin your Java version. Use //JAVA 21 rather than an open range so every run — laptop, CI, teammate's machine — compiles against the same JDK.
  • Pin dependency versions. Avoid ranges or LATEST. Reproducibility matters more here than convenience, because there's no lockfile.
  • Lean on the cache. JBang caches JDKs, dependencies, and compiled output. In ephemeral CI runners, cache ~/.jbang between runs to avoid re-downloading on every job.
  • Compile to native for hot paths. jbang export native yourscript.java produces a GraalVM binary with near-instant startup — worth it for tools invoked frequently or in latency-sensitive steps.
  • Keep secrets in the environment. As in the example above, read credentials from environment variables, never hard-code them.
  • Know when to graduate. JBang is ideal for a single file, maybe a few //SOURCES companions. Once a script grows real structure, multiple modules, or a test suite, convert it: jbang export mavenrepo or simply move the code into a proper Maven or Gradle project. The transition is painless because it was always just Java.

That last point is the key to using JBang well. It isn't trying to replace your build system — it's filling the gap below it, where a full project is too much and a shell script is too little.

Conclusion

JBang doesn't add anything to the Java language. What it removes is the activation energy — the directories, build files, and setup steps that made Java a poor fit for small tasks. By moving configuration into comment directives and handling the JDK and dependencies automatically, it lets you write throwaway scripts, CI utilities, and CLI tools in the same language and with the same libraries as your production code.

The payoff is not novelty. It's consolidation: one language, one skill set, from a five-line experiment to a nightly pipeline job, without the ceremony in between.

Resources

Share “JBang: Turning Java Into a Scripting Language”
Mohammed Ahmadi

Mohammed Ahmadi

Software Developer