Web-UI für den URL Shortener

Sven Ruppert

1. Einleitung und Zielsetzung

Die ersten beiden Teile dieser Serie haben die theoretischen und praktischen Grundlagen eines URL-Shorteners in reinem Java gelegt – von der semantischen Einordnung kurzer URLs über die Architektur eines robusten Mappingsystems bis hin zur konkreten Implementierung eines REST-basierten Dienstes auf Basis des JDK-HTTP-Servers. Damit steht ein funktionales, modular erweiterbares Backend zur Verfügung, das Kurzlinks erzeugt, verwaltet und effizient auflöst. Was bisher jedoch fehlt, ist eine visuelle Schnittstelle, über die Nutzer direkt mit dem System interagieren können – sei es zur manuellen Erzeugung von Kurzlinks, zur Einsicht bestehender Mappings oder zur gezielten Analyse einzelner Weiterleitungen.

Genau hier setzt der dritte Teil dieser Serie an. Im Mittelpunkt steht die Entwicklung einer grafischen Benutzeroberfläche auf Basis von Vaadin Flow. Ziel ist es, eine vollständig in Java entwickelte Weboberfläche bereitzustellen, die ohne clientseitige Frameworks auskommt und sich nahtlos in die bestehende WAR-Struktur der Anwendung einfügt. Die Entscheidung für Vaadin Flow folgt dabei einem pragmatischen Leitmotiv: Wer bereits Java schreibt, soll auch Oberflächen in Java definieren können – mit typsicherem Layout, deklarativem Komponentenmodell und klarer Trennung zwischen UI- und Domänenschicht.

Neben der funktionalen Erweiterung verfolgt die UI-Integration auch eine didaktische Zielsetzung. Denn gerade in sicherheitsrelevanten Anwendungen – wie es URL-Shortener potenziell sind – spielt die menschliche Interaktion eine entscheidende Rolle. Eine gute UI ermöglicht nicht nur komfortable Bedienung, sondern verhindert durch geeignete Validierung, klare Rückmeldungen und restriktive Rechtekonzepte auch typische Fehlbedienungen oder Missbrauch. Damit wird die Benutzeroberfläche selbst zu einem sicherheitsrelevanten Teil der Architektur.

Im weiteren Verlauf dieses Artikels werden wir die Struktur, das Design und die konkrete Umsetzung der Vaadin-Oberfläche detailliert beleuchten. Angefangen bei der Modularchitektur über die Navigation und View-Komponenten bis hin zu erweiterten Funktionen wie QR-Code-Generierung oder Ablaufzeitanzeige. Dabei bleibt der Anspruch bestehen, sämtliche Funktionalität ausschließlich mit Bordmitteln von Java umzusetzen – ganz im Sinne eines transparenten, wartbaren und erweiterbaren Systemdesigns.

Die UI ist somit keine bloße Zugabe, sondern ein zentraler Bestandteil einer ganzheitlichen Shortener-Lösung – sowohl für produktiven Einsatz als auch für Lehre und Exploration.

2. UI-Strategie und Technologiewahl

Die Wahl der geeigneten Technologie für die Benutzeroberfläche eines technischen Dienstes ist weit mehr als eine Geschmacksfrage – sie berührt zentrale Aspekte wie Wartbarkeit, Sicherheitsmodell, Entwicklungseffizienz und Deploymentstrategie. Im Kontext unseres URL-Shorteners, der bewusst auf externe Frameworks verzichtet und vollständig in Java 24 umgesetzt wurde, lag es nahe, auch für die UI eine Lösung zu wählen, die sich nahtlos in diese Philosophie einfügt. Die Entscheidung fiel daher auf Vaadin Flow, ein serverseitiges UI-Framework, das vollständig in Java betrieben und entwickelt wird.

2.1 Warum Vaadin Flow für eine serverseitige Oberfläche?

Vaadin Flow ermöglicht die Entwicklung komponentenbasierter Webanwendungen in Java – ohne JavaScript, ohne Frontend-Buildprozess und ohne manuelle Pflege von HTML-, CSS- oder TypeScript-Artefakten. Alle UI-Komponenten, von einfachen Eingabefeldern bis hin zu komplexen Tabellen, werden deklarativ in Java beschrieben, wodurch sich die UI direkt in die bestehende Modularstruktur einfügt. Für eine rein Java-basierte Backendlogik wie unseren URL-Shortener ergibt sich daraus eine kohärente Gesamtarchitektur, die ohne Sprach- oder Kontextwechsel auskommt.

Die Ausführung der Logik erfolgt vollständig serverseitig: Benutzerinteraktionen lösen Ereignisse aus, die auf dem Server verarbeitet werden, wobei die Darstellung im Browser über den Vaadin-Client synchronisiert wird. Diese Architektur vereinfacht nicht nur die Zustandsverwaltung, sondern reduziert auch potenzielle Angriffsflächen, da keine serverseitige Geschäftslogik ins Frontend wandert.

2.2 UI-Paradigmen für Administrationssysteme

Bei der Gestaltung einer Admin-Oberfläche geht es nicht um ästhetische Brillanz, sondern um funktionale Klarheit, Interaktionsrobustheit und Datenverantwortung. Die Nutzer eines URL-Shorteners – sei es im Selbsthosting, in Bildungseinrichtungen oder innerhalb einer Organisation – benötigen eine einfache Möglichkeit, neue URLs manuell zu kürzen, bestehende Mappings zu durchsuchen, Ablaufzeiten zu prüfen und eventuell kritische Einträge zu löschen. Diese Funktionen müssen sichtbar, nachvollziehbar und jederzeit abbrechbar sein.

Vaadin Flow bietet hierfür eine Vielzahl vorgefertigter Komponenten wie Grid, FormLayout, Dialog, Notification oder Binder, mit denen sich solche Anwendungsfälle effizient umsetzen lassen – inklusive Validierung, Eventsteuerung und dynamischer Datenbindung. Damit wird nicht nur Entwicklungszeit gespart, sondern auch eine konsistente Benutzererfahrung ermöglicht, die sich mit wachsender Komplexität elegant skalieren lässt.

2.3 Abgrenzung zu clientseitigen Frameworks

Im Gegensatz zu Single-Page-Application-Frameworks wie Angular, React oder Vue erfordert Vaadin Flow keine separate Frontend-Build-Pipeline, keine TypeScript-Toolchains und keine JSON-Serialisierung der Serverkommunikation. Die gesamte Anwendung – inklusive UI – lässt sich als klassische WAR-Datei paketieren und in einem Servlet-Container wie Jetty oder Tomcat betreiben. Dadurch reduziert sich nicht nur der DevOps-Aufwand, sondern auch das Risiko von Versionskonflikten, CDN-Ausfällen oder Cross-Origin-Problemen.

Diese serverseitige Herangehensweise eignet sich besonders für Anwendungen, bei denen Datenhoheit, Sicherheit und langfristige Wartbarkeit im Vordergrund stehen – etwa bei internen Tools, Admin-Konsolen oder sicherheitskritischen Diensten. Auch die Integration von Session-basierten Sicherheitsmechanismen, IP-Filtern oder rollenbasiertem Zugriff gestaltet sich in einem Vaadin-Servlet weitaus kontrollierter als bei Headless-Backends mit externem JavaScript-Frontend.

Die Wahl von Vaadin Flow ist damit nicht nur eine technologische Entscheidung, sondern Ausdruck eines Architekturstils: stabil, serverzentriert, Java-durchgängig – ideal für sichere, wartbare und erklärbare Systeme.

3. Architektur der Benutzeroberfläche

Die Benutzeroberfläche ist keine isolierte Komponente, sondern integraler Bestandteil des Systems. Sie muss sich sauber in die bestehende Modularchitektur einfügen, ohne dabei fachliche Abhängigkeiten zu erzwingen oder die lose Kopplung zwischen Kernlogik und Anwendungsschichten aufzugeben. In einer rein Java-basierten Umgebung ohne Frameworks wie Spring oder Jakarta EE kommt dieser Trennung eine besondere Bedeutung zu – denn sie erlaubt es, die UI sowohl technisch als auch semantisch als eigenständige Schicht zu betrachten.

3.1 Integration in die bestehende WAR-Struktur

Die Anwendung ist bereits als mehrgliedriges Maven-Projekt organisiert, das in einem zentralen WAR-Modul zusammengeführt wird. Dieses WAR-Modul kennt den Servlet-Kontext, initialisiert den HTTP-Server (für REST) sowie das VaadinServlet (für die UI), und übernimmt damit die Rolle der Integrations- und Auslieferungseinheit. Die bisherigen Module shortener-core (Domäne) und shortener-api (HTTP-Routing) bleiben hiervon unberührt.

Das neue UI-Modul – shortener-ui-vaadin – wird als zusätzliches Maven-Modul implementiert, das ausschließlich von shortener-core abhängt. Es enthält sämtliche Vaadin-spezifische Logik, UI-Komponenten und Views. Im WAR-Modul wird dieses Modul dann durch eine Servlet-Registrierung angebunden – typischerweise über VaadinServlet, das im Deployment-Deskriptor (web.xml) oder via ServletContainerInitializer eingebunden wird.

Der Aufbau folgt dem Prinzip: Nur das WAR-Modul kennt die konkrete Deployment-Topologie. Alle anderen Module bleiben generisch und unabhängig.

3.2 Trennung von Routing, Views und Services

Innerhalb des UI-Moduls erfolgt eine saubere Aufteilung der Zuständigkeiten:

  • Routing und Navigation werden zentral über eine MainLayout-Klasse und mit @Route-annotierten Views organisiert. Diese definieren die Pfade innerhalb der UI (z. B. /, /create, /admin) und übernehmen automatisch das Session- und History-Management von Vaadin.
  • Views (etwa CreateView, OverviewView, StatsView) bilden die interaktiven Oberflächen. Sie binden Vaadin-Komponenten wie TextField, Grid, Button, Dialog und stellen die Benutzerinteraktion bereit. Jede View ist zustandslos und delegiert an Services.
  • Services kapseln die Interaktion mit der fachlichen Logik. Dabei handelt es sich nicht um typische Spring-Beans, sondern um reguläre Java-Klassen, die entweder direkt oder per Singleton-Pattern (ServiceRegistry, StaticFactory) instanziiert werden. Sie rufen Methoden aus shortener-core auf und übernehmen Konvertierung, Validierung und Fehlerbehandlung.

Diese Struktur erlaubt eine klare Trennung zwischen Darstellung, Anwendungslogik und Geschäftslogik. Sie orientiert sich explizit an klassischen UI-Architekturmustern wie Model-View-Service (MVS), ohne den Overhead oder die Magie vollwertiger Frameworks.

3.3 Kommunikation mit dem Core-Modul (DI ohne Frameworks)

Da auf Dependency-Injection-Container verzichtet wird, erfolgt die Bereitstellung der Views mit Service-Instanzen manuell oder über zentrale Klasse zur Bereitstellung. Ein typischer Ansatz ist die Implementierung eines UiServiceLocator, der alle benötigten Komponenten (z. B. UrlMappingStore, ShortCodeGenerator) einmalig erzeugt und dann als Singleton verfügbar macht.

Beispielsweise kann der Konstruktor einer View folgendermaßen aussehen:

public class OverviewView extends VerticalLayout {
  private final UrlMappingStore store = UiServiceLocator.getUrlMappingStore();
  public OverviewView() {
    // Komponenten aufbauen, Daten anzeigen, Aktionen definieren
  }
}

Diese Form der expliziten Abhängigkeitserzeugung hat den Vorteil, dass die Herkunft aller Komponenten nachvollziehbar bleibt, keine versteckte Initialisierung stattfindet und der gesamte Anwendungskontext transparent kontrollierbar bleibt – eine Eigenschaft, die gerade in sicherheitskritischen Systemen von zentraler Bedeutung ist.

4. Benutzerführung und UX-Design

Eine Administrationsoberfläche ist kein klassisches Frontend für Endnutzer, sondern ein spezialisiertes Werkzeug – vergleichbar mit einem Präzisionsinstrument. Ihre Benutzerführung muss effizient, robust und erklärbar sein. Im Gegensatz zu dynamischen Consumer-UIs, die durch Animation, Branding oder komplexe Interaktionen glänzen, zählen hier vor allem Übersichtlichkeit, Vorhersagbarkeit und Redundanzfreiheit. Die Gestaltung einer Vaadin-Oberfläche für einen URL-Shortener folgt daher einer minimalistischen, funktionszentrierten UX-Strategie.

4.1 Navigationskonzept: Dashboard, Erstellen, Übersicht

Die Anwendung gliedert sich aus Sicht der Nutzer in drei zentrale Aufgabenbereiche:

  • Erstellen neuer Kurzlinks: Eine einfache Eingabemaske mit Validierung für Ziel-URLs und optionalem benutzerdefiniertem Alias. Hier stehen Geschwindigkeit und Klarheit im Vordergrund.
  • Übersicht vorhandener Mappings: Eine tabellarische Darstellung aller generierten Shortlinks, ergänzt um Sortier- und Filtermöglichkeiten. Dies erlaubt gezielte Suche nach Alias, Zieladresse oder Ablaufdatum.
  • Administratives Dashboard: Ein Bereich zur Einsicht von Metriken (z. B. Anzahl der Mappings, häufig genutzte Kurzlinks) sowie zur gezielten Löschung oder Verlängerung von Einträgen.

Die Navigation erfolgt über ein Vaadin-Layout mit Hauptmenü (z. B. AppLayout oder DrawerToggle) und klar benannten Routen. Alle Routen sind per URL direkt erreichbar, wobei Bookmarks und Refreshs stets zu einem konsistenten Zustand führen – ein inhärenter Vorteil von Vaadin Flow durch serverseitige Sessionhaltung und deklaratives Routing.

4.2 Eingabevalidierung und Rückmeldungen

Ein zentrales UX-Kriterium ist die Korrektheit der Eingaben. Beim Erstellen eines neuen Mappings muss sichergestellt werden, dass

  • das Eingabefeld für die URL nicht leer ist,
  • die URL syntaktisch gültig ist (inkl. http/https),
  • optionale Aliase nicht bereits vergeben oder reserviert sind.

Diese Validierungen erfolgen sofort bei Eingabe (clientseitig über Binder) und nochmals serverseitig im Service-Layer, wobei die View entsprechende Fehlermeldungen als Notification, ErrorMessage oder direkt im Feldkontext anzeigt. Ein bewusstes UX-Detail: Bei Fehlern bleibt der Fokus auf dem Feld, die bisherige Eingabe wird nicht gelöscht, und es erfolgt eine visuelle Hervorhebung – statt eines allgemeinen Fehldialogs.

Erfolgreiche Vorgänge (z. B. erfolgreich erstellter Kurzlink) führen zu klaren Erfolgsmeldungen inklusive sofortiger Kopiermöglichkeit, z. B. per Button mit Icon („in Zwischenablage kopieren“), um medienübergreifende Nutzung zu unterstützen.

4.3 Lesbarkeit, Designsystem und Barrierefreiheit

Die UI ist funktional, nicht dekorativ. Dementsprechend wird auf ein klares visuelles Raster geachtet: ausreichend Weißraum, konsistente Beschriftungen, verständliche Icons und keine unnötigen Animationen. Farben dienen ausschließlich der semantischen Markierung (z. B. Rot für Löschaktionen, Grün für Erfolgsmeldungen), nicht der stilistischen Individualisierung.

Vaadin Flow bringt ein solides Standarddesign mit, das sich über @Theme oder eigene CSS-Klassen erweitern lässt – dies jedoch gezielt und nur dort, wo es funktional begründet ist. Die gesamte Anwendung bleibt vollständig tastaturbedienbar, Bildschirmlesegeräte-kompatibel und kontrastreich genug für barrierearme Nutzung. Das ist insbesondere in Kontexten wichtig, wo Admins aus sicherheitskritischen Bereichen mit erhöhten Accessibility-Anforderungen arbeiten.

5. Implementierung der UI-Komponenten mit Vaadin Flow (inkl. Quelltext)

5.1 MainLayout: Navigationsrahmen und Routing

Das MainLayout bildet die gemeinsame Struktur aller Views. Es basiert auf AppLayout und definiert ein vertikales Navigationsmenü mit Links zu den zentralen Ansichten:

@Theme("shortener-theme")
public class MainLayout extends AppLayout {

    public MainLayout() {
        DrawerToggle toggle = new DrawerToggle();
        H1 title = new H1("URL Shortener Admin");
        title.getStyle().set("font-size", "var(--lumo-font-size-l)")
                         .set("margin", "0");
        HorizontalLayout header = new HorizontalLayout(toggle, title);
        header.setDefaultVerticalComponentAlignment(Alignment.CENTER);
        header.setWidthFull();
        header.setPadding(true);
        addToNavbar(header);
        RouterLink createLink = new RouterLink("Erstellen", CreateView.class);
        RouterLink overviewLink = new RouterLink("Übersicht", OverviewView.class);
        VerticalLayout menu = new VerticalLayout(createLink, overviewLink);
        menu.setPadding(false);
        menu.setSpacing(false);
        menu.setSizeFull();
        addToDrawer(menu);
    }
}

Diese Struktur trennt Navigationslogik von Anwendungslogik. Neue Views müssen lediglich über @Route(…, layout = MainLayout.class) angebunden werden.

5.2 CreateView: Eingabeformular mit Validierung

Die View zur Erzeugung neuer Kurzlinks besteht aus einem TextField für die Ziel-URL, einem optionalen Alias-Feld sowie einem Button zur Erzeugung des Mappings. Einfache Validierungslogik wird über den Binder bereitgestellt:

@Route(value = "", layout = MainLayout.class)
public class CreateView extends VerticalLayout {
    private final UrlMappingStore store = UiServiceLocator.getUrlMappingStore();
    public CreateView() {
        setSpacing(true);
        setPadding(true);
        TextField urlField = new TextField("Ziel-URL");
        urlField.setWidthFull();
        TextField aliasField = new TextField("Alias (optional)");
        aliasField.setWidth("300px");
        Button shortenButton = new Button("Verkürzen");
        shortenButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        HorizontalLayout actions = new HorizontalLayout(shortenButton);
        actions.setAlignItems(Alignment.END);
        Binder<ShortenRequest> binder = new Binder<>(ShortenRequest.class);
        ShortenRequest request = new ShortenRequest();
        binder.forField(urlField)
              .asRequired("URL darf nicht leer sein")
              .withValidator(url -> url.startsWith("http://") || url.startsWith("https://"), 
                             "Nur HTTP(S)-URLs erlaubt")
              .bind(ShortenRequest::url, ShortenRequest::url);
        binder.forField(aliasField)
              .bind(ShortenRequest::alias, ShortenRequest::alias);
        shortenButton.addClickListener(click -> {
            if (binder.writeBeanIfValid(request)) {
                Optional<String> code = createShortCode(request);
                code.ifPresentOrElse(c -> {
                    Notification.show("Kurzlink erstellt: " + c);
                    urlField.clear();
                    aliasField.clear();
                }, () -> Notification.show("Alias bereits vergeben oder Fehler beim Speichern", 3000, Position.MIDDLE));
            }
        });
        add(new H2("Neuen Kurzlink erstellen"), urlField, aliasField, actions);
    }

    private Optional<String> createShortCode(ShortenRequest req) {
        try {
            return Optional.ofNullable(
                req.alias() == null || req.alias().isBlank()
                ? store.createMapping(req.url()).shortCode()
                : store.createCustomMapping(req.alias(), req.url()).shortCode()
            );
        } catch (IllegalArgumentException e) {
            return Optional.empty();
        }
    }

    private record ShortenRequest(String url, String alias) {
        public ShortenRequest() { this("", ""); }
        public String url() { return url; }
        public String alias() { return alias; }
    }
}

Diese View kapselt die komplette Benutzerinteraktion für das Erstellen neuer Kurzlinks. Fehlerhafte Eingaben führen zu sofortiger Rückmeldung, gültige werden persistiert und direkt als Rückmeldung ausgegeben.

5.3 OverviewView: Grid-basierte Verwaltungsansicht

Die OverviewView zeigt alle vorhandenen Mappings im Grid. Optional können hier zusätzliche Spalten wie Erstellungszeitpunkt oder Ablaufdatum eingeblendet werden. Jede Zeile erlaubt Löschaktionen über einen Bestätigungsdialog:

@Route(value = "overview", layout = MainLayout.class)
public class OverviewView extends VerticalLayout {

    private final UrlMappingStore store = UiServiceLocator.getUrlMappingStore();
    private final Grid<ShortUrlMapping> grid = new Grid<>(ShortUrlMapping.class, false);

    public OverviewView() {
        setSizeFull();
        setPadding(true);
        setSpacing(true);
        grid.addColumn(ShortUrlMapping::shortCode).setHeader("Short Code");
        grid.addColumn(ShortUrlMapping::originalUrl).setHeader("Original URL").setFlexGrow(1);
        grid.addColumn(m -> m.createdAt().toString()).setHeader("Erstellt am");
        grid.addComponentColumn(this::buildActionButtons).setHeader("Aktionen");
        grid.setItems(store.findAll());
        grid.setSizeFull();
        add(new H2("Alle Kurzlinks"), grid);
    }

    private Component buildActionButtons(ShortUrlMapping mapping) {
        Button delete = new Button("Löschen", e -> openConfirmDialog(mapping));
        delete.addThemeVariants(ButtonVariant.LUMO_ERROR);
        return delete;
    }

    private void openConfirmDialog(ShortUrlMapping mapping) {
        Dialog dialog = new Dialog();
        dialog.add(new Text("Kurzlink wirklich löschen? (" + mapping.shortCode() + ")"));
        Button confirm = new Button("Ja", e -> {
            store.delete(mapping.shortCode());
            grid.setItems(store.findAll());
            dialog.close();
            Notification.show("Kurzlink gelöscht.");
        });
        confirm.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        Button cancel = new Button("Abbrechen", e -> dialog.close());
        dialog.add(new HorizontalLayout(confirm, cancel));
        dialog.open();
    }
}

Die Aktualisierung des Grid erfolgt nach Löschaktionen automatisch. Zusätzliche Filter- oder Exportfunktionen können auf Basis dieser Struktur einfach ergänzt werden.

5.4 Einheitliches Verhalten für Dialoge und Notifications (mit Code)

In einer Verwaltungsoberfläche für einen sicherheitsrelevanten Dienst wie einen URL-Shortener ist die Art und Weise, wie das System mit Benutzerinteraktionen umgeht, von zentraler Bedeutung. Die Reaktion auf Eingaben – seien es korrekte, fehlerhafte oder kritische – muss vorhersehbar, erklärbar und kontextgerecht erfolgen. Besonders wichtig sind dabei:

  • sofortige, nicht-blockierende Erfolgsmeldungen für valide Aktionen,
  • präzise, feldnahe Fehleranzeigen bei fehlerhaften Eingaben,
  • absichtsbestätigende Dialoge bei potenziell destruktiven Operationen wie dem Löschen von Mappings.

Diese drei Rückmeldetypen lassen sich vollständig mit Bordmitteln von Vaadin Flow realisieren – konkret: Notification, Dialog und Komponentenbindung über Binder.

Erfolgsmeldungen via Notification

Nach erfolgreichen Aktionen (z. B. dem Erstellen eines Shortlinks oder dem Löschen eines Eintrags) wird eine dezente, automatisch verschwindende Notification angezeigt. Diese ist nicht-blockierend, informiert eindeutig über den Erfolg und lässt die Benutzer nahtlos weiterarbeiten.

Notification.show(“Kurzlink erfolgreich erstellt.”, 3000, Notification.Position.TOP_CENTER);

Bei gezielter Rückmeldung mit Inhalt (z. B. dem erzeugten Kurzcode) kann alternativ eine Inline-Komponente oder ein kopierbarer Textblock angezeigt werden:

var shortCodeField = new TextField("Ihr Kurzlink");
shortCodeField.setValue("https://short.ly/" + code);
shortCodeField.setReadOnly(true);
shortCodeField.setWidthFull();
add(shortCodeField);
Notification.show("Sie können den Link jetzt kopieren.");

Fehlermeldungen durch feldnahe Validierung mit Binder

Fehlgeschlagene Eingaben – etwa ungültige URLs oder doppelt vergebene Aliase – müssen nicht in allgemeinen Dialogen, sondern direkt beim betroffenen Feld angezeigt werden. Dazu verwendet Vaadin den Binder<T>-Mechanismus, der Validierungsregeln direkt an UI-Komponenten bindet:

Binder<ShortenRequest> binder = new Binder<>(ShortenRequest.class);
binder.forField(urlField)
    .asRequired("URL darf nicht leer sein")
    .withValidator(this::isValidHttpUrl, "Nur gültige HTTP(S)-URLs erlaubt")
    .bind(ShortenRequest::url, ShortenRequest::url);

Die Methode isValidHttpUrl prüft dabei beispielsweise mit einem simplen Regex oder URI.create(…)-Logik auf syntaktische Gültigkeit. Fehler werden automatisch als Fehlermeldung direkt unter dem Feld eingeblendet – visuell hervorgehoben und fokussiert:

private boolean isValidHttpUrl(String url) {
    return url != null && (url.startsWith("http://") || url.startsWith("https://"));
}

Kommt es zu einem Fehler auf Anwendungsebene (z. B. Alias bereits vergeben), kann dies zusätzlich über eine globale Notification kommuniziert werden:

Notification.show("Alias bereits vergeben", 3000, Notification.Position.MIDDLE)
            .addThemeVariants(NotificationVariant.LUMO_ERROR);

Destruktive Aktionen nur nach expliziter Bestätigung

Beim Löschen eines Kurzlinks, dem Zurücksetzen eines Zählers oder ähnlichen nicht umkehrbaren Aktionen darf die Operation niemals ohne Bestätigung erfolgen. Dafür eignet sich Vaadins Dialog-Komponente, kombiniert mit einem beschreibenden Text und zwei expliziten Buttons.

Beispiel aus der OverviewView:

private void openConfirmDialog(ShortUrlMapping mapping) {
    Dialog dialog = new Dialog();
    dialog.setHeaderTitle("Löschen bestätigen");
    Span message = new Span("Soll der Kurzlink wirklich gelöscht werden? (" + mapping.shortCode() + ")");
    dialog.add(message);
    Button confirm = new Button("Löschen", click -> {
        store.delete(mapping.shortCode());
        grid.setItems(store.findAll());
        dialog.close();
        Notification.show("Kurzlink gelöscht.", 3000, Notification.Position.TOP_CENTER)
                    .addThemeVariants(NotificationVariant.LUMO_SUCCESS);
    });
    Button cancel = new Button("Abbrechen", event -> dialog.close());
    cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
    dialog.getFooter().add(cancel, confirm);
    dialog.open();
}

Merkmale dieser Umsetzung:

  • Die Aktion ist durch einen expliziten Button ausgelöst.
  • Die Bestätigung erfolgt dialoggestützt mit Cancel-Option.
  • Die Änderung wird nach Abschluss sofort reflektiert.
  • Die Rückmeldung erfolgt unmittelbar per Notification.

Zusammenspiel und Wiederverwendung

Ein zentrales UX-Muster dieser Oberfläche besteht darin, das Verhalten aller Interaktionen zu standardisieren. Dadurch entstehen keine Überraschungen für die Benutzer. Ein konsistentes Verhalten bedeutet:

AktionReaktion
URL erfolgreich gekürztNotification, Anzeige des Kurzlinks
Fehlerhafte EingabeFeldmarkierung, Inline-Fehlermeldung
Alias bereits vergebenzentrale Notification mit Fehlfarbgebung
Kurzlink löschenDialog mit zwei Buttons
Bestätigtes LöschenNotification, Grid wird neu geladen

Diese Muster lassen sich komponentenbasiert kapseln – etwa durch Utility-Methoden für Dialog-Erzeugung oder Notification-Fabriken, die später auch für Logging oder Internationalisierung angepasst werden können.

7. Deployment der UI im WAR-Kontext

Die Auslieferung der Benutzeroberfläche erfolgt nicht als separate Anwendung, sondern integriert in eine zentrale WAR-Datei, die sowohl die REST-API als auch die Vaadin-basierte UI enthält. Dieser monolithische Ansatz ist bewusst gewählt: Er ermöglicht ein konsistentes Deployment-Modell auf Servlet-Containern wie Jetty, Tomcat oder Undertow, ohne auf komplexe Build-Pipelines, Containerisierung oder Framework-Integration angewiesen zu sein.

7.1 Servlet-Konfiguration für Vaadin in web.xml

Vaadin Flow wird klassisch über das Servlet com.vaadin.flow.server.VaadinServlet registriert. Da keine automatische Konfiguration (wie bei Spring Boot) erfolgt, wird die Zuordnung explizit im Deployment-Deskriptor vorgenommen:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         version="4.0">
    <!-- Vaadin UI -->
    <servlet>
        <servlet-name>VaadinServlet</servlet-name>
        <servlet-class>com.vaadin.flow.server.VaadinServlet</servlet-class>
        <init-param>
            <param-name>ui</param-name>
            <param-value>shortener.ui.MainUI</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>VaadinServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

Der Eintrag /* als URL-Muster bewirkt, dass alle nicht durch die REST-API belegten Pfade (z. B. /shorten, /abc123) von der UI übernommen werden. Die REST-Endpunkte können weiterhin über explizite HttpServlet-Registrierungen mit Vorrang angesprochen werden.

Alternativ ist eine manuelle Aufteilung über Filter oder explizite Ausschlusslisten möglich, wenn z. B. /api/* strikt dem REST-Zweig vorbehalten bleiben soll.

7.2 Theme-Integration ohne Frontend-Toolchain

Ein großer Vorteil von Vaadin Flow im Servermodus ist der Verzicht auf Node.js, npm oder Webpack. Alle Komponenten und Ressourcen werden auf JVM-Ebene behandelt und zur Laufzeit als Servlet-Ressourcen ausgeliefert. Themes lassen sich direkt im Classpath definieren:

@Theme(value = "shortener-theme")
public class MainLayout extends AppLayout { ... }

Im Verzeichnis frontend/themes/shortener-theme/ liegen dann:

  • styles.css – eigene Anpassungen
  • theme.json – Metadaten und Komponentenbindung

Das Maven-Plugin vaadin-maven-plugin sorgt bei package oder install automatisch für die Generierung des entsprechenden Frontend-Bundles – vollständig integriert in den WAR-Build, ohne externe Tools.

Ein typischer Maven-Ausschnitt im shortener-war/pom.xml:

<plugin>
    <groupId>com.vaadin</groupId>
    <artifactId>vaadin-maven-plugin</artifactId>
    <version>${vaadin.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-frontend</goal>
                <goal>build-frontend</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Die Verwendung von build-frontend ist dabei nur beim ersten Deployment oder bei Theme-Änderungen erforderlich. Im Betrieb wird alles aus dem WAR geladen.

7.3 Betrieb im Tomcat oder Jetty

Die resultierende WAR-Datei lässt sich in jedem Servlet 4.0+ kompatiblen Container deployen. Empfehlenswert sind:

  • Jetty 12 für Embedded-Deployments oder lokale Tests
  • Apache Tomcat 10.1+ für produktionsnahe Szenarien
  • Eclipse Servlet Container (z. B. Open Liberty) für modulare Anwendungen

Das Deployment ist denkbar einfach:

cp target/shortener.war $CATALINA_BASE/webapps/

Nach dem Start des Containers ist die Anwendung unter dem üblichen Pfad erreichbar:

http://localhost:8080/shortener/

REST-Endpunkte (z. B. POST /shorten) und UI-Routen (/overview, /create) sind koexistent verfügbar. Damit ergibt sich ein robustes, klar kontrolliertes Deploymentmodell:

  • Kein Framework-Magic
  • Kein Container Lock-in
  • Keine Buildkomplexität

8. Fazit und Ausblick

Mit der Integration einer Vaadin-Flow-basierten Oberfläche erhält unser URL-Shortener nicht nur eine interaktive Bedienkomponente, sondern eine vollständige, wartbare und produktionsfähige Verwaltungsschicht – realisiert ausschließlich mit Mitteln des JDK. Die grafische UI dient dabei nicht als schmückendes Beiwerk, sondern als gleichberechtigter Bestandteil der Systemarchitektur. Sie ermöglicht die sichere Erzeugung, Analyse und Pflege von Kurzlinks – typischerweise über dieselbe Infrastruktur, die auch für REST-Endpunkte oder Hintergrundprozesse verwendet wird.

Die Entscheidung, ausschließlich Core Java und Vaadin Flow einzusetzen, hat sich als tragfähig erwiesen: Die Trennung in Modulgrenzen bleibt klar, das Deployment erfolgt als klassische WAR-Datei, und alle Interaktionen – von Validierungen über Feedbacklogik bis hin zu Rechtemodellen – lassen sich nachvollziehbar und transparent umsetzen. Statt Framework-Magie, Konvention-over-Configuration oder Annotation-basiertem Autowiring steht hier wieder das verstehbare Systemdesign im Zentrum.

Besonders hervorzuheben ist die Effizienz, mit der Vaadin Flow UI-Logik direkt im Java-Kontext abbildet. Entwickler:innen behalten die Kontrolle über Zustand, Zugriffsschutz und Lebenszyklen – ohne dabei auf Build-Toolchains, Client-Routing oder Third-Party-Bibliotheken angewiesen zu sein. Die damit verbundene Vereinfachung im Wartungs- und Sicherheitsmodell ist im Kontext sicherheitskritischer Infrastruktur von erheblichem Vorteil.

Auch perspektivisch lässt sich das System ohne architektonischen Bruch weiterentwickeln. Mögliche nächste Schritte umfassen:

  • Modularisierung der UI in lesende und schreibende Segmente
  • asynchrone Event-Analyse via Server-Push oder WebSockets
  • Anbindung externer Sicherheitssysteme wie OAuth2 oder LDAP
  • Deployment auf embedded Jetty für minimalistische Distributionen
  • Migration auf native Builds via GraalVM zur Reduktion von Laufzeit-Overhead

Damit eignet sich der URL-Shortener nicht nur als Lernobjekt oder internes Werkzeug, sondern auch als Blaupause für schlanke, robuste und selbstbestimmte Java-Webanwendungen – ohne Vendor-Lock-in, ohne komplexe Tooling-Abhängigkeiten und mit dem Fokus auf Lesbarkeit, Transparenz und Kontrolle.

Happy Coding

Sven

Total
0
Shares
Previous Post

Microfrontends mit Module Federation für bestehende Anwendungen – Ein Erfahrungsbericht

Next Post

Java verbindet seit 30 Jahren Systeme und Communities

Related Posts