Asynchrone Aktualisierungen
Die API Environment.runLater() bietet einen Mechanismus, um die UI in webforJ-Anwendungen sicher von Hintergrundthreads aus zu aktualisieren. Dieses experimentelle Feature ermöglicht asynchrone Vorgänge und gewährleistet gleichzeitig die Threadsicherheit bei UI-Änderungen.
The webforj-handling-timers-and-async skill can schedule timers, debouncers, and async work safely on the UI thread. After installing the webforJ AI plugin, ask your assistant:
- "Refresh this dashboard every 30 seconds."
- "Add a search-as-you-type debouncer."
- "Run this CPU-heavy work in the background and update the progress bar."
Das Thread-Modell verstehen
webforJ erzwingt ein striktes Thread-Modell, bei dem alle UI-Vorgänge im Environment-Thread stattfinden müssen. Diese Einschränkung besteht aus folgenden Gründen:
- Einschränkungen der webforJ-API: Die zugrunde liegende webforJ-API ist an den Thread gebunden, der die Sitzung erstellt hat.
- Thread-Affinität von Komponenten: UI-Komponenten verwalten einen Zustand, der nicht threadsicher ist.
- Ereignisverarbeitung: Alle UI-Ereignisse werden sequenziell in einem einzelnen Thread verarbeitet.
Dieses Single-Thread-Modell verhindert Race Conditions und gewährleistet einen konsistenten Zustand für alle UI-Komponenten. Bei der Integration mit asynchronen, lang laufenden Berechnungsaufgaben stellt es jedoch eine Herausforderung dar.
Jede Benutzersitzung hat ihr eigenes Environment und ihren eigenen Environment-Thread. Ein Hintergrundthread gehört zu keiner Sitzung. Daher muss ihm mitgeteilt werden, welches Environment aktualisiert werden soll.
Die RunLater-API
Die API Environment.runLater() bietet zwei Methoden zum Planen von UI-Aktualisierungen. Beide erwarten das Ziel-Environment als erstes Argument:
// Eine Aufgabe ohne Rückgabewert planen
public static PendingResult<Void> runLater(Environment target, Runnable task)
// Eine Aufgabe planen, die einen Wert zurückgibt
public static <T> PendingResult<T> runLater(Environment target, Supplier<T> supplier)
Beide Methoden geben ein PendingResult zurück, das den Abschluss der Aufgabe verfolgt und Zugriff auf das Ergebnis oder auf eventuell aufgetretene Ausnahmen bietet.
Die Überladungen runLater(Runnable) und runLater(Supplier) ermitteln das Ziel anhand des Threads, der den aufrufenden Thread erstellt hat. In einem Thread-Pool, der von mehreren Benutzern gemeinsam verwendet wird, wird jede Aufgabe im Environment des Benutzers ausgeführt, dessen Aufruf diesen Pool-Thread erstellt hat. Diese Überladungen sind seit Version 26.10 veraltet und sollen in der nächsten Hauptversion entfernt werden. Übergeben Sie immer das Ziel.
Das Ziel übergeben
Environment.getCurrent() gibt das Environment des aktuellen Threads zurück. Im Environment-Thread, in dem Konstruktoren, Ereignisbehandler und Lifecycle-Methoden ausgeführt werden, gibt die Methode das Environment der Sitzung zurück. In einem Hintergrundthread gibt sie null zurück.
Lesen Sie das Ziel an der Stelle aus, an der es verfügbar ist, also bevor die Hintergrundarbeit beginnt, und übergeben Sie es an die Arbeit:
@Route
public class DataView extends Composite<Div> {
private final ExecutorService executor = Executors.newCachedThreadPool();
public DataView() {
// Der Konstruktor wird im Environment-Thread ausgeführt
Environment env = Environment.getCurrent();
executor.submit(() -> {
String data = fetchRemoteData();
Environment.runLater(env, () -> {
dataLabel.setText(data);
loadingSpinner.setVisible(false);
});
});
}
}
Da das Ziel als Argument übergeben wird, funktioniert der Aufruf von jedem Thread aus: von einem Thread, den die View erstellt hat, einem Spring-@Async-Thread, dem gemeinsamen ForkJoinPool, einem Timer-Thread oder einem Callback-Thread eines HTTP-Clients.
Das Ziel im falschen Thread auslesen
Vermeiden Sie unbedingt, Environment.getCurrent() innerhalb der Hintergrundarbeit auszulesen. Dort gibt die Methode null zurück, und runLater() wirft eine NullPointerException:
executor.submit(() -> {
String data = fetchRemoteData();
// Falsch: getCurrent() gibt in einem Hintergrundthread null zurück
Environment.runLater(Environment.getCurrent(), () -> dataLabel.setText(data));
});
Dasselbe gilt für statische Initialisierer, java.util.Timer-Threads und Callbacks externer Bibliotheken. Keiner davon hat ein Environment. Das Ziel muss daher aus einem Feld, einem Parameter oder einer lokalen Variable stammen, die im Environment-Thread befüllt wurde.
Ausführungsverhalten
Das Ausführungsverhalten von runLater() hängt davon ab, welcher Thread die Methode aufruft:
Aufruf im UI-Thread
Wird die Methode im eigenen Environment-Thread des Ziels aufgerufen, werden Aufgaben synchron und sofort ausgeführt:
button.onClick(e -> {
Environment env = Environment.getCurrent();
System.out.println("Vorher: " + Thread.currentThread().getName());
PendingResult<String> result = Environment.runLater(env, () -> {
System.out.println("Innerhalb: " + Thread.currentThread().getName());
return "abgeschlossen";
});
System.out.println("Danach: " + result.isDone()); // true
});
Dank dieses synchronen Verhaltens werden UI-Aktualisierungen aus Ereignisbehandlern sofort angewendet, ohne unnötigen Aufwand durch das Einreihen in eine Warteschlange.
Aufruf in Hintergrundthreads
Wird die Methode von einem anderen Thread aus aufgerufen, werden Aufgaben zur asynchronen Ausführung in eine Warteschlange gestellt:
@Override
public void onDidCreate() {
Environment env = Environment.getCurrent();
CompletableFuture.runAsync(() -> {
// Wird in einem ForkJoinPool-Thread ausgeführt
System.out.println("Hintergrund: " + Thread.currentThread().getName());
PendingResult<Void> result = Environment.runLater(env, () -> {
// Wird im Environment-Thread ausgeführt
System.out.println("UI-Aktualisierung: " + Thread.currentThread().getName());
statusLabel.setText("Verarbeitung abgeschlossen");
});
// result.isDone() wäre hier false
// Die Aufgabe wird in die Warteschlange gestellt und asynchron ausgeführt
});
}
webforJ verarbeitet Aufgaben, die von Hintergrundthreads eingereicht wurden, in strikter FIFO-Reihenfolge. So bleibt die Reihenfolge der Vorgänge erhalten, selbst wenn mehrere Threads gleichzeitig Aufgaben einreichen. Dank dieser garantierten Reihenfolge werden UI-Aktualisierungen genau in der Reihenfolge angewendet, in der sie eingereicht wurden. Reicht Thread A also Aufgabe 1 und anschließend Thread B Aufgabe 2 ein, wird Aufgabe 1 im UI-Thread immer vor Aufgabe 2 ausgeführt. Die FIFO-Verarbeitung verhindert Inkonsistenzen in der UI.
Aufgaben abbrechen
Das von Environment.runLater() zurückgegebene PendingResult unterstützt das Abbrechen von Aufgaben. Dadurch können Sie verhindern, dass Aufgaben aus der Warteschlange ausgeführt werden. Wenn Sie ausstehende Aufgaben abbrechen, vermeiden Sie Speicherlecks und verhindern, dass lang laufende Vorgänge die UI aktualisieren, obwohl sie nicht mehr benötigt werden.
Einfaches Abbrechen
PendingResult<Void> result = Environment.runLater(env, () -> {
updateUI();
});
// Abbrechen, falls die Aufgabe noch nicht ausgeführt wurde
if (!result.isDone()) {
result.cancel();
}
Mehrere Aktualisierungen verwalten
Wenn Sie lang laufende Vorgänge mit häufigen UI-Aktualisierungen durchführen, verfolgen Sie alle ausstehenden Ergebnisse:
public class LongRunningTask {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
private volatile boolean isCancelled = false;
public void startTask() {
Environment env = Environment.getCurrent();
CompletableFuture.runAsync(() -> {
for (int i = 0; i <= 100; i++) {
if (isCancelled) return;
final int progress = i;
PendingResult<Void> update = Environment.runLater(env, () -> {
progressBar.setValue(progress);
});
// Für ein mögliches Abbrechen erfassen
pendingUpdates.add(update);
Thread.sleep(100);
}
});
}
public void cancelTask() {
isCancelled = true;
// Alle ausstehenden UI-Aktualisierungen abbrechen
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Verwaltung des Komponenten-Lifecycles
Wenn Komponenten zerstört werden (z. B. bei der Navigation), brechen Sie alle ausstehenden Aktualisierungen ab, um Speicherlecks zu vermeiden:
@Route
public class CleanupView extends Composite<Div> {
private final List<PendingResult<?>> pendingUpdates = new ArrayList<>();
@Override
protected void onDestroy() {
super.onDestroy();
// Alle ausstehenden Aktualisierungen abbrechen, um Speicherlecks zu vermeiden
for (PendingResult<?> pending : pendingUpdates) {
if (!pending.isDone()) {
pending.cancel();
}
}
pendingUpdates.clear();
}
}
Überlegungen zum Design
-
Ziel erforderlich: Das Ziel-
Environmentwird mitEnvironment.getCurrent()imEnvironment-Thread ausgelesen und an die Hintergrundarbeit übergeben. Wird es in einem Hintergrundthread ausgelesen, gibt die Methodenullzurück. -
Speicherlecks verhindern: Verfolgen Sie
PendingResult-Objekte immer und brechen Sie sie in den Lifecycle-Methoden von Komponenten ab. Eingereihte Lambdas erfassen Referenzen auf UI-Komponenten und verhindern deren Garbage Collection, wenn sie nicht abgebrochen werden. -
FIFO-Ausführung: Alle Aufgaben werden unabhängig von ihrer Wichtigkeit in strikter FIFO-Reihenfolge ausgeführt. Es gibt kein Prioritätensystem.
-
Einschränkungen beim Abbrechen: Das Abbrechen verhindert nur die Ausführung von Aufgaben, die sich noch in der Warteschlange befinden. Bereits ausgeführte Aufgaben werden normal abgeschlossen.
Vollständige Fallstudie: LongTaskView
Die folgende vollständige, produktionsreife Implementierung veranschaulicht bewährte Vorgehensweisen für asynchrone UI-Aktualisierungen:
Analyse der Fallstudie
Diese Implementierung veranschaulicht mehrere wichtige Vorgehensweisen:
1. Thread-Pool-Verwaltung
private final ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "LongTaskView-Worker");
t.setDaemon(true);
return t;
});
- Verwendet einen Executor mit einem einzelnen Thread, um eine Ressourcenerschöpfung zu verhindern
- Erstellt Daemon-Threads, die das Herunterfahren der JVM nicht verhindern
2. Das Ziel bestimmen
Environment env = Environment.getCurrent();
startLongTask() wird im Environment-Thread ausgeführt. Daher wird das Ziel dort ausgelesen und von der Hintergrundaufgabe an jeden Aufruf von Environment.runLater() übergeben. Derselbe Code funktioniert unverändert mit einem gemeinsam genutzten, anwendungsweiten Thread-Pool.
3. Ausstehende Aktualisierungen nachverfolgen
private final List<PendingResult<?>> pendingUIUpdates = new ArrayList<>();
Jeder Aufruf von Environment.runLater() wird erfasst, um Folgendes zu ermöglichen:
- Abbrechen, wenn der Benutzer auf „Abbrechen“ klickt
- Speicherlecks in
onDestroy()verhindern - Ordnungsgemäße Bereinigung während des Komponenten-Lifecycles
4. Kooperatives Abbrechen
private volatile boolean isCancelled = false;
Der Hintergrundthread prüft dieses Flag bei jeder Iteration. Dadurch werden Folgendes ermöglicht:
- Sofortige Reaktion auf einen Abbruch
- Sauberes Beenden der Schleife
- Verhindern weiterer UI-Aktualisierungen
5. Lifecycle-Verwaltung
@Override
protected void onDestroy() {
super.onDestroy();
cancelTask(); // Verwendet die Abbruchlogik erneut
currentTask = null;
executor.shutdown();
}
Wichtig, um Speicherlecks zu verhindern durch:
- Abbrechen aller ausstehenden UI-Aktualisierungen
- Unterbrechen laufender Threads
- Herunterfahren des Executors
6. Reaktionsfähigkeit der UI testen
testButton.onClick(e -> {
int count = clickCount.incrementAndGet();
showToast("Klick Nr. " + count + " – die UI reagiert!", Theme.GRAY);
});
Zeigt, dass der UI-Thread während Hintergrundvorgängen reaktionsfähig bleibt.


