Von Low-Code zur Eigenentwicklung
Am Anfang stand die Anfrage eines neuen Kunden, ihn bei der Modernisierung der Benutzerschnittstelle seines ERP-Produkts zu unterstützen. Der Kunde ist mit seinem System zum Enterprise-Resource-Planning in der Schweiz und Deutschland seit mehr als dreißig Jahren sehr erfolgreich am Markt. Allerdings ist die aktuelle Version, welche auf Oracle Forms basiert, weder im Browser noch auf mobilen Geräten bedienbar.
Deshalb wurde ein Projekt gestartet, um Ersatz für Oracle Forms zu finden. Die Wahl fiel dabei auf ein Low-Code-Werkzeug, das versprach, durch einen UI-Designer die wesentlichen Teile der Benutzerschnittstelle zu generieren und durch ein abstraktes Programmiermodell den Aufwand für die Programmierung möglichst gering zu halten. Allerdings kam das Projekt nicht wie gewünscht voran. Eine erste Analyse zeigte, dass das Low-Code-Werkzeug eine typische 80/20-Lösung war. Das heißt, die ersten 80 Prozent der Umsetzung waren einfach, weil die Standardfälle durch das Werkzeug abgedeckt waren, aber die restlichen 20 Prozent waren mit großem Aufwand verbunden, hatten sich doch in der langen Lebensdauer des ERP-Produkts viele Spezialfälle eingeschlichen.
Ein weiterer Aspekt, der das Projekt zur Herausforderung macht, ist der Umfang und der dynamische Charakter der Applikation. Die Datenbank besteht aus rund 1800 Tabellen und Views, 4600 Prozeduren und Funktionen sowie über 100 User Defined Types. Das Menü der Applikation umfasst mehr als 800 Einträge. Die Formulare und Dialoge können sich je nach Benutzerberechtigung oder aufgrund der Daten zur Laufzeit ändern.
Ebenfalls hat der Kunde die Möglichkeit, das User Interface an seine Bedürfnisse anzupassen. Abbildung 1 zeigt das Konzept: Der Benutzer greift auf ein UI-Modul zu und anhand der Daten und Berechtigungen wird dieses dynamisch generiert. Diese Funktionalität stellt insbesondere in Bezug auf die Performanz eine gewisse Herausforderung dar, da sehr viele Daten von der Datenbank geladen werden müssen.
Abb. 1: Laufzeit-generiertes UI
Interessant war aber, was das Low-Code-Werkzeug generierte: ein Web UI basierend auf Vaadin [Vaadin]. Das machte mich neugierig. Ich konnte mich erinnern, Vaadin in einem Projekt vor zehn Jahren verwendet zu haben. Kurzerhand wurde ein Prototyp mit Vaadin erstellt, und nach einiger Diskussion entschied sich der Kunde, sich vom Low-Code-Werkzeug zu trennen und direkt auf Vaadin als UI-Framework zu setzen.
Was ist Vaadin?
Vaadin ist ein Webframework, das es erlaubt, mit Java Webapplikationen ohne Kenntnisse von Webtechnologien zu entwickeln. Vaadin ist Open Source und grundsätzlich frei, bietet aber Subskriptionen, die je nach Stufe Kurse, Support und kommerzielle Komponenten enthalten. Auch der UI Designer und die TestBench sind kostenpflichtig. Abbildung 2 illustriert die Geschichte von Vaadin. Der obere Zeitstrahl zeigt die Technologien und der untere die Versionen. Die größten Veränderungen am Framework waren die Version 6 mit der Verwendung von GWT, die Version 8 mit der Integration von Web Components und die Version 10 mit der neuen Flow-Architektur. Vaadin bietet seit Version 15 drei Programmiermodelle an. Die nach wie vor bekannteste und beliebteste Variante ist die Verwendung des Java-API, auf das der Artikel in der Folge auch eingehen wird. Es besteht des Weiteren die Möglichkeit, HTML-Templates zu erstellen und mit Java-Code zu integrieren oder, als neuste Variante, das UI in TypeScript zu programmieren und Vaadin Flow als Kommunikationsschnittstelle zu nutzen. Die letzte Variante ist entstanden, weil Vaadin-Applikationen Zustand haben und dieser an die HTTP-Session gebunden ist. Dadurch ist eine beliebige horizontale Skalierung nicht ohne Weiteres möglich. Auch möchte man heute nicht auf Offlinefähigkeit verzichten. Mit einer Trennung von Client und Server werden beide Nachteile behoben.
Abb. 2: Vaadin-Geschichte
Alles Java, oder was?
Listing 1 zeigt ein einfaches „Hello World”-Beispiel in Vaadin. Eine Seite, die per URL erreicht werden soll, muss mit der Annotation @Route versehen werden. Der Standardpfad leitet sich vom Klassennamen ab. In unserem Fall wird HelloView zu /hello. Als Nächstes fällt auf, dass die Klasse von VerticalLayout erbt. Vaadin kennt verschiedene Layouts. VerticalLayout ist ein sogenanntes Ordered Layout, das die Komponenten untereinander anordnet. Im Konstruktor der Klasse wird dann der Inhalt der View erzeugt: ein TextField, ein Label und ein Button. Beim Button wird ein Listener für das Click-Event registriert, der beim Anklicken eine Grußbotschaft ausgibt. Das Ergebnis sieht dann wie in Abbildung 3 aus.
@Route
@PageTitle("Hello")
public class HelloView extends VerticalLayout {
public HelloView() {
TextField textField = new TextField("Your Name");
Label label = new Label();
Button button = new Button("Greet",
e -> label.setText("Hello, " + textField.getValue()));
button.addClickShortcut(Key.ENTER);
add(textField, label, button);
}
}
Abb. 3: HelloView
Dieses Programmiermodell erinnert stark an AWT, Swing oder JavaFX und genau das ist für Java-Entwickler ein großer Vorteil. Bestehendes Wissen kann wiederverwendet werden. HTML, CSS oder JavaScript bleiben verborgen.
Vaadin-Architektur
In Version 10 wurde die Architektur (s. Abb. 4) radikal verändert. Von GWT ist für den Benutzer nichts mehr zu sehen. Stattdessen ist eine Komponente ins Zentrum gerückt, die als Vaadin Flow bezeichnet wird. Sie bietet ein typsicheres Java-Komponenten-API auf der Serverseite. Auf Clientseite kommen Web Components zum Einsatz. Vaadin Flow kümmert sich um die bidirektionale Kommunikation zwischen Browser und Server und bietet Data Binding in beide Richtungen. Das heißt, Änderungen auf der einen Seite werden automatisch an die andere Seite übertragen. Damit wird auch ein Push von Java zum Browser möglich. Dazu wird das Atmosphere-Framework [Atmosphere] verwendet, das sicherstellt, dass Push auch funktioniert, wenn keine Websockets zur Verfügung stehen.
Abb. 4: Vaadin-Architektur
Der große Vorteil, im Vergleich zu einer Architektur, wie sie beim Einsatz von Single-Page Applications (SPA) zum Einsatz kommt, ist, dass kein REST-API nötig ist. Damit entfällt ein großer Implementierungsaufwand und damit einhergehend auch der ganze Testaufwand auf API-Ebene. Die Integration des Java-Backends wird damit deutlicher einfacher und effizienter. Zudem bleiben die Entwickler auf dem Java-Stack und müssen weder eine andere Programmiersprache noch eine andere Build-Umgebung erlernen.
Routes
Eine wesentliche Änderung zu früheren Vaadin-Versionen ist das Konzept des Routings. Damit rückt Vaadin ein Stück näher an herkömmliche Webapplikationen heran.
Der Pfad der Route leitet sich aus dem Klassennamen ohne Suffix View ab. In Listing 2 wäre das /hasparameter. Bei der Deklaration kann in der Annotation der Pfad definiert werden. Wird ein leerer String als Pfad definiert @Route("") oder heißt die Klasse MainView, wird diese an den Root-Pfad gebunden. Es ist auch möglich, Pfade zu verschachteln, indem die einzelnen Teile mit / getrennt werden. Wie in Listing 2 ersichtlich, können Routen URL-Parameter haben und es kann auf Query-Parameter zugegriffen werden. Damit ist es möglich, einzelne Seiten einer Vaadin-Applikation direkt aufzurufen und Lesezeichen zu setzen. Um URL-Parameter zu verwenden, muss das Interface HasUrlParameter implementiert werden. Über den generischen Typ wird der Parametertyp bestimmt, der dann der Methode setParameter() übergeben wird. Auf die Query-Parameter kann über das Location-Objekt, das beim BeforeEvent angefragt wird, zugegriffen werden.
@Route
public class HasParameterView implements HasUrlParameter<String> {
@Override
public void setParameter(BeforeEvent event, String parameter) {
QueryParameters params = event.getLocation()
.getQueryParameters();
}
}
Komponenten
Vaadin bringt eine Vielzahl qualitativ hochwertiger Komponenten mit, die über sogenanntes Theming den Anforderungen angepasst werden können. Außerdem gibt es ein Verzeichnis mit Komponenten von Drittanbietern. Sämtliche Komponenten sind Web Components. Vaadin bietet die Möglichkeit, bestehende Web Components zu integrieren oder eigene zu erzeugen. Gemäß WebComponents. org [WebComponents] sind Web Components „… eine Reihe von Webplattform-APIs, mit denen Sie neue benutzerdefinierte, wiederverwendbare, gekapselte HTML-Tags erstellen können, die in Webseiten und Webanwendungen verwendet werden.“ Ein großer Vorteil von Web Components ist, dass diese auch unabhängig von Vaadin eingesetzt werden können.
PWA
Mit Vaadin können auch Progressive Web Apps [PWA] erzeugt werden. Die wesentlichen drei Pfeiler einer PWA sind das Web App Manifest, der Service Worker und die Push Notifications. Das Web App Manifest enthält Informationen zu einer Anwendung, zum Beispiel Name, Icon und Beschreibung. Damit wird eine PWA auf dem Gerät installierbar. Der Service Worker stellt alle Funktionalität zur Verfügung, die für die Offlinefähigkeit und Push Notifications benötigt wird.
Listing 3 zeigt eine minimale Konfiguration für PWA und Push. Wird die Applikation gestartet, wird unten im Browserfenster die Mitteilung angezeigt, dass die Applikation installiert werden kann (s. Abb. 5). Nach Klick auf „Install“ und Bestätigung der Meldung installiert der Browser die Applikation. Ruft man diese auf, wird sie in einem Browserfenster ohne Menü und Adresszeile gestartet und sieht aus wie eine Native Applikation. Der Offline-Support kann über eine Offlineseite sowie die Service Worker konfiguriert werden. Vaadin generiert eine Datei sw.js, die den Code für den Service Worker enthält und an die eigenen Bedürfnisse angepasst werden kann.
@Push
@Route
@PageTitle("Demo Application")
@PWA(name = "Demo Application", shortName = "Demo",
description = "This is a Vaadin Demo Application")
public class MainView extends VerticalLayout {
}
Abb. 5: PWA
Die Datenbankzugriffsschicht
Wie eingangs erwähnt, finden sich in der Datenbank des ERP-Systems 1800 Tabellen und Views und 4600 Prozeduren. Es musste ein Weg gefunden werden, um auf diese zuzugreifen. JDBC als Datenbankschnittstelle war natürlich gesetzt, aber ist ein O/R-Mapper wie Hibernate hier der richtige Ansatz? Wie viel Aufwand bedeutet das Mapping von 1800 Entitäten? Und wie können die 4600 Prozeduren typsicher aufgerufen werden? Da das UI zur Laufzeit erzeugt wird, werden die Typinformationen aus der Datenbank benötigt. Wie kann auf diese zugegriffen werden? Alle diese Fragen konnten glücklicherweise mit einem Framework beantwortet werden: jOOQ [jOOQ]. jOOQ integriert SQL in Java und macht es möglich, SQL typsicher zu verwenden. jOOQ generiert Java-Code basierend auf den Metadaten der Datenbank und bietet eine Domain Specific Language (DSL) in Form eines Fluent-API, um typsicheres SQL zu formulieren und Prozeduren auszuführen. Um den Code zu generieren, kann wie in Listing 4 das jOOQ Codegen-Maven-Plug-in konfiguriert werden.
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<configuration>
<generator>
<database>
<name>org.jooq.meta.extensions.ddl.DDLDatabase</name>
<properties>
<property>
<key>scripts</key>
<value>src/main/resources/db/migration/*.sql</value>
</property>
</properties>
</database>
<target>
<packageName>io.seventytwo.demo.database</packageName>
<directory>target/generated-sources/jooq</directory>
</target>
</generator>
</configuration>
</plugin>
Als Basis für die Generierung kann entweder direkt die Datenbank mittels JDBC-Verbindung verwendet oder wie im Listing 4 auf DDL-Skripte zugegriffen werden. Listing 4 zeigt nur einen Ausschnitt der Konfigurationsmöglichkeiten des jOOQ-Code-Generators. Im Beispiel werden FlyWay-Migrationen als Basis verwendet. Das Resultat zeigt Abbildung 6. Für alle Datenbankobjekte wird eine Klasse generiert. Im Beispiel gibt es eine Employee- und eine Department-Tabelle sowie eine VEmployee View. Zudem wurden noch drei Record-Klassen generiert. Ein Record ist die Repräsentation eines Datensatzes in der Tabelle oder View, und falls in der Tabelle der Primärschlüssel vorhanden ist, wird ein UpdatableRecord erzeugt. Dieser bietet die Möglichkeit, den Datensatz einzufügen, zu aktualisieren oder zu löschen.
Abb. 6: Generierter Code
Listing 5 zeigt einen Ausschnitt des Codes der generierten Klasse Department. Die von jOOQ generiert Klasse enthält sämtliche Metadaten dieser Tabelle. Im generierten Code finden wir unter anderem auch alle Tabellenspalten mit allen Informationen zum Namen, Typ, ob die Spalte nullable ist und ob es eine Primärschlüsselspalte ist. Sogar der Kommentar aus der Datenbank wird übernommen. Das ist äußerst hilfreiche und steigert die Effizienz, weil der Entwickler diese Informationen nicht in der Datenbank nachschauen muss. Auf der anderen Seite können diese Informationen auch verwendet werden, um Benutzereingaben zu validieren.
public final TableField<DepartmentRecord, Integer> ID =
createField(DSL.name("ID"),
INTEGER.nullable(false).identity(true), this, "");
public final TableField<DepartmentRecord, String> NAME =
createField(DSL.name("NAME"), VARCHAR(255).nullable(false),
this, "");
Listing 6 enthält Beispiele, wie es mittels dieser Klassen möglich ist, typsicheres, das heißt zur Kompilierzeit geprüftes SQL, zu schreiben. Wichtig zu erwähnen ist, dass jOOQ nicht nur den Java-Code in SQL umsetzt, sondern dabei auch das jeweilige Datenbankprodukt berücksichtigt und unter Umständen Funktionalität emuliert. Dadurch ergibt sich eine Unabhängigkeit, die es erlauben würde, das verwendet Datenbankprodukt auszutauschen. jOOQ unterstützt alle gängigen Datenbanken.
// Insert
dsl.insertInto(DEPARTMENT)
.columns(DEPARTMENT.NAME)
.values("IT")
.execute();
// Select All
List<EmployeeRecord> records = dsl.selectFrom(EMPLOYEE).fetch();
// Select mit Projektion
List<Record1<String>> records =
dsl.select(EMPLOYEE.NAME)
.from(EMPLOYEE)
.join(DEPARTMENT).on(DEPARTMENT.ID.eq(
EMPLOYEE.DEPARTMENT_ID))
.where(DEPARTMENT.NAME.eq("IT"))
.fetch();
Records anzeigen
Um datenzentrierte Applikationen zu entwickeln, gibt es eine Hauptkomponente, die nicht fehlen darf: das Grid. Vaadin hat ein Hauptaugenmerk auf diese Komponente gelegt und bietet hervorragende Unterstützung für Paging, Sortierung und Filterung.
Im Beispiel in Listing 7 wird ein Grid erzeugt, das auf der Klasse VEmployeeRecord basiert. Beim Erzeugen einer Spalte wird eine Funktion übergeben, die verwendet wird, um auf die Daten zuzugreifen. Spalten sind sortierbar, dazu kann das SortProperty gesetzt werden.
Grid<VEmployeeRecord> grid = new Grid<>();
grid.addColumn(VEmployeeRecord::getEmployeeId)
.setHeader("ID")
.setSortProperty("ID");
grid.addColumn(VEmployeeRecord::getEmployeeName)
.setHeader("Name")
.setSortProperty(V_EMPLOYEE.EMPLOYEE_NAME.getName());
grid.addColumn(VEmployeeRecord::getDepartmentName)
.setHeader("Department")
.setSortProperty(V_EMPLOYEE.DEPARTMENT_NAME.getName());
Um die Daten kümmert sich in Vaadin der DataProvider. Es werden zwei Varianten unterschieden, je nachdem, wo die Daten gehalten werden. Bei kleinen Datenmengen eignet sich ein In-Memory-DataProvider, welchem die Daten in einer Collection übergeben werden. Bei vielen Datensätzen bietet sich der CallbackDataProvider an, welcher Lazy Loading unterstützt. Das Grid wird im Lazy-Loading-Modus beim Blättern automatisch nachgeladen. Dem CallbackDataProvider muss ein FetchCallback, der einen Stream von Datensätzen und ein CountCallback, das die Anzahl der Datensätze zurückgibt, übergeben werden. Als optionaler dritter Parameter kann auch noch ein ValueProvider mitgegeben werden, der einen eindeutigen Schlüsselwert zurückgeben muss. Mit diesem Schlüssel werden die Zeilen identifiziert, um beispielsweise eine Aktualisierung durchzuführen. Ohne diesen ValueProvider werden equals() und hashCode() der Datenklasse verwendet.
Die beiden Callback-Methoden erhalten als Parameter eine Abfrage. Diese enthält Offset, Limit, Sortierung und gegebenenfalls ein Filterobjekt und wird verwendet, um die Resultatmenge einzuschränken und zu sortieren. Listing 8 zeigt, wie gut Vaadin und jOOQ zusammenpassen. Es werden eine Abfrage für die Daten und eine für die Anzahl Datensätze erzeugt.
dataProvider = new CallbackDataProvider<VEmployeeRecord,
Condition>(
query -> dsl.selectFrom(V_EMPLOYEE)
.where(query.getFilter().orElse(DSL.noCondition()))
.orderBy(createOrderBy(query))
.offset(query.getOffset())
.limit(query.getLimit())
.fetchStream(),
query -> dsl.selectCount()
.from(V_EMPLOYEE)
.where(query.getFilter().orElse(DSL.noCondition()))
.fetchOneInto(Integer.class),
VEmployeeRecord::getEmployeeId).withConfigurableFilter();
Mit withConfigurableFilter() kann daraus einem DataProvider ein ConfigurableFilterDataProvider erzeugt werden. Dieser stellt die Methode setFilter() zur Verfügung, mit der die Resultate eingeschränkt werden können. Listing 9 zeigt, wie bei einem TextField bei Änderung eine jOOQ Condition erzeugt und der setFilter()-Methode übergeben wird. Condition ist eine Klasse von jOOQ und wird in der WHERE-Bedingung der Abfragen verwendet.
Selbstverständlich kann jede Art von Datenzugriffsschicht, wie ein Service oder ein Repository, verwendet werden. In vielen Fällen ist das aber unnötig und bedeutet einzig höheren Aufwand mit geringem Nutzen.
TextField filter = new TextField("Filter");
filter.setValueChangeMode(ValueChangeMode.EAGER);
filter.addValueChangeListener(event -> {
if (StringUtils.isNotBlank(event.getValue())) {
dataProvider.setFilter(DSL.upper(V_EMPLOYEE.EMPLOYEE_NAME)
.like("%" + event.getValue().toUpperCase() + "%"));
} else {
dataProvider.setFilter(null);
}
});
Data Binding
Das zweite wesentliche Element einer Applikation sind Formulare. Vaadin kennt das Konzept des Data Bindings. Dafür ist bei Vaadin die Klasse Binder zuständig, die Werte aus einem Java-Objekt an UI-Komponenten bindet und sich um die Konvertierung und Validierung kümmert. Ein Binding kann schreibbar oder auch nur read-only sein. Für alle Felder, die nicht vom Typ String sind, muss ein Konverter angegeben werden. Es sind bereits gängige Konverter, um zum Beispiel Strings in Zahlen oder Datumstypen zu konvertieren, eingebaut. Aber auch das Schreiben eines eigenen Konverters ist ganz einfach.
In Listing 10 ist auch zusehen, wie ein Feld als Pflichtfeld markiert werden kann. Das führt dazu, dass die Komponente entsprechend markiert wird. Um nun ein Objekt zu binden, muss die Methode setBean() vom Binder aufgerufen werden. Mit der Methode validate() kann geprüft werden, ob das bearbeitete Objekt gültige Werte aufweist. Hat es ungültige Werte, werden die jeweiligen Komponenten markiert und die Validierungsnachricht ausgegeben.
Binder<EmployeeRecord> binder = new Binder<>();
TextField id = new TextField("ID");
binder.forField(id)
.withConverter(new StringToIntegerConverter(
"Darf nicht leer sein"))
.bind(EmployeeRecord::getId, null);
TextField name = new TextField("Name");
binder.forField(name)
.asRequired()
.withValidator(s -> s.length() >=3, "Name ist zu kurz")
.bind(EmployeeRecord::getName, EmployeeRecord::setName);
Integration
Vaadin bietet Integrationen für Spring Boot und Jakarta EE. In dem Fall wird eine Route automatisch zu einer Spring Bean oder zu einer CDI Bean. Vaadin kennt in beiden Integrationen einen wichtigen zusätzlichen Scope beziehungsweise Context. Der UIScope ist gültig, solange der Benutzer die Applikation im Browser geöffnet hat. Damit kann die gleiche Applikation mehrfach unabhängig im gleichen Browser gestartet werden. Die Applikationen können als Web-Archive (WAR) in einem Applikationsserver deployed oder als Spring Boot-Applikation standalone betrieben werden.
Testing
Da der UI-Code vollständig in Java geschrieben wird, kann dieser einfach mit Unittests getestet werden. Die kommerzielle Version enthält die Vaadin TestBench, ein Tool zum Erstellen und Ausführen von browserbasierten Integrationstests. Unter der Haube verwendet Test-Bench Selenium mit WebDriver. Einen Test zum „Hello World”-Beispiel vom Anfang sieht wie in Listing 11 aus. Vaadin stellt eine jQuery-ähnliche Syntax zur Verfügung, um auf die Elemente zuzugreifen.
@Test
public void showHelloJava() {
TextFieldElement textField =
$(TextFieldElement.class).first();
textField.setValue("JavaSpektrum");
textField.sendKeys(Keys.ENTER);
LabelElement label = $(LabelElement.class).first();
Assert.assertEquals("Hello, JavaSpektrum", label.getText());
}
Fazit
Viele Anwendungen, die heute mit JavaScript-Frameworks als SPA implementiert werden, bieten unwesentlich mehr Funktionalität als simples Create, Read, Update und Delete (CRUD). Dafür ist der Aufwand, eine Client/Server-Applikation zu entwickeln, sehr hoch. Entweder muss eine Trennung zwischen Frontend- und Backend-Team erfolgen oder sogenannte Fullstack-Entwickler müssen beide Technologiestacks gut beherrschen.
Mit Vaadin steht ein Webframework bereit, mit dem Java-Entwickler rasch und effizient sehr ansprechende Webapplikationen entwickeln können. Insbesondere der Umstand, dass kein REST-API benötigt wird, bedeutet eine große Einsparung bei der Entwicklung und Wartung der Applikation. In der Datenzugriffsschicht kann mit jOOQ die Datenbank effizient und typsicher angebunden werden. Es entfällt der Umweg über ein O/R-Mapping, der in vielen Fällen nicht nur unnötig ist, sondern verhindert, dass der volle Funktionsumfang der Datenbank genutzt werden kann. In dem Sinne: KISS (Keep it simple and stupid)!
Literatur und Links
[Atmosphere]
https://github.com/Atmosphere/atmosphere
[Code]
Code zu den Beispielen, https://github.com/72services/vaadin-jooq-demo
[GWT]
http://www.gwtproject.org/
[jOOQ]
https://www.jooq.org/
[PWA]
https://de.wikipedia.org/wiki/Progressive_Web_App
[Vaadin]
https://vaadin.com
[WebComponents]
https://www.webcomponents.org/