Übungsaufgaben zum Anbinden des Backends: Mitfahrgesuche im Hintergrund laden

https://bildung.social/@oerinformatik/

https://oer-informatik.de/javafx-backend-anbinden-uebungsaufgaben

JavaFX, JavaFX Application Thread, Task, Platform.runLater, Nebenläufigkeit, REST-Client, Problem Details, WireMock, Integrationstest, Fehlersuche, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (Bearbeitungszeit insgesamt 56 min, mehrere Aufgaben): Übungen zum Anbinden des Backends in der JavaFX-Desktop-App: Du ordnest Arbeiten dem JavaFX Application Thread oder einem Hintergrund-Thread zu und sagst die Reihenfolge von Ausgaben voraus, lädst die Mitfahrgesuche mit einem Task im Hintergrund, testest einen REST-Client für die Gesuche mit WireMock und findest heraus, warum eine Oberfläche einfriert und warum der erste Reparaturversuch neue Fehler bringt [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über eine Desktop-App mit JavaFX: Übersicht / Kanban-Projektstart / Projekt anlegen / FXML, Layouts und CSS / Properties, Bindings und MVVM / Backend anbinden / TableView für Buchungen / Formulare und Dialoge / Anmeldung mit Keycloak / Verwaltung und Diagramme / GUI-Tests mit TestFX / Abnahmetest und Kanban-Metriken / Paketieren mit jpackage

Diese Übungsaufgaben gehören zum Artikel Backend anbinden: OpenAPI-Client, Task und WireMock in der Desktop-App. Sie spielen im Projekt Mitfahrlabor und führen den Strang der Mitfahrgesuche weiter: Für Ticket JFX-13 „Offene Gesuche an meinem Haltepunkt anzeigen“ hat das Gesuche-Fenster seit den Übungen zu Properties, Bindings und MVVM ein GesucheViewModel, das die Gesuche aus Beispielgesuche filtert. Jetzt sollen die Gesuche aus dem Backend kommen, ohne dass das Fenster dabei hängt.

Überblick: Klick auf Aktualisieren im JavaFX Application Thread startet über das ViewModel einen Task im Hintergrund-Thread; der Task ruft über die RestAngebotQuelle den aus openapi.yaml erzeugten Client auf, der das Backend oder im Test WireMock anfragt; Ergebnis und Ausnahme kommen über onSucceeded und onFailed auf den JavaFX Application Thread zurück (mit KI erstellt)

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Als Startcode dient der Code-Stand src/04-properties-binding-mvvm/uebung-mitfahrgesuche mit Deinem GesucheViewModel aus den letzten Übungen. Eine vollständige Beispiellösung der Aufgaben 2.1 und 2.2 mit allen Tests liegt unter src/05-backend-anbinden/uebung-mitfahrgesuche; dort steht auch der Startcode der RestGesuchQuelle für Aufgabe 2.2. Code-Aufgaben löst Du am besten am Rechner und prüfst sie mit gradlew test (bzw. mvnw test). Die Operatoren (nennen, beschreiben, erläutern, beurteilen …) sind im Artikel Wie sind die Aufgaben aufgebaut? erklärt. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „Anbinden des Backends:
Mitfahrgesuche im Hintergrund laden“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/javafx-backend-anbinden
- https://oer-informatik.de/szenario-mitfahrlabor
- https://oer-informatik.de/javafx-kanban-projektstart
- Stilbeispiel: https://oer-informatik.de/javafx-backend-anbinden-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- JavaFX
- JavaFX Application Thread
- Task
- Platform.runLater
- Nebenläufigkeit
- REST-Client
- Problem Details
- WireMock
- Integrationstest
- Fehlersuche

Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 1.1: Thread-Regeln anwenden (Basiskompetenz)

(ca. 13 min Bearbeitungszeit)

Oksana Melnyk geht mit Dir den Code-Stand von JFX-04 durch, bevor sie das Ticket in der Spalte „Test“ prüft. Sie möchte wissen, ob Du die Regeln des JavaFX Application Thread sicher anwendest.

  1. Gib für jede Arbeit an, ob sie auf dem JavaFX Application Thread (FX) oder in einem Hintergrund-Thread (HG) laufen soll, und nenne zu den Zeilen 3 und 5 den Grund. (4 Punkte)
Welche Arbeit läuft wo?
Nr. Arbeit Thread
1 angeboteApi.sucheAngebote(…) aufrufen
2 statusLabel.setText("3 Fahrtangebote")
3 new Alert(AlertType.ERROR, "Backend nicht erreichbar").show()
4 die ObservableList, an die die ListView gebunden ist, mit setAll(…) füllen
5 Thread.sleep(2000) als Wartezeit vor einem zweiten Versuch
6 eine 3 MB große JSON-Antwort in DTOs umwandeln
  1. Das folgende Programmstück läuft im Handler des Buttons „Aktualisieren“. Die Quelle braucht für ihre Antwort etwa zwei Sekunden. Gib die Ausgaben in der Reihenfolge an, in der sie auf der Konsole erscheinen, und nenne jeweils den Thread, der sie schreibt. (4 Punkte)
System.out.println("A1 " + Thread.currentThread().getName());
Task<List<Angebot>> task = new Task<>() {
    @Override
    protected List<Angebot> call() {
        System.out.println("A2 " + Thread.currentThread().getName());
        return quelle.angeboteLaden();
    }
};
task.setOnSucceeded(e -> System.out.println("A3 " + Thread.currentThread().getName()));
hintergrund.execute(task);   // Executor mit Daemon-Thread "mitfahrlabor-laden"
System.out.println("A4 " + Thread.currentThread().getName());
  1. Ein Teammitglied schlägt vor, statt eines Task einfach Platform.runLater(() -> angebote.setAll(quelle.angeboteLaden())) im Handler aufzurufen, „weil runLater ja für Nebenläufigkeit da ist“. Beschreibe, was beim Klick auf „Aktualisieren“ passiert, und erläutere, wofür Platform.runLater(…) tatsächlich gedacht ist. (3 Punkte)

  2. Nenne drei Eigenschaften, in denen sich ein Task<V> von einem einfachen Runnable unterscheidet, die für das Laden aus dem Backend wichtig sind. (3 Punkte)

  1. Je Zeile 0,5 Punkte, je Begründung 0,5 Punkte:
Welche Arbeit läuft wo? (Lösung)
Nr. Arbeit Thread
1 angeboteApi.sucheAngebote(…) aufrufen HG
2 statusLabel.setText("3 Fahrtangebote") FX
3 new Alert(…).show() FX: ein Alert öffnet ein eigenes Fenster (Stage); aus einem anderen Thread wirft JavaFX IllegalStateException: Not on FX application thread
4 gebundene ObservableList mit setAll(…) füllen FX
5 Thread.sleep(2000) HG: auf dem FX-Thread würde das Fenster zwei Sekunden lang nicht neu gezeichnet und nähme keine Eingaben an
6 3 MB JSON umwandeln HG
  1. Reihenfolge (je Zeile 1 Punkt, Reihenfolge und Thread müssen stimmen):
A1 JavaFX Application Thread
A4 JavaFX Application Thread
A2 mitfahrlabor-laden
A3 JavaFX Application Thread

execute(…) kehrt sofort zurück, deshalb kommt A4 direkt nach A1. A2 kann im Einzelfall schon vor A4 erscheinen, wenn der Hintergrund-Thread sehr schnell startet; beide Reihenfolgen von A2 und A4 sind richtig, wenn das begründet wird. A3 kommt sicher als letzte Ausgabe, zwei Sekunden später, und wieder auf dem JavaFX Application Thread, weil Task die Handler dorthin übergibt.

  1. runLater reiht den Lambda-Ausdruck in die Warteschlange des JavaFX Application Thread ein, er läuft also kurz darauf auf demselben Thread wie der Handler (1 Punkt). Der zwei Sekunden lange Aufruf von angeboteLaden() blockiert dann den FX-Thread genauso, als stünde er direkt im Handler: Das Fenster friert ein (1 Punkt). Platform.runLater(…) ist für den umgekehrten Weg gedacht: Code aus einem fremden Thread übergibt eine kurze Änderung an die Oberfläche an den JavaFX Application Thread (1 Punkt).

  2. Je Eigenschaft 1 Punkt, z. B.:

  • call() liefert ein Ergebnis vom Typ V und darf Ausnahmen werfen; getValue() und getException() stellen beides bereit.
  • setOnSucceeded, setOnFailed und setOnCancelled rufen ihre Handler auf dem JavaFX Application Thread auf, dort darf man die Oberfläche ändern.
  • Properties wie running, message und progress lassen sich an Controls binden; updateMessage(…) und updateProgress(…) übergeben Änderungen selbst an den FX-Thread.
  • cancel() bricht den Task ab und unterbricht einen blockierenden HTTP-Aufruf.

Aufgabe 2.1: Die Mitfahrgesuche im Hintergrund laden (Kernkompetenz)

(ca. 15 min Bearbeitungszeit)

Bisher lädt Dein GesucheViewModel die Gesuche im Konstruktor direkt aus der GesuchQuelle. Mit einer REST-Quelle würde das Gesuche-Fenster beim Öffnen hängen. Ticket JFX-13 bekommt deshalb eine neue Akzeptanzbedingung: „Während die Gesuche laden, ist die Maske gesperrt und der Hinweis meldet ‚Gesuche werden geladen …‘. Kommt ein Fehler, nennt der Hinweis die Meldung des Backends.“ Nach der Board-Regel für „In Arbeit – erledigt“ braucht die neue Anzeige-Logik einen Komponententest im ViewModel.

  1. Baue das GesucheViewModel um: Der Konstruktor bekommt zusätzlich einen Executor und lädt nicht mehr selbst. Eine neue Methode laden() startet einen Task, der die Gesuche aus der Quelle holt; bei Erfolg ersetzt sie die Gesuche, bei einem Fehler merkt sie sich die Meldung. Biete die schreibgeschützten Properties laedt und fehler an und sorge dafür, dass ein zweiter Aufruf von laden() während des Ladens keinen zweiten Task startet. (6 Punkte)

  2. Ergänze das Binding hinweis so, dass es während des Ladens „Gesuche werden geladen …“ und nach einem Fehler „Gesuche nicht geladen: “ liefert; sonst bleibt es wie bisher. Sperre im GesucheController die Maske (GridPane #gesuchMaske), solange geladen wird, und starte das Laden in initialize(). (4 Punkte)

  3. Schreibe zwei JUnit-Tests: Der erste prüft, dass während des Ladens der Hinweis stimmt und ein zweiter Aufruf keinen zweiten Task erzeugt; der zweite prüft den Hinweis nach einem Fehler der Quelle. Erläutere, warum die Tests jetzt die JavaFX-Plattform brauchen und wie Du im Test bestimmst, wann der Task läuft. (6 Punkte)

  1. Die vollständige Klasse steht im Code-Stand src/05-backend-anbinden/uebung-mitfahrgesuche. Die wesentlichen Teile:
private final GesuchQuelle quelle;
private final Executor hintergrund;
private final ReadOnlyBooleanWrapper laedt = new ReadOnlyBooleanWrapper(this, "laedt", false);
private final ReadOnlyStringWrapper fehler = new ReadOnlyStringWrapper(this, "fehler");

public GesucheViewModel(GesuchQuelle quelle, Executor hintergrund) {
    this.quelle = quelle;
    this.hintergrund = hintergrund;
    // Filter- und Hinweis-Bindings wie bisher, siehe b)
}

/** Startet das Laden im Hintergrund; nur auf dem JavaFX Application Thread aufrufen. */
public void laden() {
    if (laedt.get()) {
        return;                                  // läuft schon
    }
    fehler.set(null);
    laedt.set(true);
    Task<List<Gesuch>> aufgabe = new Task<>() {
        @Override
        protected List<Gesuch> call() {
            return quelle.gesucheLaden();        // Hintergrund-Thread
        }
    };
    aufgabe.setOnSucceeded(e -> {                // JavaFX Application Thread
        gesuche.setAll(aufgabe.getValue());
        laedt.set(false);
    });
    aufgabe.setOnFailed(e -> {                   // JavaFX Application Thread
        fehler.set(aufgabe.getException().getMessage());
        laedt.set(false);
    });
    hintergrund.execute(aufgabe);
}

public ReadOnlyBooleanProperty laedtProperty() { return laedt.getReadOnlyProperty(); }
public ReadOnlyStringProperty fehlerProperty() { return fehler.getReadOnlyProperty(); }

Punkte: Executor im Konstruktor, kein Laden im Konstruktor (1), Task mit call() nur für den Aufruf der Quelle (1), Erfolg ersetzt die Liste im Handler (1), Fehler im Handler mit getException() (1), schreibgeschützte Properties über ReadOnly…Wrapper (1), Sperre gegen einen zweiten Task (1). Die Properties ändern sich nur in laden() und in den Handlern, also immer auf dem FX-Thread.

  1. Das Binding bekommt laedt und fehler als zusätzliche Abhängigkeiten (2 Punkte):
hinweis = Bindings.createStringBinding(() -> {
    if (laedt.get()) {
        return "Gesuche werden geladen …";
    }
    if (fehler.get() != null) {
        return "Gesuche nicht geladen: " + fehler.get();
    }
    if (!filterVollstaendig.get()) {
        return "Bitte Haltepunkt und Datum wählen.";
    }
    return passend.isEmpty() ? "Keine offenen Gesuche für diese Auswahl."
            : passend.size() == 1 ? "1 offenes Gesuch"
            : passend.size() + " offene Gesuche";
}, laedt, fehler, filterVollstaendig, passend);

Im Controller (2 Punkte), am Ende von initialize():

gesuchMaske.disableProperty().bind(viewModel.laedtProperty());
viewModel.laden();

Da das ViewModel jetzt einen Executor braucht, erzeugt die MitfahrgesucheApp es selbst (mit einem Daemon-Thread wie im Artikel) und übergibt es über loader.setControllerFactory(typ -> new GesucheController(viewModel)); das ist für die volle Punktzahl nicht nötig.

  1. Tests (je Test 2 Punkte):
@Test
void waehrendDesLadensHinweisUndKeinZweiterTask() throws Exception {
    List<Runnable> wartend = new ArrayList<>();
    GesucheViewModel vm = new GesucheViewModel(Beispielgesuche::offeneGesuche, wartend::add);

    aufFxThread(() -> {
        vm.laden();
        vm.laden();
        return null;
    });

    assertTrue(vm.laedtProperty().get());
    assertEquals("Gesuche werden geladen …", vm.hinweisProperty().get());
    assertEquals(1, wartend.size());
    wartend.get(0).run();      // Task jetzt ausführen
    warten();                  // Handler auf dem FX-Thread abwarten
    assertFalse(vm.laedtProperty().get());
}

@Test
void fehlerDerQuelleErscheintImHinweis() throws Exception {
    GesucheViewModel vm = new GesucheViewModel(() -> {
        throw new GesuchFehler(0, "Backend nicht erreichbar", null);
    }, Runnable::run);

    aufFxThread(() -> {
        vm.laden();
        return null;
    });
    warten();

    assertEquals("Gesuche nicht geladen: Backend nicht erreichbar", vm.hinweisProperty().get());
}

aufFxThread(…) und warten() sind die Hilfen aus FxPlattform im Code-Stand: Die erste führt Code per Platform.runLater aus und wartet auf das Ergebnis, die zweite wartet, bis alles abgearbeitet ist, was bis dahin eingereiht wurde.

Erläuterung (2 Punkte): Task übergibt onSucceeded und onFailed über Platform.runLater an den JavaFX Application Thread. Ohne gestartete Plattform (Platform.startup(…), headless mit Monocle) gibt es diesen Thread nicht, und die Handler laufen nie. Wann der Task läuft, bestimmt der Test über den Executor: wartend::add sammelt den Task nur, sodass der Zustand „lädt“ prüfbar bleibt; Runnable::run führt ihn sofort im aufrufenden Thread aus.

Aufgabe 2.2: Den REST-Client für die Gesuche mit WireMock testen (Kernkompetenz)

(ca. 12 min Bearbeitungszeit)

Das Backend soll die offenen Gesuche später über GET /gesuche liefern. Der Endpunkt ist als Vorschlag für den Vertrag notiert, aber noch nicht umgesetzt, und steht deshalb nicht in der openapi.yaml. Bis dahin hat das Team die RestGesuchQuelle mit dem HttpClient des JDK von Hand geschrieben (Startcode im Code-Stand src/05-backend-anbinden/uebung-mitfahrgesuche). Sie holt erst GET /haltepunkte, dann GET /gesuche, setzt den Namen des Haltepunkts mit Ort in Klammern ein (z. B. „Bahnhof Brackwede (Bielefeld)“) und wirft bei einem Status ungleich 200 eine GesuchFehler-Ausnahme mit dem title der Problem Details oder, wenn keine kommen, mit „Fehler “. Kommt gar keine Antwort, lautet die Meldung „Backend nicht erreichbar“ mit Status 0.

Ein Gesuch sieht in der Antwort so aus:

{"id": 3, "haltepunktId": 1, "richtung": "zur-schule", "datum": "2026-11-16",
 "fruehestens": "06:45", "spaetestens": "07:15", "notiz": "mit Sporttasche"}
  1. Beschreibe, wozu ein Integrationstest gegen WireMock dient, und nenne zwei Unterschiede zu einem Komponententest mit einer Lambda-Quelle wie in Aufgabe 2.1. (3 Punkte)

  2. Schreibe eine Testklasse RestGesuchQuelleIntegrationTest, die für jeden Test einen WireMock-Server auf einem freien Port startet und danach stoppt, und mindestens diese Fälle prüft: zwei Gesuche mit den richtigen Haltepunktnamen und dem Header Accept: application/json; eine Antwort 503 mit Problem Details (application/problem+json, title „Wartung“); ein Backend, das nicht läuft. (7 Punkte)

  3. Ein Test mit einer Notiz null in der Antwort schlägt mit einer NullPointerException in Gesuch.toString() fehl, sobald die Liste angezeigt wird. Erläutere, an welcher Stelle der Quelle das abgefangen wird und warum gerade ein Test gegen WireMock solche Fälle findet. (3 Punkte)

  1. WireMock ist ein HTTP-Server, der die REST-API nachahmt; der Test schickt echte HTTP-Anfragen und prüft so den Client mit Serialisierung, URL, Headern und Statuscodes, ohne dass das Backend laufen muss (1 Punkt). Unterschiede (je 1 Punkt), z. B.: Der Komponententest ersetzt die ganze Quelle und prüft nur das ViewModel, der Integrationstest prüft die Quelle selbst mit dem Netzwerkweg; WireMock kann Fehlerfälle (503, kaputtes JSON, langsame Antwort, nicht erreichbar) gezielt liefern; WireMock kann im Nachhinein prüfen, welche Anfragen ankamen (verify).

  2. Beispiellösung (Aufbau 2 Punkte, je Fall 1,5 Punkte, verify des Headers 0,5 Punkte):

class RestGesuchQuelleIntegrationTest {

    private static final String HALTEPUNKTE = """
            [{"id": 1, "name": "Hauptbahnhof, ZOB Steig 3", "ort": "Gütersloh", "entfernungKm": 18.6},
             {"id": 6, "name": "Bahnhof Brackwede", "ort": "Bielefeld", "entfernungKm": 6.2}]
            """;
    private static final String GESUCHE = """
            [{"id": 2, "haltepunktId": 6, "richtung": "nach-hause", "datum": "2026-11-16",
              "fruehestens": "15:30", "spaetestens": "16:30", "notiz": null},
             {"id": 3, "haltepunktId": 1, "richtung": "zur-schule", "datum": "2026-11-16",
              "fruehestens": "06:45", "spaetestens": "07:15", "notiz": "mit Sporttasche"}]
            """;

    private WireMockServer backend;
    private RestGesuchQuelle quelle;

    @BeforeEach
    void starten() {
        backend = new WireMockServer(wireMockConfig().dynamicPort());
        backend.start();
        quelle = new RestGesuchQuelle(HttpClient.newHttpClient(), URI.create(backend.baseUrl() + "/v1"));
    }

    @AfterEach
    void stoppen() {
        backend.stop();
    }

    @Test
    void liefertGesucheMitNamenDesHaltepunkts() {
        backend.stubFor(get("/v1/haltepunkte").willReturn(okJson(HALTEPUNKTE)));
        backend.stubFor(get("/v1/gesuche").willReturn(okJson(GESUCHE)));

        List<Gesuch> gesuche = quelle.gesucheLaden();

        assertEquals(List.of(
                new Gesuch(2, "Bahnhof Brackwede (Bielefeld)", "nach-hause", LocalDate.of(2026, 11, 16),
                        LocalTime.of(15, 30), LocalTime.of(16, 30), ""),
                new Gesuch(3, "Hauptbahnhof, ZOB Steig 3 (Gütersloh)", "zur-schule", LocalDate.of(2026, 11, 16),
                        LocalTime.of(6, 45), LocalTime.of(7, 15), "mit Sporttasche")), gesuche);
        backend.verify(getRequestedFor(urlEqualTo("/v1/gesuche")).withHeader("Accept", equalTo("application/json")));
    }

    @Test
    void problemDetailsLiefernDieMeldung() {
        backend.stubFor(get("/v1/haltepunkte").willReturn(okJson(HALTEPUNKTE)));
        backend.stubFor(get("/v1/gesuche").willReturn(aResponse().withStatus(503)
                .withHeader("Content-Type", "application/problem+json")
                .withBody("{\"title\": \"Wartung\", \"status\": 503, \"detail\": \"Bis 06:00 Uhr\"}")));

        GesuchFehler fehler = assertThrows(GesuchFehler.class, quelle::gesucheLaden);

        assertEquals(503, fehler.getStatus());
        assertEquals("Wartung", fehler.getMessage());
    }

    @Test
    void ohneServerBackendNichtErreichbar() {
        backend.stop();

        GesuchFehler fehler = assertThrows(GesuchFehler.class, quelle::gesucheLaden);

        assertEquals(0, fehler.getStatus());
        assertEquals("Backend nicht erreichbar", fehler.getMessage());
    }
}

Die Imports kommen aus com.github.tomakehurst.wiremock.client.WireMock (statisch: get, okJson, aResponse, getRequestedFor, urlEqualTo, equalTo) und com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig. Die Datei im Code-Stand enthält zusätzlich einen Test für einen Fehler ohne Problem Details („Fehler 500“).

  1. Die Quelle ersetzt null beim Umwandeln von GesuchJson in Gesuch durch einen leeren Text: g.notiz() == null ? "" : g.notiz() (1 Punkt). Damit gilt im Model die Regel „Notiz ist nie null“, auf die sich toString() verlässt (1 Punkt). Die Beispieldaten und Lambda-Quellen sind von Hand gebaut und enthalten solche Werte meist nicht; erst der Test gegen WireMock liest echtes JSON, wie es das Backend liefert, und findet so den Unterschied zwischen Vertrag (Notiz optional) und Model (1 Punkt).

Aufgabe 3.1: Eine eingefrorene Oberfläche analysieren (Profilkompetenz)

(ca. 16 min Bearbeitungszeit)

Ben testet die Gesuche-Ansicht im Schul-WLAN gegen das Backend der Klasse. Er meldet im Kanban-Meeting:

  • B1: Beim Öffnen des Fensters und bei jedem Klick auf „Aktualisieren“ hängt die App zwei bis drei Sekunden; Windows zeigt im Titel „Keine Rückmeldung“.
  • B2: Den Hinweis „Gesuche werden geladen …“ hat er nie gesehen, obwohl der Code ihn setzt.
  • B3: Läuft das Backend nicht, hängt die App beim Klick, bis die Verbindung abbricht.

Das ViewModel enthält diese Fassung von laden() (absichtlich fehlerhaft):

public void laden() {
    laedt.set(true);
    fehler.set(null);
    try {
        gesuche.setAll(quelle.gesucheLaden());
    } catch (GesuchFehler e) {
        fehler.set(e.getMessage());
    }
    laedt.set(false);
}

Ein Teammitglied repariert das schnell so (ebenfalls absichtlich fehlerhaft):

public void laden() {
    laedt.set(true);
    fehler.set(null);
    new Thread(() -> {
        try {
            gesuche.setAll(quelle.gesucheLaden());
        } catch (GesuchFehler e) {
            fehler.set(e.getMessage());
        }
        laedt.set(false);
    }).start();
}

Jetzt hängt die App nicht mehr, aber es gibt neue Beobachtungen:

  • B4: Bei nicht laufendem Backend erscheint der Fehlerdialog nicht; im Log steht java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4. Den Dialog öffnet ein Listener des Controllers an fehlerProperty().
  • B5: Ab und zu zeigt die Liste nach „Aktualisieren“ für einen Moment falsche Zeilen oder bleibt leer, obwohl der Hinweis „2 offene Gesuche“ meldet.
  • B6: Schließt Ben das Fenster, während eine Anfrage hängt, läuft der Prozess im Task-Manager noch weiter.
  1. Erläutere für B1, B2 und B3 die gemeinsame Ursache in der ersten Fassung und begründe insbesondere, warum der Hinweis aus B2 nie erscheint. (5 Punkte)

  2. Ordne B4, B5 und B6 je einer Ursache in der zweiten Fassung zu. (4 Punkte)

  3. Gib eine korrigierte Fassung von laden() an, die alle Beobachtungen behebt, und begründe, warum der Dialog aus B4 damit wieder funktioniert, ohne dass der Controller geändert wird. (5 Punkte)

  4. Das Team will verhindern, dass so etwas wieder bis zu Ben durchrutscht. Beurteile die beiden Vorschläge „Wir ergänzen die Board-Regel für ‚Test‘: Die prüfende Person startet die App mit einem künstlich verzögerten Backend“ und „Ein Komponententest prüft, dass laden() sofort zurückkehrt“ und leite eine Regel für die Spalte „In Arbeit – erledigt“ ab. (3 Punkte)

  1. laden() läuft im Handler bzw. in initialize(), also auf dem JavaFX Application Thread, und ruft dort quelle.gesucheLaden() auf; der HTTP-Aufruf blockiert diesen Thread für die Dauer der Anfrage (2 Punkte). Solange er blockiert ist, zeichnet JavaFX nicht neu und verarbeitet keine Eingaben (B1); bei nicht laufendem Backend dauert das bis zum Verbindungsabbruch bzw. zum connectTimeout (B3) (1 Punkt). Zu B2: laedt.set(true) ändert zwar die Property und über das Binding den Text des Labels, gezeichnet wird aber erst im nächsten Pulse, also nach dem Ende des Handlers. Bis dahin steht laedt schon wieder auf false, der Text ist wieder der alte (2 Punkte).

  2. Je Beobachtung 1 Punkt, für eine genaue Begründung bei B5 zusätzlich 1 Punkt:

  • B4: fehler.set(…) läuft im neuen Thread; der Listener des Controllers läuft im selben Thread und erzeugt den Alert. Ein Fenster darf nur auf dem JavaFX Application Thread entstehen, deshalb die IllegalStateException.
  • B5: gesuche.setAll(…) und laedt.set(false) ändern Liste und Properties, an die ListView, Label und Maske gebunden sind, aus dem Hintergrund-Thread. Gleichzeitig liest der FX-Thread diese Werte beim Zeichnen. Es gibt keine Synchronisation, die Darstellung kann einen halb geänderten Zustand zeigen; JavaFX meldet das nicht.
  • B6: new Thread(…) übernimmt die Eigenschaft „kein Daemon“ des JavaFX Application Thread. Die JVM wartet beim Beenden auf alle Threads, die keine Daemon-Threads sind, also auf die hängende Anfrage.
  1. Korrektur mit Task und dem Executor aus Aufgabe 2.1 (3 Punkte, Ausschnitt):
public void laden() {
    if (laedt.get()) {
        return;
    }
    fehler.set(null);
    laedt.set(true);
    Task<List<Gesuch>> aufgabe = new Task<>() {
        @Override
        protected List<Gesuch> call() {
            return quelle.gesucheLaden();     // nur das läuft im Hintergrund
        }
    };
    aufgabe.setOnSucceeded(e -> {
        gesuche.setAll(aufgabe.getValue());
        laedt.set(false);
    });
    aufgabe.setOnFailed(e -> {
        fehler.set(aufgabe.getException().getMessage());
        laedt.set(false);
    });
    hintergrund.execute(aufgabe);             // Executor mit Daemon-Thread
}

Begründung (2 Punkte): Im Hintergrund läuft nur der Aufruf der Quelle. Alle Änderungen an Liste und Properties stehen in den Handlern, die Task auf dem JavaFX Application Thread aufruft. fehler.set(…) und damit der Listener des Controllers laufen also wieder auf dem FX-Thread, und der Alert darf entstehen. Der Executor mit Daemon-Thread (thread.setDaemon(true)) und shutdownNow() in stop() behebt B6. Gleichwertig ist eine Lösung, die im eigenen Thread nur quelle.gesucheLaden() aufruft und Ergebnis bzw. Fehler per Platform.runLater(…) übergibt, wenn der Thread ein Daemon-Thread ist.

  1. Bewertung (je Vorschlag 1 Punkt, Regel 1 Punkt): Die Prüfung mit verzögertem Backend in „Test“ findet B1 bis B3 sicher, kommt aber spät und hängt von der Sorgfalt der prüfenden Person ab; B5 tritt selten auf und rutscht trotzdem durch. Ein Komponententest, der mit einem Executor wartend::add prüft, dass laden() sofort zurückkehrt und laedt auf true steht, findet die erste Fassung schon in „In Arbeit“ automatisch; die zweite Fassung erkennt er aber nicht, weil sie ebenfalls sofort zurückkehrt. Sinnvoll ist beides. Regel für „In Arbeit – erledigt“, z. B.: „Jeder Aufruf des Backends läuft über den Executor des ViewModels; ein Komponententest mit zurückgehaltenem Task prüft den Zustand während des Ladens, und Properties werden nur in laden() und in den Handlern des Tasks geändert (Review-Punkt).“

Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zum Anbinden des Backends: Mitfahrgesuche im Hintergrund laden” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/javafx-backend-anbinden-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/javafx/javafx-einstieg. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: