Java Magazin   6.2023 - Frontends im Fokus

Preis: 9,80 €

Erhältlich ab:  Mai 2023

Umfang:  100

Autoren / Autorinnen: 
Elisavet Vasileiadou ,  
Robert Hoffmann ,  
Nils Hartmann ,  
Manfred Steyer ,  
Michael Egger-Zikes ,  
Rainer Hahnekamp ,  
Tim ZöllerIngo Küper ,  
Anzela Minosi ,  
Jason Clark ,  
Dr. Veikko Krypczyk ,  
Frank Delporte ,  
Sebastian Springer ,  
Gernot Starke ,  
Mark Sailes ,  
Boris FresowMarkus Günther ,  
Gregor Bauer ,  

Das Frontend ist wie eine Straßenkarte: Es lädt uns ein, Anwendungen zu erkunden und zu nutzen. Es weist Kund:innen den Weg durch Ihre Website. Doch wie plane ich die Reise der Nutzer:innen? Wie gestalte ich ein gutes Frontend?

Zu den grundlegenden Entscheidungen bei der Frontend-Entwicklung gehören die Wahl des richtigen Architekturmusters und der passenden Technologie. Eine gut durchdachte und strukturierte Frontend-Architektur kann Skalierbarkeit, Wartbarkeit und Erweiterbarkeit einer Anwendung verbessern und die Entwicklungsgeschwindigkeit erhöhen. Hier gibt es heutzutage eine Vielzahl von Orientierungsmöglichkeiten: Frameworks, Bibliotheken und Tools, die Ihnen als Kompass dienen und die Entwicklung erleichtern und beschleunigen können.

In der neuen Ausgabe des Java Magazin treffen auf die Vielzahl von Optionen eine Vielzahl von Artikeln, die sich diesem wichtigen Thema widmen und Ihnen wie eine Straßenlaterne in der Dunkelheit den Weg weisen. Dabei geht es um Design-to-Code-Funktionen, Performante Microfrontends und Best Practices. Manfred Steyer beschäftigt sich z. B. mit der Frage, wie sich die Idee von Server-side Rendering und Hydration auf SPA-basierte Microfrontends übertragen lässt. Wie Strukturen für das Frontend aussehen können, zeigt uns Rainer Hahnekamp in einem nach Anwendungsgrößen sortierten Überblick; dabei kommt das schlanke Tool Sheriff zum Einsatz. Die Rundumlösung AWS Amplify steht im Fokus des Artikels von Robert Hofmann; das Framework für die Full-Stack-Entwicklung kann sowohl für Web- als auch für Mobile-Anwendungen verwendet werden. Zudem geht Nils Hartmann der Frage nach, ob React zu einem „Backend-Framework“ geworden ist. Was meinen Sie?

Viel Freude beim Lesen wünscht Ihnen

vasileiadou_elisa_sw.tif_fmt1.jpgElisavet Vasileiadou | Redakteurin

Mail Website Twitter

Kommt es pünktlich zum zehnjährigen Jubiläum von React zu einer Neuausrichtung der populären UI-Bibliothek? Die jüngst überarbeitete React-Dokumentation lässt keinen Zweifel daran, dass der bisherige Ansatz, rein clientseitige React-Anwendungen zu entwickeln, nicht mehr zeitgemäß sei. Stattdessen wird explizit die Verwendung von „Full-Stack-Frameworks“ empfohlen. Was sind Full-Stack-Frameworks und was bedeutet das für den künftigen Einsatz von React?

Hintergrund dieser Aussagen ist das verstärkte Bemühen, React-Anwendungen auch als serverseitige Anwendungen auszuführen, was zu besserer Performance führen soll. Kritiker bemängeln schon seit Anbeginn vermeintlich schlechte Performance von Single-Page-Anwendungen – ganz unabhängig vom verwendeten Framework. Mit Performance ist hier die initiale Dauer beim Laden und Darstellen der Seite einer Anwendung gemeint. Bei einer klassischen Webanwendung muss der Browser beim Aufruf einer Seite HTML- und CSS-Code sowie statische Artefakte wie Bilder und Schriften vom Server laden, diese interpretieren und die Seiten dann rendern. Das geht in vielen Fällen sehr schnell, insbesondere wenn die zu ladenden Dateien in einem (lokalen) Cache liegen oder dynamisch nachgeladen werden können. Bei klassischen Single-Page-Anwendungen sieht das anders aus. Hier muss der Browser zwar ebenfalls die genannten Artefakte herunterladen. Die HTML-Seite ist allerdings in der Regel weitgehend leer und enthält nur script-Elemente, die dazu dienen, den JavaScript-Code einzubinden, in dem letztlich die Darstellung der Anwendung implementiert ist. Dieser Code muss zusätzlich vom Browser geladen und ausgeführt werden. Erst nach Ausführung des JavaScript-Codes wird die Anwendung dann dargestellt. Der ausgeführte JavaScript-Code sorgt auch dafür, dass die Anwendung ab diesem Zeitpunkt interaktiv nutzbar ist.

Dieser Mehraufwand, „nur“ um eine interaktive Anwendung zu erhalten, und die damit einhergehende schlechtere Performance gegenüber klassischen Webanwendungen sind ein häufiger Kritikpunkt, insbesondere wenn die Anwendung eher statischen Charakter hat und die Implementierung als Single-Page-Anwendung von außen betrachtet mangels Interaktivität scheinbar keinen Mehrwert bietet.

Einstieg in React - Entdecken im Entwickler Magazin 6.23

React ist ein beliebtes und verbreitetes JavaScript-Framework, doch auch hier muss der Einstieg gelingen. In der Entwickler Magazin Ausgabe 6.23 widmen wir uns daher dem Framework und seine Ökosystem. Alles zu Remix, Jotai, Barrierefreiheit und dem richtigen Initiieren eines React-Projekts findet ihr in dieser Ausgabe.

Konzentriert man sich nur auf diesen Aspekt der Performance von Anwendungen, mag die Kritik an Single-Page-Anwendungen und deren Einsatz im Einzelfall berechtigt sein. Sie lässt allerdings aus, dass die weitere Interaktion innerhalb der Anwendung nach deren Start sehr schnell sein kann, weil sich diese nur im Browser abspielt und nun zum Beispiel bei der Navigation oder der Überprüfung von Eingaben Round Trips zum Server vermieden werden können. Je interaktiver eine Anwendung ist, desto relevanter ist auch dieser Teil der Performance und desto weniger fällt der initiale Mehraufwand möglicherweise ins Gewicht.

Serverseitiges Rendern

Im Folgenden geht es aber um die Frage, wie die Performance beim Laden und Starten einer Webanwendung, die als Single-Page-Anwendung implementiert ist, verbessert werden kann. Dazu bieten alle bekannten SPA-Frameworks die Möglichkeit an, die Anwendung serverseitig vorzurendern. Das wird auch als Server-side Rendering (SSR) bezeichnet. Dabei wird beim ersten Aufruf einer Seite einer SPA der (weitgehend) identische Code wie auf dem Client ausgeführt, um die Komponenten der abgefragten Seite zu rendern. Das Ergebnis wird dann nicht in den DOM geschrieben (der auf dem Server natürlich nicht zur Verfügung steht), sondern wird als fertiger HTML-Code an den Browser zurückgesendet. Dieser kann – wie bei einer statischen HTML-Seite – die Seite darstellen, ohne den JavaScript-Code laden, parsen und ausführen zu müssen. Die Zeit bis zur ersten Darstellung der Anwendung im Browser ist dann vergleichbar mit der einer klassischen Webanwendung. Damit die Anwendung dann interaktiv wird, lädt der Browser nun auch (wie bei einer klassischen SPA) den JavaScript-Code der Anwendung und führt diesen clientseitig aus. Erst jetzt ist die Anwendung nicht nur sichtbar, sondern auch interaktiv bedienbar. Jede weitere Navigation erfolgt dann nur noch auf dem Client.

React unterstützt serverseitiges Rendern schon seit jeher. Es gibt dazu im React API Funktionen, die eine React-Komponente in einen HTML-String rendern können. Diese Funktion kann zum Beispiel in einem Webserver auf Node.js-Basis bei einem Request aufgerufen und das Ergebnis dann an den Client zurückgegeben werden. Allerdings ist das in der Praxis nicht so trivial umzusetzen, denn man muss zum Beispiel darauf achten, dass der Code sowohl im Browser als auch auf dem Server funktioniert (auf dem zum Beispiel keine Interaktion mit dem DOM möglich ist). Außerdem ist die Arbeit mit Komponenten knifflig, die asynchron arbeiten, um zum Beispiel Daten von einem API zu laden. Dann muss der Serverprozess irgendwie wissen, wie lange bzw. auf welche Komponente er noch warten muss, bis das Rendern vollständig abgeschlossen und das Ergebnis komplett ist. Zudem muss man dafür sorgen, dass Tooling und Build-Prozess samt Bundling nun auch auf dem Server funktionieren.

Auch bei einer serverseitig gerenderten Anwendung muss der Browser den JavaScript-Code der Anwendung laden, sonst wäre die Anwendung zwar dargestellt, aber nicht interaktiv bedienbar. Leider gilt das auch für Java-Script-Code, der gar nicht für Interaktivität zuständig ist, sondern Komponenten betrifft, die ausschließlich für die reine Darstellung von Daten zuständig sind (und zum Beispiel selbst keine Event Handler registrieren). Eine Komponente, die zum Beispiel eine Liste von Artikeln darstellt, wird diese auf dem Server genauso rendern wie auf dem Client. Ist diese Komponente bereits auf dem Server gerendert worden, müsste sie prinzipiell auf dem Client nicht erneut gerendert werden, und ihr JavaScript-Code wird im Browser eigentlich gar nicht mehr benötigt.

React Server Components

An diesem Punkt setzen die React Server Components (RSC) an, die Ende 2020 erstmals vorgestellt wurden. Dabei handelt es sich um React-Komponenten, die nur auf dem Server ausgeführt werden können. Ihr JavaScript-Code wird nicht auf den Client übertragen. Das kann die Performance beim Laden und Starten der Anwendung verbessern, weil das Volumen der übertragenen Daten gesenkt wird und der Browser auch weniger JavaScript-Code zu parsen und auszuführen hat. Das gilt dann auch für externe Bibliotheken, wenn diese ausschließlich innerhalb einer RSC verwendet werden: Auch deren Code muss nun nicht mehr auf den Client übertragen werden. Da die RSC nur auf dem Server verwendet werden, scheiden sie für alle Komponenten aus, die direkt mit dem DOM arbeiten, interaktiv sind oder Zustand verwenden (useState, useEffect etc. funktionieren in den Komponenten nicht, allerdings können RSC weiterhin „normale“ Komponenten einbinden, die dann wie gewohnt auf dem Client funktionieren und alle React APIs verwenden dürfen). Da eine RSC sich allerdings darauf verlassen kann, ausschließlich auf dem Server zu laufen, kann sie dessen Infrastruktur nutzen. Sie kann zum Beispiel direkt auf das Dateisystem des Servers oder auf Datenbanken zugreifen. Das folgt einerseits dem „Co-Location“-Prinzip von React, nach dem Dinge, die zusammengehören, auch im Code möglichst beisammen sein sollen (hier: Laden und Darstellen der Daten direkt innerhalb der Komponente). Andererseits gilt auch hier das Versprechen bessere Performance. Dabei wird vorausgesetzt, dass Latenzen beim Ermitteln benötigter Daten in einer Serverkomponente aufgrund der Nähe zur Datenbank kürzer sind als auf dem Client, wo einzelne Requests im schlechtesten Fall um die halbe Welt gehen müssten. Ob dieses Versprechen eingelöst werden kann, hängt aber auch von vielen weiteren Faktoren wie dem Deployment und der Architektur des Gesamtsystems ab.

Der Einsatz von React Server Components bedeutet für Server weitere Herausforderungen gegenüber dem herkömmlichen SSR. Unter anderem wird das Protokoll zwischen Client und Server komplizierter. Die serverseitige Ausführung der Anwendung beschränkt sich nämlich nicht mehr auf die Auslieferung einer kompletten (ersten) Seite für den Client. Die Anwendung kann jederzeit weitere Teile einer Seite vom Server abfragen. Es gibt vielleicht eine Clientkomponente, die festlegt, ob dargestellte Artikel auf- oder abwärts sortiert werden sollen. Die Komponente zur Darstellung dieser Artikel ist aber eine Serverkomponente, deren JavaScript-Code auf dem Client nicht vorhanden ist und die somit auch nicht auf dem Client von React ausgeführt und neu gerendert werden kann. Damit beim Ändern der Sortierreihenfolge nun aber nicht die gesamte Seite neu geladen und gerendert werden muss, muss React einen Request an den Server senden, mit dem nur die fertig gerenderte Artikellistekomponente vom Server abgefragt wird. Der Server muss entsprechend auch mit solchen partiellen Renderings umgehen können. Dieses Verfahren wird auch Partial Hydration genannt.

React-Frameworks

Möchte man mit Server Components arbeiten, ist man realistischerweise dazu gezwungen, ein Full-Stack-Framework wie Next.js zu verwenden. Dabei handelt sich allerdings nicht nur um ein Framework im klassischen Sinne, sondern eher um ein ganzes Produkt inklusive Build-System und Server.

Einen anderen Ansatz, um die genannten Probleme hinsichtlich Performance und Ressourcen zu lösen, hat Remix. Auch hierbei handelt es sich wie bei Next.js um ein komplettes Produkt. Remix unterstüzt zwar zurzeit keine React Server Components, kann aber auch Komponenten auf dem Server ausführen und fertig gerendert zum Browser senden. Der Zugriff auf Daten erfolgt allerdings nicht direkt in der Komponente, sondern in sogenannten loader-Funktionen. Diese können ebenfalls – wie Server Components – mit der kompletten Serverinfrastruktur oder auch dem HTTP-Request- und -Response-Objekt (z. B. für Cookies) arbeiten. Die zugehörige Komponente kann die Daten mittels eines Hooks abfragen.

Beide Tools, Remix und Next.js, gehen über die Entwicklung von React-Anwendungen hinaus und verstehen sich als Komplettlösungen für die Entwicklung von Backend- bzw. ganzen Full-Stack-Anwendungen. Dazu gehören auch die Möglichkeiten, serverseitig UI-unabhängige Geschäftslogik zu implementieren oder „normale“ HTTP-Endpunkte z. B. für REST APIs zu entwickeln. Auch das Deployment in populäre Cloud-Umgebungen ist vorgesehen.

Vor diesem Hintergrund sind die einleitend genannten Aussagen zu verstehen. Die serverseitige Ausführung von React-Anwendungen soll „normal“ werden. Und wer diese Features verwenden will, kommt um die Verwendung eines „Full-Stack-Framework“ (manchmal auch „Meta-Framework“ bezeichnet) nicht herum. Aus diesem Grund enthält die jüngst aktualisierte React-Dokumentation nun auch ganz offiziell die Empfehlung, für „vollständige“ React-Anwendungen („entire apps“) auf eines der genannten Frameworks zu setzen. Die Präferenz des React-Teams geht klar in Richtung Next.js, für das mittlerweile auch zwei prominente Ex-Mitglieder des React-Teams arbeiten. Ob der Begriff „Framework“ hier so glücklich gewählt ist, sei dahingestellt. Insbesondere darf man nicht vergessen, dass deren Einsatz zwingend JavaScript auf dem Server bedeutet. Ein reiner Webserver zur Auslieferung der fertig gebauten Anwendung funktioniert damit nicht mehr. Und bei aller lautstarken Kritik an der Ladezeit und den benötigten Mengen an JavaScript-Code von SPAs auf dem Client darf man nicht vergessen, dass es durchaus Anwendungen gibt, bei denen beispielsweise aufgrund ihrer hohen Interaktivität die serverseitigen Optimierungen keinen Mehrwert bringen oder schlicht nicht notwendig sind.

Aus diesem Grund kann man für klassische React-Anwendungen auch weiterhin ohne ein „Framework“ auskommen – beispielsweise mit Vite oder Nx. Auch create-react-app gibt es noch, es wird aber zur Zeit nicht mehr weiterentwickelt und enthält zum Beispiel keinen modernen nativen Bundler. Wie es in der Zukunft mit create-react-app weitergeht, bleibt abzuwarten. Immerhin gibt es mittlerweile Überlegungen, zumindest auf Vite umzustellen.

Zukunft von React

Die Diskussion über die Zukunft von React und den Einsatz von Full-Stack-Frameworks wie Next.js und Remix wird lautstark und kontrovers diskutiert. Kritiker weisen darauf hin, dass viele dieser Ansätze bereits in früheren Technologien existierten. Und tatsächlich: Wenn man sich eine JSP von Anfang der 2000er Jahre mit direkter Verwendung einer Data Source und SQL-Abfragen ansieht, sieht das einer React Server Component, die direkt eine Datenbank verwendet, nicht unähnlich. Ryan Florence, einer der Remix-Entwickler, bezeichnete diesen Ansatz unironisch als „PHP, aber in gut“. Auch das Rendering von einzelnen Teilen einer Seite auf dem Server ist nicht neu, man denke etwa an Java Server Faces. Trotzdem sind Häme und Spott („Nichts Neues!“), die auch aus Teilen der Java-Community kommen, hier unangebracht. Ein gravierender Unterschied ist nämlich, dass in den React-Frameworks in Backend und Frontend ausschließlich mit einer Programmiersprache, einem Ökosystem mit Bibliotheken und einem Tooling ohne Technologiebruch gearbeitet wird. Technologische Innovationen und Weiterentwicklung erfolgen zudem selten linear, sondern in Folge von Wiederholungen und Variationen – man erinnere sich beispielsweise an die Kreise und Schleifen, die das Thema Modularisierung in der Vergangenheit durchlaufen hat.

Auf der anderen Seite sollte man auch dem teilweise sehr grellen Marketing („Mit Remix braucht ihr kein useState und useEffect mehr.“) der Framework-Hersteller und ihrer Apologeten nicht blindlings hinterlaufen. Pauschale Marketingaussagen müssen nicht immer passende Lösungen für konkrete Probleme einer Anwendung sein. Eine Stärke von React ist bislang die sehr hohe Abwärtskompatibilität, sodass man damit auch gefahrlos Anwendungen bauen kann, die einen langen Zeitraum leben sollen. Ob das bei den neuen Tools der Fall ist, wird sich zeigen. Sowohl in Remix als auch in Next.js werden derzeit noch von Release zu Release größere Änderungen vorgenommen (app-Verzeichnis in Next.js, neue Verzeichniskonventionen in Remix). Next.js verwendet teilweise abenteuerlich anmutende Ansätze, die den Charakter von Workarounds haben, um technische Probleme zu lösen. Der React-Router, der zu einem (eigenständigen) Teil von Remix gemacht wurde, hat in der Vergangenheit bereits mehrfach zum Teil erhebliche API-Änderungen erfahren, die nicht abwärtskompatibel waren.

Richtig ist, dass diese Frameworks bestehende Probleme lösen können. Allerdings hat der Einsatz serverseitiger Technologien erhebliche Konsequenzen auch für Verständnis, Architektur und Betrieb der (React-)Anwendungen. Auch wenn der Code einer serverseitigen Komponente nahezu identisch aussieht wie in herkömmlichen React-Komponenten, kann das Verhalten ein ganz anderes sein. Es ist zum Beispiel nicht immer trivial, zu verstehen, an welchen Stellen die Anwendung Server-Requests zum Neuladen der Darstellung macht. Die Technologien, die solche „gemischten“ Ansätze in Java ermöglicht haben, stehen nicht zu Unrecht in einem eher schlechten Licht bzw. sind sogar schon in Vergessenheit geraten.

Fazit

Die Weiterentwicklung von React verlief in letzter Zeit eher schleppend: React 17 brachte gar keine neuen Features mit, React 18 legte immerhin die Grundlagen für die serverseitigen Features, brachte aber ansonsten eher Features für Corner Cases mit. Suspense für Data Fetching (ein wichtiger Baustein für die Umsetzung von Partial Hydration) wurde bereits für Mitte 2019 angekündigt und ist immer noch nicht stabil verfügbar. Dasselbe gilt für die Serverkomponenten, die auch noch nicht in finaler Form vorliegen.

Das Tempo scheint sich momentan allerdings zu erhöhen, möglicherweise befeuert durch Tools wie Next.js und Remix und dem Druck durch spezialisierte Frameworks wie Astro und Gatsby. Ob man pathetisch vom Beginn einer „neuen Ära für React“ sprechen kann, bleibt abzuwarten. Profitieren werden vor allem Anwendungen, die an vielen Stellen die serverseitigen Features verwenden können. Das dürften in erster Linie Anwendungen mit einem hohen Anteil statischen Contents sein. In jedem Fall bleibt es sinnvoll, vor dem Um- oder Einstieg auf oder in eins der Frameworks die eigenen Anforderungen zu prüfen und auf dieser Basis zu entscheiden. Und nicht auf Basis der buntesten Werbeversprechen.

Welche Vorteile bieten Kommandozeilentools? Wir sehen uns einige Grundlagen und praktische Beispiele von Java für die Konsole an.

Kommandozeilentools haben gegenüber dem GUI etliche Vorteile: Befehle werden auf der Konsole schneller verarbeitet, Tasks, die sich wiederholen, können unkomplizierter automatisiert werden und Entwickler von Kommandozeilentools können auf der Konsole mehr aus dem Tool herausholen. Allerdings haben die Tools auch einen großen Nachteil: Anwender brauchen meistens länger, um sich daran zu gewöhnen [1].

Was zeichnes ein gutes Kommandozeilentool aus?

Während der Entwicklung von Konsolenprogrammen sollten einige Konventionen beachtet werden. Die Konsistenz erleichtert es Benutzern, sich schneller und besser einzuarbeiten. Folgende Merkmale kennzeichnen ein gutes Kommandozeilentool [2]:

  1. Nützliche Hilfe: die einzelnen Optionen übersichtlich aufzählen, formatiert ausgeben, eventuell ASCII-Art einsetzen

  2. Optionen vor Argumenten: Optionen, die mit einem Wert verknüpft sind, machen es Benutzern leichter, sie sich zu merken, zum Beispiel mycmd -dir /path/to/directory; verglichen mit Optionen sind Argumente positionsgebunden, was es Anwendern erschwert, sich die Position des jeweiligen Parameters zu merken

  3. Version ausgeben: die Version sollte mittels eines geeigneten Flags ausgegeben werden, zum Beispiel -v oder --version

  4. Stdout vs. Stderr: Warnungen oder Fehlermeldungen über Stderr ausgeben, während alle anderen Infos auf Stdout erscheinen. Benutzer, die zum Beispiel die Dateinamen eines Verzeichnisses in einer Textdatei abspeichern, stellen so sicher, dass nur die relevanten Infos in der Textdatei vorkommen, die Warnungen aber nicht.

  5. Fehler ausgeben: eine gute Fehlermeldung beinhaltet Code, Titel, Beschreibung und einen Hinweis, um den Fehler zu beheben, sowie eventuell einen Link zur Website des Programms

  6. Moderne Benutzerschnittstelle: Ausgaben mit Animationen versehen, zum Beispiel Fortschrittsbalken oder Indikatoren einbauen, um den Benutzer über den Fortschritt zu informieren, Farben einsetzen; andererseits sollte der Benutzer selbst entscheiden können, ob die Anzeige farbig ist, indem sich das Kommandozeilentool anpassen lässt

  7. Eingabeaufforderungen einsetzen: Passwörter per Eingabeaufforderung abfragen, anstatt sie als Argument zu übergeben, sonst besteht die Gefahr, dass sie leichter ausgelesen werden können; zusätzlich sollte der Benutzer die Möglichkeit haben, andere Argumente mittels Eingabeaufforderung einzugeben

  8. Listen in Form von Tabellen ausgeben

  9. Schnelle Befehlsverarbeitung: falls es länger dauert, zum Beispiel bei Downloads, dann den Benutzer über den Fortschritt informieren

  10. Anweisungen vs. Unteranweisungen: schlichte Programme wie cp (copy) oder cd (change directory) kommen mit Optionen und Argumenten aus; umfangreiche Kommandozeilentools wie git sind auf Unterbefehle angewiesen, um die Befehlseingabe übersichtlich zu halten

  11. Während der Entwicklung die Richtlinien befolgen: zum Beispiel Open Group [3], GNU [4], Command Line Interface Guidelines [5]

Java-Programme für die Konsole: Trickreiche Tabellen

Etliche Kommandozeilentools lesen Daten ein und verarbeiten sie weiter, um sie anschließend in Form einer Tabelle auszugeben. Beispielsweise beschäftigen sich Wissenschaftler oftmals mit Datenreihen, die als einfache Textdatei, bei der die Spalten durch Tabs voneinander getrennt sind, vorliegen. Solche Datenreihen lassen sich bequem mit JavaPlot (Version 0.5.0) [6] einlesen (Listing 1).

Listing 1

import java.io.File;
import java.io.IOException;
import com.panayotis.gnuplot.dataset.FileDataSet;
 
public class MyData {
  private String fileData;
  private FileDataSet dataset;
  private File file;
  
  public MyData(String d) {
    this.fileData = d;
  }
  
  // liest Datenreihen ein
  public void setDataset() {
    try{
    file = new File(fileData);
    this.dataset = new FileDataSet(file);
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }
  
  // Datenpunkt abrufen
  public Double getDataPoint(int iCol, int index ) {
    String sDataPoint = this.dataset.getPointValue(index, iCol);
    Double dVal = Double.parseDouble(sDataPoint);
    return dVal;
  }
}

Ein MyData-Objekt liest die Daten ein, nachdem es zusammen mit dem Pfad der Textdatei initialisiert wird:

MyData data = new MyData("/home/user/dataset.txt");
data.setDataset();

Für die Weiterverarbeitung der Daten kann ein Liste mit Zahlen eingesetzt werden. So lassen sich die Daten vom Typ FileDataSet in ein double-Array umwandeln (Listing 2). Laut der Dokumentation von JavaPlot ist es nicht möglich, durch die Liste vom Typ FileDataSet zu iterieren, weshalb die Hilfsmethode getDataPoint() aus Listing 1 erforderlich ist.

Listing 2

public double [][] toData() {
  int iSize = dataset.size();
  int xcol = 0;
  int ycol = 1;
  double [] [] data =new double[iSize][2];
  
  for(int i=0; i< iSize; i++) {
    Double x = mydata.getDataPoint(xcol,i );
    Double y = mydata.getDataPoint(ycol,i );
    data[i][xcol] = x;
    data[i][ycol] = y;
  }
  
  return data;
}

Im Gegensatz zu Zahlen lassen sich Textdateien zeilenweise einlesen, indem Apache FileUtils – aktuell in der Version 2.11 [7] – zum Einsatz kommt (Listing 3).

Listing 3

public List<String> readInFile(String sPath) {
  File file = new File(sPath);
  List<String> lines = null;
  
  try {
    lines = FileUtils.readLines(file,StandardCharsets.UTF_8.name());
  } catch(IOException e) {
    e.printStackTrace();
  }
  
  return lines;
}

Sobald das Tool die Daten weiterverarbeitet, können sie auf der Konsole in Form einer ASCII-Tabelle erscheinen. Eine solche ASCII-Tabelle könnt ihr wahlweise mit StringBuilder oder Formatter [8] erstellen. Java Formatter hat den Vorteil, dass es die Daten zusätzlich formatiert. Das Tool kann unterschiedliche Datentypen formatieren und direkt ausgeben oder als String speichern. Für die Formatierung der Ausgabe wendet Java Formatter eine spezielle Syntax an (Abb. 1). Beispielsweise gibt die Methode printDate() aus dem folgenden Code-Schnipsel den 23. Mai 1995 so aus: 23/05/1995.

public void printDate(Calendar c ) {
  fmt.format("%1$te/%1$tm/%1$tY\n", c);
}

Angewandt auf das Datum wird der Tag mittels %1$te/ formatiert, wobei %1$ für die Position steht, an der sich das Argument c befindet, t für den Datentyp Date/Time, e für Tag und / für das Trennsymbol.

Abb. 1: Syntax des Java Formatter

Abb. 1: Syntax des Java Formatter

Java Formatter verfügt zwar über keine Methode, um Tabellen zu generieren, allerdings lässt sich die Länge der einzelnen Ausgaben festlegen, sodass die benachbarten Werte etwas entfernt voneinander liegen, was dann wie eine Tabelle aussieht. Im folgenden Snippet gibt Formatter die erste Spalte einschließlich Doppelpunkt mit einer Breite von 15 Zeichen aus, wobei diese Formatierung auf einen String angewendet wird, zu erkennen am s für den Datentyp String. Der zweite Teil des Codes formatiert eine Dezimalzahl. So werden für die zweite Spalte 13 Zeichen einschließlich des Vorzeichens reserviert.

public void printRow(String msg, float fVal) {
  fmt.format("%2$14s: %1$+12.2f\n",fVal,msg);
}

Zum Beispiel lassen sich Einnahmen im Oktober damit wie folgt ausgeben:

       October:    +3254,45

Um mit dem Formatter eine Tabelle zu erzeugen, könnt ihr eine Klasse anlegen, die den Vorgang automatisiert. Ihr könnt für die Tabelle eine feste Breite von 100 Zeichen bestimmen, da etliche Terminals das gut darstellen.

Wie aus Listing 4 hervorgeht, kommen bei zweispaltigen Tabellen unterschiedliche Spaltenbreiten zum Einsatz. So ist die erste Spalte 30 Zeichen breit. Der Rest der verfügbaren Zeichen, das sind 100 - 30 = 70, kommt bei der zweiten Spalte zum Einsatz. Sobald die Klasse MyTable unter Einbezug des ersten Konstruktors initialisiert ist, wird die Methode generateTable() aufgerufen. Bei der Iteration der Map wird jeweils der Schlüssel sowie Wert einer Datenreihe dem Formatter übergeben, wobei die Formatierung für die letzte Spalte das Escape-Zeichen \n enthält, damit die nachfolgenden Schlüssel-Wert-Paare in die nächsten Zeilen geschrieben werden. Dadurch, dass für jede Zeile die Spaltenbreite neu bestimmt wird, können beispielsweise der Tabellenkopf (siehe Methode generateTblHeader) und das Ende einer Tabelle mit Sonderzeichen versehen werden, um das Look and Feel einer Tabelle hervorzurufen (Listing 4).

Listing 4

import java.util.Formatter;
import java.util.LinkedHashMap;
 
public class MyTable {
  private LinkedHashMap<String, String> map;
  private double [][] dataRows;
  private String [] headers;
  private String header;
  private int numColumns=0;
  private int columnSize=0;
  private int diff = 0;
  private int dimension = 0;
  private int widestColumn = 0;
  public static final int TABLE_WIDTH = 100;
  public static final int KEY_COLUMN = 30;
  public static final String HEADER_BORDER = "=";
  public static final String BORDER = "+";
  public static final String ROW_SEP = "-";
  private Formatter fmt;
  
  // Für Tabellen mit 2 Spalten
  public MyTable(int numCol,LinkedHashMap<String, String> map,String header) {
    initMyTable(numCol,header);
    this.map = map;
    calcColumnSize(KEY_COLUMN);
    calcTableOffset(KEY_COLUMN);
    generateTable();
  }
  
  // Für Tabellen mit n Spalten
  public MyTable(double [][] rows  ,String header,String [] headers) {
    initMyTable(rows[0].length,header);
    this.dataRows = rows;
    this.headers = headers;
    this.dimension = rows[0].length;
    calcColumnSize(0);
    calcTableOffset(0);
    getMaxHeader();
    generateTableData();
  }
  
  private void initMyTable(int numCol,String header) {
    this.header = header;
    this.numColumns = numCol;
    fmt = new Formatter();
  }
  
  // Spaltenbreite berechnen
  private void calcColumnSize(int width) {
    columnSize = ((TABLE_WIDTH - width) / this.numColumns) -1 ;
    
  }
  
  // Offset der gesamten Tabelle berechnen
  private void calcTableOffset(int width) {
    diff = TABLE_WIDTH -  ( width + ((this.numColumns)*this.columnSize));
  }
  
  // Tabelle mit 2 Spalten erstellen
  private void generateTable() {
    generateTblTop();
    generateTblHeader();
    for(String key : map.keySet()) {
      String value = map.get(key);
      fmt.format("%"+(KEY_COLUMN)+"s" + Constants.SPACE + "%"+((columnSize))+"s\n",key,value);
    }
    generateTblBottom();
  }
  
  // Tabelle mit n Spalten erstellen
  private void generateTableData() {
    generateTblTop();
    generateTblHeader();
    generateTblHeaders();
    generateHeaderBorder();
    for (int i =0; i < this.dataRows.length; i++) {
      for (int j = 0; j < this.dimension; j++) {
        double dVal = dataRows[i][j];
        String s = Utility.toString(dVal, 2);
        generateColumn(j, s);
      }
    }
    generateTblBottom();
  }
  
  // Tabellenüberschrift einfügen
  private void generateTblHeader() {
    fmt.format("%"+(this.header.length())+"s\n",this.header);
    String bottomHeader = Utility.repeat(HEADER_BORDER , TABLE_WIDTH);
    fmt.format ( "%"+TABLE_WIDTH+"s\n",bottomHeader);
  }
  
  // Spaltenüberschriften einfügen
  private void generateTblHeaders() {
    for (int i = 0; i < this.headers.length; i++) {
      String s = headers[i];
      generateColumn(i, s);
    }
  }
  
  // unteren Tabellenrand erstellen
  private void generateTblBottom() {
    String bottom = Utility.repeat(BORDER , TABLE_WIDTH);
    fmt.format ( "%"+TABLE_WIDTH+"s\n",bottom);
  }
  
  // Trennung zwischen Überschrift und Daten erzeugen
  private void generateHeaderBorder() {
    String bottom = Utility.repeat(ROW_SEP , TABLE_WIDTH);
    fmt.format ( "%"+TABLE_WIDTH+"s\n",bottom);
  }
  
  // oberen Tabellenrand erstellen
  private void generateTblTop() {
    String bottom = Utility.repeat(BORDER , TABLE_WIDTH);
    fmt.format ( "%"+TABLE_WIDTH+"s\n",bottom);
  }
  
  // größte Spaltenüberschrift finden
  private void getMaxHeader() {
    int max = 0;
    int size = this.headers.length;
    for (int i = 0; i < size; i++) {
      int len = headers[i].length();
      if(len > max) {
        max = len;
        this.widestColumn = i;
      }
    }
  }
  
  // Daten in Spalten einfügen
  private void generateColumn(int i, String s) {
    String sFmt = "%";
    if(i == this.widestColumn) {
      sFmt += (this.columnSize+diff)+"s";
    } else {
      sFmt += (this.columnSize)+"s";
    }
    
    if (i == this.dimension - 1) {
      sFmt += "\n";
    }
    fmt.format(sFmt,s);
  }
  
  // Formatter zu String
  public String toString() {
    return fmt.toString();
  }
}

Unmittelbar nachdem die Klasse MyTable initialisiert wurde, wird die Methode calcColumnSize() aufgerufen, um die Spaltenbreite columnSize zu berechnen. Danach ermittelt die Methode calcTableOffset() die Zahl der restlichen Zeichen diff, die nicht für die Spalten vergeben worden sind. Die Differenz wird anschließend zu der Spalte addiert, die die breiteste (widestColumn) einer Tabelle ist. Die Methode generateTable() formatiert die Daten der Map schließlich zu einer zweispaltigen Tabelle. Der obere Rand der Tabelle wird mittels der Methode generateTblTop() eingefügt, während generateTblBottom() die Trennsymbole für den unteren Rand der Tabelle generiert. Die Überschrift wird von der Methode generateTblHeader() formatiert, wobei lediglich eine Spalte zum Einsatz kommt (Abb. 2). Die Klasse MyTable lässt sich bei zweispaltigen Tabellen wie folgt initialisieren und als String ausgeben:

MyTable tab = new MyTable(1,map,tblHeadline, data);
System.out.println(tab.getFormatter());

Für mehrspaltige Tabellen existiert die Methode generateTableData() der Klasse MyTable, die durch eine Liste mit Zahlen iteriert und die Daten mittels der Methode generateColumn() in die einzelnen Spalten einfügt. Die letzte Spalte enthält das Escape-Zeichen \n, damit die nächsten Daten in einer neuen Zeile erscheinen.

Abb. 2: Zweispaltige Tabelle mit Java Formatter

Abb. 2: Zweispaltige Tabelle mit Java Formatter

Darüber hinaus beherbergt die Klasse MyTable die Methode toString(), um die formatierten Daten in einen String umzuwandeln. So bleibt beim Abspeichern des Strings in eine neue Datei die Formatierung erhalten (Abb. 3). Passend dazu schreibt die Methode writeToFile(), die auf die Apache-Commons-IO-Bibliothek zurückgreift, den String in eine neue Datei (Listing 5).

Abb. 3: Eine mit Java Formatter erstellte Tabelle, die aus Datenreihen besteht

Abb. 3: Eine mit Java Formatter erstellte Tabelle, die aus Datenreihen besteht

Listing 5

public static void writeToFile(String msg, String f) {
  File ff = new File(f);
  try {
    FileUtils.touch(ff);
    FileUtils.writeStringToFile(ff, msg, StandardCharsets.UTF_8.name());
  } catch (IOException e) {
    e.getMessage();
  }
}

Vorgefertigte Tabellen für Kommandozeilentools

Abgesehen von ASCII-Tabellen der Marke Eigenbau kann man sich vorhandener Bibliotheken bedienen. Dadurch müsst ihr euch lediglich um die Dateneingabe kümmern, während die Bibliothek die Ausgabe der Tabelle übernimmt. Für die Erstellung einer mehrspaltigen Tabelle wird die Klasse MyAsciiTable zusammen mit einem Double Array, der Tabellenüberschrift header und den Spaltenüberschriften headers initialisiert. Anschließend wandelt die Methode toStrings() den Double Array in einen String Array bestehend aus formatierten Zahlen um. Danach kommt die Code2care-Bibliothek Bethecoder-Ascii_table [9] zum Einsatz, um aus den Strings eine Tabelle zu erstellen (Listing 6).

Listing 6

import de.vandermeer.asciitable.AsciiTable;
import com.bethecoder.ascii_table.ASCIITable;
 
public class MyAsciiTable {
  private double [][] dataRows;
  private String [][] sData;
  private String [] headers;
  private String header;
  private int iSize = 0;
  private int dimension = 0;
  
    // Für Tabellen mit n Spalten
    public MyAsciiTable(double [][] rows  ,String header,String [] headers) {
      initMyTable(rows[0].length,header);
      this.dataRows = rows;
      this.headers = headers;
      this.iSize = this.dataRows.length;
      this.sData = new String [this.iSize][this.dimension];
      this.toStrings();
    }
    
    private void initMyTable(int numCol,String header) {
      this.header = header;
      this.dimension = numCol;
    }
    
    // AsciiTable mittels Spaltenüberschriften + Datensätzen erstellen
    public String getTableData() {
      return ASCIITable.getInstance().getTable(headers, sData);
    }
    
    // Datenreihen in String Array kopieren
    private void toStrings() {
      for (int i =0; i < this.iSize; i++) {
        for (int j = 0; j < this.dimension; j++) {
          double dVal = dataRows[i][j];
          String s = Utility.toString(dVal, 2);
          this.sData[i][j] = s;
        }
      }
    }
    
    // Daten aus String Array zeilenweise in die AsciiTable kopieren
    public String getTableDataV2() {
      StringBuilder sb = new StringBuilder();
      AsciiTable at = new AsciiTable();
      sb.append(header.toUpperCase()+"\n");
      at.addRule();
      at.addRow(headers);
      at.addRule();
      for (int i =0; i < this.iSize; i++) {
        String [] arr =   toArray(i);
        at.addRow(arr);
        at.addRule();
      }
      sb.append(at.render());
      return sb.toString();
    }
    
    // aktuelle Zeile der Zahlenreihen in einen String Array kopieren
    private String [] toArray(int i) {
      String [] arr = new String [this.dimension];
      for (int g = 0; g < this.dimension; g++) {
        arr[g] = Utility.toString(this.dataRows[i][g],2);
      }
      return arr;
    }
 
}

Mittels der Methode getTableData() gibt die Bibliothek die Tabelle aus (Abb. 4). Allerdings beherbergt die Code2care-Klasse ASCIITable keine Methode, um eine Tabellenüberschrift einzufügen. Das lässt sich lediglich mit dem Verknüpfen von Strings bewerkstelligen.

Abb. 4: ASCII-Tabelle von Code2care

Abb. 4: ASCII-Tabelle von Code2care

Die Bibliothek Vandermeer Asciitable [10] formatiert die Tabelle aufwendiger. Ähnlich wie Code2care erlaubt Vandermeers ASCII-Tabelle nicht explizit das Setzen von Tabellenüberschriften. Die selbst definierte Methode getTableDataV2() fügt zunächst die Tabellenüberschrift in ein Objekt vom Typ StringBuilder ein. Um die Formatierung von Vandermeer anzuwenden, kommt anschließend dessen Methode AsciiTable.addRule() zum Einsatz. In der darauffolgenden for-Schleife kommen zunächst die Spaltenüberschriften, dann die Datenreihen dran. Für die Datenreihen ist es erforderlich, pro Zeile ein eindimensionales Stringarray zu erstellen, und es dann einer Instanz vom Typ AsciiTable zu übergeben (Abb. 5).

Abb. 5: Vandermeers ASCII-Tabelle

Abb. 5: Vandermeers ASCII-Tabelle

Das wars für dieses Mal! Im zweiten Teil dieser Miniserie werden wir uns intensiver mit der Bibliothek Picocli beschäftigen.

Mehr zu Java:

Java-Anwendungen mit Bordmitteln absichern Im Artikel "Java-Anwendungen mit Bordmitteln absichern" beschäftigt sich die Softwareentwicklerin Anzela Minosi mit IT-Sicherheit. Sie demonstriert, wie man Anwendung auf sicherheitsbezogene Ereignisse in Java untersucht.


Links & Literatur

[1] Techquickie: „Why Do Command Lines Still Exist?“: https://www.youtube.com/watch?v=Q1dwzi5DKio

[2] Dickey, Jeff: „12 Factor CLI Apps“: https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46

[3] Utility Conventions: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html

[4] Standards for Command Line Interfaces: https://www.gnu.org/prep/standards/html_node/Command_002dLine-Interfaces.html#Command_002dLine-Interfaces

[5] Command Line Interface Guidelines: https://clig.dev/

[6] JavaPlot: https://javaplot.yot.is

[7] FileUtils: https://commons.apache.org/proper/commons-io/

[8] Formatter: https://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html

[9] Code2care: https://code2care.org/java/display-output-in-java-console-as-a-table

[10] Vandermeer ASCII-Tabelle: https://github.com/vdmeer/asciitabl