JBang: Java als Skriptsprache

Den größten Teil seiner Geschichte war Java das falsche Werkzeug für kleine Aufgaben. Wer eine URL abrufen, JSON transformieren oder eine schnelle Datenbank-Bereinigung durchführen wollte, musste für Java ein Verzeichnis anlegen, eine pom.xml oder build.gradle schreiben, Abhängigkeiten deklarieren, kompilieren — und erst dann etwas ausführen. Bis die Zeremonie vorbei war, hätte man dasselbe in Python oder Bash locker dreimal geschrieben.
Genau diese Reibung hat Teams selbst in reinen Java-Shops zu Shell-Skripten und Python-Klebecode getrieben — mit den vorhersehbaren Kosten, den Zugriff auf die Domänenmodelle, Treiber und Bibliotheken zu verlieren, die der Produktionscode ohnehin schon nutzt.
JBang schafft diese Zeremonie ab. Es lässt eine einzelne .java-Datei direkt ausführen, löst Abhängigkeiten über eine Kommentarzeile auf und lädt bei Bedarf sogar das passende JDK herunter. Das Ergebnis ist Java, das sich wie eine Skriptsprache verhält — ohne alles zu verlieren, was Java lohnenswert macht: statische Typisierung, ausgereifte Bibliotheken und echtes Tooling.
Dieser Artikel führt von einem ersten Skript bis zum produktionsreifen Einsatz in CI/CD.
Was JBang tatsächlich leistet
Im Kern erledigt JBang ein paar Dinge, die klassisches Java-Tooling schwer macht:
- Führt einzelne Dateien aus.
jbang script.javakompiliert und führt in einem Schritt aus. Kein Projekt, keine Build-Datei. - Löst Abhängigkeiten inline auf. Ein
//DEPS group:artifact:version-Kommentar zieht Abhängigkeiten direkt aus Maven Central. - Verwaltet das JDK für dich. Ist die vom Skript geforderte Java-Version nicht installiert, lädt JBang sie herunter und nutzt sie. Der Rechner braucht überhaupt kein vorinstalliertes Java.
- Integriert sich in Editoren.
jbang editöffnet das Skript in einem temporären Projekt, sodass die IDE volle Autovervollständigung und Navigation bietet. - Kompiliert zu nativen Binaries. Mit GraalVM wird aus einem Skript eine eigenständige ausführbare Datei mit nahezu sofortigem Start.
Das mentale Modell ist einfach: Die Abhängigkeits- und Laufzeitkonfiguration, die normalerweise in einer Build-Datei liegt, wandert in Kommentardirektiven an den Anfang der Quelldatei.
Das 60-Sekunden-Setup
JBang lässt sich mit jedem Tool installieren, das bereits vorhanden ist:
# SDKMAN (recommended if you manage JDKs already)sdk install jbang# Homebrew (macOS / Linux)brew install jbangdev/tap/jbang# Or the portable install scriptcurl -Ls https://sh.jbang.dev | bash -s - app setup
Und dann das erste Skript aufsetzen:
jbang init hello.javajbang hello.java
Das ist der ganze Kreislauf: erstellen, ausführen. Keine Verzeichnisstruktur zum Auswendiglernen.
Das erste echte Skript: Ein API-Fetcher
Hier ein in sich geschlossenes Skript, das JSON von einer öffentlichen API abruft und ein Feld daraus ausgibt. Es gibt nirgendwo eine Build-Datei — die JDK-Version und die Abhängigkeit stehen direkt im Quellcode.
///usr/bin/env jbang "$0" "$@" ; exit $?//JAVA 21//DEPS com.fasterxml.jackson.core:jackson-databind:2.17.0import 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);}}
Ausführen:
jbang FetchRate.java
Beim ersten Lauf lädt JBang JDK 21 herunter, falls nicht vorhanden, holt Jackson von Maven Central, cached beides, kompiliert und führt aus. Spätere Läufe nutzen den Cache und starten fast sofort.
Die erste Zeile verdient eine Anmerkung. Das Shebang ///usr/bin/env jbang ... erlaubt es, die Datei ausführbar zu machen (chmod +x FetchRate.java) und direkt als ./FetchRate.java auszuführen — die Datei ist das Programm.
Die Direktiven-Übersicht
Alles, was JBang braucht, steckt in Kommentardirektiven am Anfang der Datei:
//JAVA 21— legt die Java-Version fest. JBang beschafft sie bei Bedarf selbst.//JAVA 21+bedeutet "21 oder neuer".//DEPS group:artifact:version— eine Abhängigkeit pro Zeile, Standard-Maven-Koordinaten.//SOURCES OtherFile.java— bindet zusätzliche lokale Quelldateien ein.//FILES resource.txt— bündelt Nicht-Java-Ressourcen.//JAVAC_OPTIONSund//JAVA_OPTIONS— reichen Flags an Compiler oder Laufzeit weiter.//MExtensionund//REPOS— fügen bei Bedarf zusätzliche Maven-Repositories jenseits von Central hinzu.
Das ist die gesamte Konfigurationsoberfläche für die meisten Skripte, und sie bleibt lesbar, weil sie direkt neben dem Code steht, den sie konfiguriert.
Wo JBang seinen Platz verdient
JBang ersetzt nicht das Build-System einer echten Anwendung. Es ist das richtige Werkzeug, wenn ein vollständiges Projekt überdimensioniert wäre:
- DevOps und Automatisierung — Health-Checks, einmalige Migrationen, Report-Generatoren, die echte Datenbanktreiber brauchen.
- Prototyping von Bibliotheken — eine neue Maven-Abhängigkeit in zehn Zeilen ausprobieren, bevor man sie in ein Projekt übernimmt.
- CI/CD-Utilities — Pipeline-Schritte, die mehr brauchen, als Bash sicher ausdrücken kann.
- Verteilbare CLI-Tools — mit Picocli plus nativer Kompilierung als einzelne Binary ausliefern.
Der gemeinsame Nenner: Man will produktionsreife Bibliotheken und Typsicherheit, aber kein Repository, keinen Build und kein Deployment für etwas, das in ein paar Sekunden läuft.
Praxisbeispiel: Ein brüchiges Pipeline-Skript ersetzen
Hier wird der Nutzen konkret. Ein nächtlicher Wartungsjob, der soft-gelöschte Datensätze bereinigt. Die beiden klassischen Optionen sind beide schlecht:
- Ein Bash- oder Python-Skript hat keinen Zugriff auf die bestehenden Java-Domänenmodelle, die Connection-Pool-Konfiguration oder interne Bibliotheken. Es implementiert Logik neu, die bereits existiert, und scheitert auf Arten, die sich schwer testen lassen.
- Ein eigener Java-Microservice oder ein Maven-Modul ist weit zu viel Maschinerie für einen Job, der eine einzige Query ausführt. Er bringt Build-Zeit, Repository-Aufblähung und einen Deployment-Lifecycle für etwas mit, das eigentlich Wegwerfcode ist.
JBang bietet einen dritten Weg: eine einzelne typsichere Datei, die dieselben produktionsreifen Bibliotheken importiert wie die eigenen Services — einen echten Connection Pool, den echten JDBC-Treiber — und saubere Exit-Codes zurückgibt, damit CI korrekt reagieren kann.
///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.5import 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);}@Overridepublic 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.}}}
Zwei Details machen das produktionsreif statt zu einem Spielzeug: Die Query ist parametrisiert, sodass der Tage-Schwellenwert nicht zum Einfallstor für Injection werden kann. Und der Exit-Code hat Aussagekraft — CI behandelt einen Rückgabewert ungleich null als fehlgeschlagenen Schritt, sodass ein kaputter Wartungslauf sichtbar wird, statt stillschweigend durchzulaufen.
Einbindung in GitHub Actions
Die Pipeline braucht kein Java-Setup und keinen Build-Schritt. Die offizielle Action führt das Skript direkt aus:
name: Database Maintenanceon:schedule:- cron: '0 2 * * *' # Nightly at 02:00 UTCjobs:cleanup:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: jbangdev/jbang-action@v0.119.0with:script: .github/scripts/DbMaintenance.javaargs: "--url=jdbc:postgresql://db.internal:5432/prod --days=60"env:DB_USER: ${{ secrets.DB_USER }}DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
Secrets bleiben in der Umgebung, nie auf der Kommandozeile. Das Skript ist zusammen mit dem Workflow versioniert, wird wie jeder andere Code reviewt und lässt sich lokal mit exakt demselben Befehl ausführen, den auch die Pipeline nutzt — genau das, was ein Bash-Skript nicht bieten kann.
Best Practices und Fallstricke für den Produktivbetrieb
JBang skaliert gut mit, aber ein paar Gewohnheiten halten es zuverlässig:
- Java-Version fixieren.
//JAVA 21statt eines offenen Bereichs verwenden, damit jeder Lauf — Laptop, CI, Kollegenrechner — gegen dasselbe JDK kompiliert. - Abhängigkeitsversionen fixieren. Bereiche oder
LATESTvermeiden. Reproduzierbarkeit zählt hier mehr als Bequemlichkeit, weil es keine Lockfile gibt. - Auf den Cache verlassen. JBang cached JDKs, Abhängigkeiten und kompilierte Ausgaben. In kurzlebigen CI-Runnern
~/.jbangzwischen Läufen cachen, um wiederholtes Herunterladen zu vermeiden. - Für Hot Paths nativ kompilieren.
jbang export native yourscript.javaerzeugt eine GraalVM-Binary mit nahezu sofortigem Start — lohnt sich für Tools, die häufig oder in latenzkritischen Schritten aufgerufen werden. - Secrets in der Umgebung halten. Wie im Beispiel oben: Zugangsdaten aus Umgebungsvariablen lesen, niemals hart codieren.
- Wissen, wann man umsteigt. JBang eignet sich ideal für eine einzelne Datei, vielleicht ein paar
//SOURCES-Begleitdateien. Sobald ein Skript echte Struktur, mehrere Module oder eine Testsuite braucht, umsteigen:jbang export mavenrepooder den Code einfach in ein richtiges Maven- oder Gradle-Projekt verschieben. Der Übergang ist schmerzlos, weil es immer schon einfach Java war.
Dieser letzte Punkt ist der Schlüssel zum sinnvollen Einsatz von JBang. Es versucht nicht, das Build-System zu ersetzen — es füllt die Lücke darunter, wo ein vollständiges Projekt zu viel und ein Shell-Skript zu wenig ist.
Fazit
JBang fügt der Java-Sprache nichts hinzu. Was es entfernt, ist die Aktivierungsenergie — die Verzeichnisse, Build-Dateien und Setup-Schritte, die Java für kleine Aufgaben ungeeignet gemacht haben. Indem es Konfiguration in Kommentardirektiven verschiebt und JDK sowie Abhängigkeiten automatisch verwaltet, lassen sich Wegwerfskripte, CI-Utilities und CLI-Tools in derselben Sprache und mit denselben Bibliotheken schreiben wie der Produktionscode.
Der Gewinn ist keine Spielerei. Es ist Konsolidierung: eine Sprache, ein Skillset, vom Fünf-Zeilen-Experiment bis zum nächtlichen Pipeline-Job, ganz ohne die Zeremonie dazwischen.
Ressourcen
- Offizielle Seite: jbang.dev
- Dokumentation: jbang.dev/documentation
- GitHub Action: jbangdev/jbang-action
- Picocli (CLI-Framework): picocli.info
Mohammed Ahmadi
Software Developer
Empfohlen
- Resilience4j in Spring Boot: Von null zu produktionsreifer Fehlertoleranz
- Eine Schritt-für-Schritt-Anleitung aus der Praxis zur Absicherung der Unternehmens-zu-Unternehmens-API-Kommunikation mit OAuth 2.0
- End-to-End-Tests mit Playwright — schnelles Setup & Best Practices
- Visualisierung von SBOMs mit dem Dependency Radar: ein praxisorientierter Ansatz für Dependency Management