Skip to content

Latest commit

 

History

History
614 lines (474 loc) · 24.4 KB

javaspektrum-jbang.adoc

File metadata and controls

614 lines (474 loc) · 24.4 KB

Java-Script - Java QuellCode ausführen mit JBang

Mit [jbang] kommt richtiges Scripting zu Java, statt traditioneller Bash-Skripte können jetzt Automatisierungsaufgaben mit den vollen Möglichkeiten von Java inklusive Zusatzbibliotheken und Parallelisierung durchgeführt werden.

Andere Sprachen wie Python oder Javascript erlauben es, mit wenig Aufwand und Zeremonie, Skripte für den täglichen Bedarf zu schreiben. Mit Java war das bisher relativ aufwändig.

In diversen Artikeln hatte ich ja schon angemerkt dass dank [JEP330] seit Java 11 Quellcode, direkt mit dem java Kommando ausgeführt werden kann (Listing 1). In Unix Systemen kann java sogar als Shell am Anfang der Datei angegeben werden, #!/usr/bin/java --source 11, dann darf allerdings die Dateiendung nicht auf .java enden.

Listing 1: Beispiel für ein Datei-Listing in Java
// java Ls.java .
import java.io.*;

public class Ls {
    public static void main(String...args) {
        for (var f : new File(args[0]).listFiles())
            System.out.println(f);
    }
}

Leider hat diese Fähigkeit noch einige Einschränkungen, die größte ist die sehr umständliche Bereitstellung von Zusatzbibliotheken (z.B. ein JDBC Treiber). Diese müssen extra heruntergeladen und im Classpath zur Verfügung gestellt werden.

In der Vergangenheit habe ich meist in solchen Fällen auf groovy und die @Grab Annotation zurückgegriffen, die Bibliotheken automatisch (transitiv) herunterlädt und einbindet. Dafür muss dann aber wieder Groovy installiert sein, und Groovy Syntax genutzt werden.

Skripte mit Java zu schreiben hat eine Menge Vorteile, neben der IDE Unterstützung hat man dank Java Streams, Java (N)IO und JDBC ein mächtiges Werkzeug in der Hand, um auch komplexere Aufgaben zu bewerkstelligen.

Entstehung von JBang

Dem JBang Entwickler, Max Rydahl Andersen (Redhat Schweiz) waren diese und andere Probleme ein Dorn im Auge, so dass er beschloss ein Tool zu veröffentlichen (Januar 2020 [jbang-historie]), dass deutlich über JEP330 hinausgeht und umfassendes Java-Scripting erlaubt.

Er wollte damit das Lernen, Ausführen, Skripting und Packaging von kleinen Java Anwendungen und Skripten so einfach wie möglich machen.

Der Name JBang ist eine Anspielung auf das "Hash-Bang" (#!) das in Shellskripten als Präfix genutzt wird, um einen Interpreter anzugeben.

Damit kann statt Python oder Go für wiederkehrende Skripte einfach Java genutzt werden zum Beispiel auch für Plugins für Tools wie git oder kubectl.

Zum Beispiel auch in CI Tools wie GitHub Actions, dafür steht jbang-action zur Verfügung.

Eine praktische Anwendung von JBang ist auch das kompakte Melden von Fehlern, da in einer einzigen Datei sowohl die Fehlerreproduktion als auch notwendige Abhängigkeiten gekapselt werden können.

Andere Möglichkeiten bieten sich in kompakten UI Anwendungen mit Java-FX (Listing 2) mittels Gluon oder Interaktion mit Browser (Selenium) oder Desktop (Java Desktop API).

Das Tool besteht vor allem aus der jbang Kommandozeilenanwendung die relevanten Funktionen in sich vereint. Zusatzfunktionen wie der App-Store sind optional.

Listing 2 - Ausführen eines JFX-Tile Demos
`jbang https://git.io/JKPVb`

Viele Beispiele für solche Anwendungen sind in [jbang-examples] verfügbar.

Erstes JBang Beispiel

JBang funktioniert ab Java 8 und steht unter der MIT Lizenz in Version 0.81.x zur Verfügung.

Hier ein erstes jbang Beispiel zum Parsen von HTML Seiten mit JSoup als Bibliothek (Listing 3). Dabei wird die übergebene URL geöffnet, und Elemente (z.B. Anchors) anhand des angeforderten Selektors gefunden und ausgegeben.

Die Abhängigkeit zu JSoup wird als DEPS Kommentar oben in der Datei vermerkt und beim ersten Ausführen heruntergeladen.

Mittels eines //JAVA 17 Kommentars können wir sogar die gewünschte Java Version angeben und mit //JAVA[C]_OPTIONS JVM Optionen konfigurieren. Alternativ kann --java 17 als Kommandozeilenparameter beim Aufruf genutzt werden.

Listing 3: Html Parser mit JSoup
//DEPS org.jsoup:jsoup:1.14.3
//JAVA 17+
//JAVA_OPTIONS --enable-preview
import static java.lang.System.*;
import org.jsoup.*;

public class Html {
    public static void main(String...args) throws Exception {
        var doc = Jsoup.connect(args[0]).get();
        out.println(doc.title());
        doc.select(args[1]).stream()
        .map(e -> new Anchor(e.text(), e.attr("href")))
        .forEach(out::println);
    }
    record Anchor(String text, String href) {}
}
Listing 4: Ausführung des Html Parsers für Wikipedia
jbang Html.java "https://en.wikipedia.org/" "#mp-itn b a"
[jbang] Building jar...
Wikipedia, the free encyclopedia
Expo 2020 -> /wiki/Expo_2020
2021 Ryder Cup -> /wiki/2021_Ryder_Cup
74th Tony Awards -> /wiki/74th_Tony_Awards
2021 German federal election -> /wiki/2021_German_federal_election
Detention of Michael Spavor and Michael Kovrig -> /wiki/Detention_of_Michael_Spavor_and_Michael_Kovrig
Meng Wanzhou -> /wiki/Meng_Wanzhou
Portal:Current events -> /wiki/Portal:Current_events
Deaths in 2021 -> /wiki/Deaths_in_2021
Wikipedia:In the news/Candidates -> /wiki/Wikipedia:In_the_news/Candidates

Installation

JBang kann am einfachsten über SDKMan installiert werden, ein einfaches sdk install jbang reicht. Auch bei homebrew, scoop, chocolatey und weiteren Paketmanagern ist es verfügbar und auch einfach in jeder Shell (siehe Listing 5).

Dankenwerterweise wird auf Rechnern auf denen noch kein Java verfügbar ist, dieses bei der Benutzung von JBang einmalig heruntergeladen und installiert.

Listing 5: Installation von jbang mittels der Shell
# Powershell:
iex "& { $(iwr https://ps.jbang.dev) } app setup"

# GitBash/cygwin/mingwin/WSL:
curl -Ls https://sh.jbang.dev | bash -s - app setup

Entwickeln mit JBang

JBang merkt man an, dass sein Entwickler ein Tool für den eigenen praktischen Einsatz geschaffen hat. Es ist wirklich überlegt und nutzerfreundlich konzipiert.

Ein neues JBang Skript kann mittels jbang init Query.java initialisiert werden, damit bekommt man den relevanten Rumpf geliefert (Listing 6).

Listing 6 - Hello World
///usr/bin/env jbang "$0" "$@" ; exit $?
// //DEPS <dependency1> <dependency2>

import static java.lang.System.*;

public class Query {
    public static void main(String... args) {
        out.println("Hello World");
    }
}

Mittels jbang edit Query.java generiert JBang ein temporäres Gradle Projekt mit den relevanten Dependencies, so dass eine IDE sie auch korrekt auflösen kann und öffnet das Projekt in der IDE (IntelliJ Idea, Eclipse, VS Code) (Listing 7). Nach einem Update der Abhängigkeiten kann das unsichtbare Projekt mittels edit wieder aktualisiert werden.

Die aktuelle Skriptdatei wird mittels eines symbolischen Links eingebunden, so dass sie im aktuellen Verzeichnis verbleibt ohne Projektsetup.

Das ist auch eine der schönen Eigenschaften von JBang, nur die aktuelle Skriptdatei ist relevant, alle Infrastruktur verschwindet aus dem Sichtfeld.

Listing 7: Initialisierung und Bearbeitung des Query.java Quellcodes
---
jbang edit --open=code Query.java
[jbang] Running `sh -c code /Users/mh/.jbang/cache/projects/Query.java_jbang_af9d1b3ed59c667238ae61b13a5c64c0d7e4486ac0f3f16fe190e844272620f4/Query`
/Users/mh/.jbang/cache/projects/Query.java_jbang_af9d1b3ed59c667238ae61b13a5c64c0d7e4486ac0f3f16fe190e844272620f4/Query
---

In diesem Beispiel (Listing 8) fragen wir eine Relationale Datenbank ab und stellen das Ergebnis mit einer Ascii-Art Tabelle dar.

Listing 8 - Datenbankabfrage mittels JDBC
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.postgresql:postgresql:42.2.24
//DEPS com.github.freva:ascii-table:1.2.0

import static java.lang.System.*;
import java.sql.*;
import java.util.*;
import com.github.freva.asciitable.*;

public class Query {

    public static void main(String... args) throws Exception {
        // JDBC URL aus Umgebungsvariable
        try (var con=DriverManager.getConnection(System.getenv("JDBC_URL"));
             var stmt=con.createStatement();
             // Alle Parameter zu AbfrageString
             var rs=stmt.executeQuery(String.join(" ",args))) {

                var meta=rs.getMetaData();
                // Spaltennamen als Feld
                var cols=new String[meta.getColumnCount()];
                for (int c=1;c<=cols.length;c++)
                    cols[c-1]=meta.getColumnName(c);
                int row=0;
                // Werte als zweidimensionales Feld (max 100 Zeilen)
                String[][] rows=new String[100][];
                while (rs.next() || row>=rows.length) {
                    rows[row]=new String[cols.length];
                    for (int c=1;c<=cols.length;c++)
                        rows[row][c-1]=rs.getString(c);
                    row++;
                }
                out.println(AsciiTable.getTable(cols,
                            Arrays.copyOf(rows,row)));
             }
    }
}
Listing 9: Ausgabe der Abfrage einer Postgres Northwind Datenbank
export JDBC_URL="jdbc:postgresql://db-examples.cmlvojdj5cci.us-east-1.rds.amazonaws.com/northwind?user=n4examples&password=36gdOVABr3Ex"
jbang Query.java "select contact_name, city, country from customers limit 5"
[jbang] Building jar...
+--------------------+-------------+---------+
| contact_name       | city        | country |
+--------------------+-------------+---------+
|       Maria Anders |      Berlin | Germany |
+--------------------+-------------+---------+
|       Ana Trujillo | México D.F. |  Mexico |
+--------------------+-------------+---------+
|     Antonio Moreno | México D.F. |  Mexico |
+--------------------+-------------+---------+
|       Thomas Hardy |      London |      UK |
+--------------------+-------------+---------+
| Christina Berglund |       Luleå |  Sweden |
+--------------------+-------------+---------+

JBang bringt auch schon einige Quelltext-Vorlagen für spezifische Anwendungen mit, diese können mit --template= oder -t angewandt werden.

Hier ein Beispiel für Kommandozeilenanwendungen mit PicoCLI (Listing 10).

Listing 10 - PicoCLI Beispiel
// jbang init -t cli Cli.java

///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS info.picocli:picocli:4.5.0

import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;

import java.util.concurrent.Callable;

@Command(name = "Cli", mixinStandardHelpOptions = true,
        version = "Cli 0.1", description = "Cli made with jbang")
class Cli implements Callable<Integer> {

    @Parameters(index = "0", description = "The greeting to print",
                defaultValue = "World!")
    private String greeting;

    public static void main(String... args) {
        int exitCode = new CommandLine(new Cli()).execute(args);
        System.exit(exitCode);
    }

    @Override
    public Integer call() throws Exception {
        System.out.println("Hello " + greeting);
        return 0;
    }
}
// jbang Cli.java Jbang!
// Hello Jbang!

Weitere existierende Vorlagen sind über jbang template list verfügbar:

  • agent = Agent template

  • cli = CLI template

  • hello = Basic Hello World template

  • hello.kt = Basic kotlin Hello World template

  • qcli = Quarkus CLI template

  • qmetrics = Quarkus Metrics template

  • qrest = Quarkus REST template

Man kann aber auch leicht Vorlagen für das eigene Team oder Projekt hinzufügen (Listing 11).

Listing 11 - Template Hinzufüugen.
jbang template add --name myapp-starter myapp.java logo-banner.jpg app.properties

Mehrere Java-Dateien und Resourcen können in JBang auch genutzt werden. Wie gehabt wird per Kommentar angegeben, wo diese zu finden sind, bzw. wohin Dateien im aktuellen Verzeichnis innerhalb der Jar-Datei abgelegt werden sollen.

  • Quellcode: SOURCES */.JAVA

  • Resourcen: FILES META-INF/resources/index.html=index.html

Hier ein Quarkus Http Service Beispiel in Listing 12.

Listing 12 - Quarkus Http Service
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS io.quarkus:quarkus-resteasy:1.8.1.Final
//SOURCES **/*.java
//FILES META-INF/resources/index.html=index.html

import static java.lang.System.*;
import io.quarkus.runtime.*;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.*;

@Path("/hello")
@ApplicationScoped
public class quarkus {

    @GET
    public String hello() {
        return "Hello Quarkus";
    }
}

Da Max hauptberuflich an Quarkus arbeitet, gibt es dafür einige dedizierte Konfigurationsoptionen und Templates für Microservices und andere Anwendungen.

Ausführung

Da JBang auch URLs als Quelle für die Skripte unterstützt, kann ich mein HTML-Parser Skript auch zum Beispiel von einem GitHub Gist laden: jbang https://git.io/JasGS https://neo4j.com/developer/ a.page.

Damit kann ich meinen Code ohne Kompilierung/Deployment transparent bereitstellen. Es wird aber von JBang nachgefragt, ob der URL vertraut werden soll.

JBang Skripte können auch als ganz normale Shell-skripte ausgeführt werden, dann müssen sie in der ersten Zeile einen Kommentar, enthalten der jbang als Interpreter ausweist: ///usr/bin/env jbang "$0" "$@" ; exit $?

Jbang kann auch Jar Dateien direkt ausführen und Skripte für jshell mit dem Suffix .jsh, oder auch Maven Koordinaten, die auf ein Jar mit einer geeigneten Main-Klasse zeigen.

Häufig wiederkehrende Aufrufe können als Aliase im "Katalog" abgelegt werden, der auch zentral, z.B. auf GitHub veröffentlicht werden kann (Listing 13).

Listing 13 - Ausführen und Aliase, hier mit Neo4j’s Cypher-Shell
# Ausführen von Maven Koordinaten
jbang -m=org.neo4j.shell.Main org.neo4j:cypher-shell:4.3.6

# Alias hinzufügen
jbang alias add --name=cypher \
 -m=org.neo4j.shell.Main org.neo4j:cypher-shell:4.3.6

# Alias ausführen
jbang cypher -a neo4j+s://demo.neo4jlabs.com \
-d movies -u movies -p movies \
"MATCH () RETURN count(*);"

Ein sehr schönes Beispiel von Max ist ein vorpaketierter Minecraft Server [jbang-minecraft] der die ganze Komplexität auf ein jbang sponge@jbangdev/jbang-minecraft reduziert.

Build und Packaging

JBang hat sich aus der initialen Idee einer Ausführungsumgebung weiterentwickelt, und bietet jetzt auch andere Dienste an.

Zum Beispiel können damit kleine Projekte gebaut und paketiert werden, sogar komplette Maven Deployments oder Erzeugung von Docker Container Images.

Kommando Beschreibung

jbang build Ls.java

Kompiliert die Datei

jbang run Ls.java .

Ausführung

jbang --native Ls.java .

Native Ausführung mit GraalVM

jbang export --native Ls.java

Natives Binary bereitstellen

jbang export [local|portable|mavenrepo] Ls.java

Stellt Binary [mit Abhängigkeiten] bereit

jbang app install [--name html] https://git.io/JasGS

Lokale Installation des Codes. Ausführung mittels ./html …​

Andere Anwendungen

Testcontainers

Mit dem Testcontainers Projekt können mittels einer fluent Java API Docker Container konfiguriert, gestarted und verwaltet werden. Sowohl generische Container für beliebige Anwendungen als auch schon vorkonfigurierte Container für Datenbanken, Webserver usw. sind vorhanden.

Dank JBang kann man jetzt Container und Java-Tests in einer Datei kapseln und diese zum Beispiel zur Reproduktion von Fehlern oder zur Demonstration von Features.

Hier ein Beispiel (Listing 14) mit dem Neo4j Testcontainer, das eine Neo4j Instanz als Docker Container startet und dann mit dem Java Treiber eine Verbindung öffnet und eine Abfrage ausführt.

Listing 14 - TestContainer starten und nutzen
///usr/bin/env jbang "$0" "$@" ; exit $?
//DEPS org.testcontainers:neo4j:1.15.3
//DEPS org.neo4j.driver:neo4j-java-driver:4.3.4

import static java.lang.System.*;
import java.util.*;
import org.testcontainers.containers.*;
import org.neo4j.driver.*;

public class Neo4jTest {

    private static Neo4jContainer startContainer() {
        var container = new Neo4jContainer()
        .withAdminPassword(null);
        container.start();
        return container;
    }

    private static Value queryDatabase(String boltUrl) {
        try (
            Driver driver = GraphDatabase.driver(boltUrl, AuthTokens.none());
            Session session = driver.session()) {
            return session.run("RETURN 1", Map.of()).single().get(0);
        }
    }
    public static void main(String... args) {
        var container = startContainer();
        var result = queryDatabase(container.getBoltUrl());
        out.println(result);
        assert result.asLong() == 1L;
        container.stop();
    }
}

GitHub Actions

Für die Automatisierung von GitHub Actions Continous Integration Aufgaben hat Max Andersen eine [jbang-github-action] bereitgestellt (Listing 15). Das war auch eine der ursprünglichen Treiber für die Entwicklung von JBang - GitHub Actions Skripte in Java zu implementieren, nicht nur in Javascript oder Python.

Listing 15: GitHub Action mittels JBang
on: [push]

jobs:
    jbang:
    runs-on: ubuntu-latest
    name: A job to run jbang
    steps:
    - name: checkout
      uses: actions/checkout@v1
    - uses: actions/cache@v1
      with:
        path: /root/.jbang
        key: ${{ runner.os }}-jbang-${{ hashFiles('*.java') }}
        restore-keys: |
            ${{ runner.os }}-jbang-
    - name: jbang
      uses: jbangdev/jbang-action@v0.81.0
      with:
        script: createissue.java
        scriptargs: "my world"
      env:
        JBANG_REPO: /root/.jbang/repository
        GITHUB_TOKEN: ${{ secrets.ISSUE_GITHUB_TOKEN }}

App Store

Viele Räder will man nicht immer wieder neu erfinden, das gilt genauso für Skripte. Daher ist mit JBang auch ein "App Store" für Skripte verfügbar, diese können direkt von der Kommandozeile aufgerufen werden.

Natürlich sollte man sich vergewissern, dass diese Skripte wirklich die Aufgaben erfüllen, die sie vorgeben. Aus Sicherheitsgründen vertraut man eher bekannten Projekten / Autoren.

Beispiele:

  • jbang jreleaser@jreleaser - Java Projekte publizieren von Andres Almiray

  • jfrprint@mbien/JFRLog~cli - Ereignisse aus JFR Logs auflisten von Michael Bien

  • httpd@quintesse - Http Server für das aktuelle Verzeichnis von Tako Schotanus

  • tabula@tabulapdf/tabula-java - Tabellen aus PDF extrahieren von Max Rydahl Andersen

Diese Apps aus dem Katalog können mittels jbang app install auch lokal unter einem einprägsamen Namen einmalig installiert, und dann einfach wie eine ausführbare Datei verwendet werden.

Mein Kollege Michael Simons hat in einem schönen Beispiel mittels der Java Bibliothek zur Fernsteuerung von MacOS seine aktuell spielenden Musiktitel erfasst, diese könnte man dann z.B. an eine API weiterreichen [simons-itunes].

Fortgeschrittene Features

JBang hat eine Reihe weiterer Features, auf die ich hier nicht eingegangen bin, die aber im Detail in der eingebauten Hilfe und Onlinedokumentation [jbang-docs] erklärt werden.

  • Aufzeichnung von JFR Events (--jfr)

  • Java Debugger aktivieren (--debug)

  • Class-Data-Sharing für schnelleres Startup (--cds)

  • JDK Management wie SDKMan,

  • Erzeugung von Java-Agents

  • Offline Modus (--offline)

  • Erzwungene Aktualisierung (--fresh)

  • Management von eines JBang Wrappers im Projekt ähnlich wie gradlew oder mvnw

Fazit

Während ich ursprünglich nur von der Fähigkeit wusste, Java Dateien wie Skripte auszuführen, hat mich JBang in seinem Umfang beeindruckt.

Max versucht damit die angenehmen Eigenschaften der Entwickler- und Nutzerfreundlichkeit von Python und Node.js auch für Java bereitzustellen. Dabei beschränkt er sich nicht nur auf die Ausführung, sondern umfasst auch Kompilierung, Bereitstellung, Deployment und Installation von Anwendungen.

Ich bin runherum begeistert, es zeigt sich wieder einmal, wenn begeisterte Entwickler ihre eigenen Probleme angehen, kommen gute Lösungen dabei heraus.

Das einzige Manko dass ich sehe ist die Kompatibilität der erzeugten Artefakte. Obwohl jbang Standard-Java Tools für seine Arbeit hinter den Kulissen verwendet, stehen die erzeugten Dateien, nicht automatisch in einem lokalen Maven-Repository auch für andere Umgebungen bereit und die jbang Kataloge sind ebenso ein proprietäres Format.

Resourcen