CodeGym /Kurse /JAVA 25 SELF /Fehlerbehandlung im Async-IO, Abbruch von Operationen

Fehlerbehandlung im Async-IO, Abbruch von Operationen

JAVA 25 SELF
Level 56 , Lektion 3
Verfügbar

1. Fehlerbehandlung in asynchronen Operationen

Im synchronen Code ist alles einfach: Wenn die Datei nicht gefunden wird oder kein Zugriff besteht, fangen Sie die Ausnahme sofort in try-catch ab. Im asynchronen Code, insbesondere wenn Sie Callbacks (CompletionHandler) verwenden, kann der Fehler erst auftreten, nachdem Ihre Methode bereits beendet wurde — irgendwo tief im Thread-Pool. Wenn er nicht richtig behandelt wird, kann sich das Programm unvorhersehbar verhalten: von der „stillen“ Datenverlust bis zum Absturz der gesamten Anwendung.

Wie werden Fehler an CompletionHandler übergeben?

Im Interface CompletionHandler<V, A> gibt es zwei Methoden:

  • completed(V result, A attachment) — wird aufgerufen, wenn die Operation erfolgreich war.
  • failed(Throwable exc, A attachment) — wird aufgerufen, wenn ein Fehler aufgetreten ist.

Ein Beispiel für die Verwendung:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Erfolgreich " + result + " Bytes gelesen");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Fehler beim Lesen der Datei: " + exc.getMessage());
                    // Man kann protokollieren, den Benutzer benachrichtigen, den Fehler weiterreichen
                }
            });
        } catch (IOException ex) {
            System.out.println("Fehler beim Öffnen der Datei: " + ex.getMessage());
        }

        // Wir geben der asynchronen Operation Zeit, zu Ende zu laufen (in realen Anwendungen verwenden Sie CountDownLatch oder andere Mechanismen)
        Thread.sleep(500);
    }
}

Was passiert hier?

  • Wenn die Datei nicht existiert, wird die Methode failed mit der entsprechenden Ausnahme (NoSuchFileException) aufgerufen.
  • Wenn die Operation erfolgreich beendet wurde — wird completed ausgeführt.

Beispiele für typische Fehler

  • Datei nicht gefunden: NoSuchFileException
  • Kein Zugriff: AccessDeniedException
  • Lese-/Schreibfehler: verschiedene Unterklassen von IOException
  • Pufferprobleme: BufferOverflowException, BufferUnderflowException

Protokollierung und Benachrichtigung des Benutzers

Ein Fehler im asynchronen Callback ist kein Grund zur Panik, aber auch kein Grund, so zu tun, als wäre nichts passiert. Gute Praxis ist es, den Fehler zu protokollieren (z. B. über Logger), und falls es für den Benutzer wichtig ist, eine Meldung anzuzeigen oder einen Handler im UI aufzurufen.

Beispiel fürs Logging:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Fehler bei der asynchronen Operation: " + exc);
    exc.printStackTrace();
}

Im Produktivcode verwenden Sie ordentliche Logger (z. B. java.util.logging oder Log4j) statt System.err.

2. Abbruch asynchroner Operationen

Wann kann der Abbruch einer Operation erforderlich sein?

Manchmal muss eine asynchrone Aufgabe mitten in der Ausführung gestoppt werden. Beispielsweise hat es sich der Benutzer anders überlegt und auf „Abbrechen“ während des Datei-Downloads geklickt. Oder das Fenster der Anwendung wurde geschlossen und die Operation hat keinen Sinn mehr. Und manchmal müssen beim Beenden der Anwendung einfach Ressourcen sauber freigegeben werden.

Für solche Fälle unterstützt der asynchrone Ein-/Ausgabe in Java den Abbruch über das Interface Future. Damit kann man eine laufende Aufgabe jederzeit abbrechen und Ressourcen nicht unnötig verbrauchen.

Wie bricht man eine Operation mit Future ab?

Die Methoden read oder write in AsynchronousFileChannel geben ein Objekt Future<Integer> zurück. Dieses Objekt besitzt die Methode cancel(boolean mayInterruptIfRunning).

Beispiel: Abbruch eines asynchronen Lesevorgangs

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Kurz warten und dann die Operation abbrechen
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Leseoperation abgebrochen!");
            } else {
                System.out.println("Operation konnte nicht abgebrochen werden (möglicherweise bereits abgeschlossen)");
            }
        }
    }
}

Wichtige Details:

  • Der Abbruch funktioniert nur für Operationen, die noch nicht beendet sind.
  • Wenn die Operation bereits beendet ist — lässt sie sich nicht mehr abbrechen.
  • Nach einem Abbruch wird beim Versuch, get() auf diesem Future aufzurufen, eine CancellationException ausgelöst.

Wann lässt sich eine Operation nicht mehr abbrechen?

Wenn die Aufgabe bereits beendet ist — ob erfolgreich oder mit Fehler —, ist ein Stoppen nicht mehr möglich, der Zug ist abgefahren.

Außerdem können nicht alle Implementierungen Operationen tatsächlich auf Betriebssystemebene unterbrechen. Beispielsweise ist der „Abbruch“ bei einigen Dateisystemen rein symbolisch: Die Operation läuft weiter, aber Sie ignorieren das Ergebnis einfach.

3. Praxis: Fehlerbehandlung und Abbruch

Beispiel 1: Fehlerbehandlung beim Lesen einer nicht vorhandenen Datei

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Operation erfolgreich abgeschlossen");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Fehler beim Lesen der Datei: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("Fehler beim Öffnen der Datei: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

Was sehen wir in der Konsole?

Fehler beim Öffnen der Datei: no_such_file.txt

oder wenn der Fehler beim Lesen und nicht beim Öffnen auftritt:

Fehler beim Lesen der Datei: NoSuchFileException - no_such_file.txt

Beispiel 2: Abbruch einer lang andauernden Operation und sauberes Beenden

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 MB

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Nach 50 ms brechen wir die Operation ab (für das Experiment)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Die Leseoperation wurde abgebrochen!");
            } else {
                System.out.println("Operation konnte nicht abgebrochen werden (vermutlich bereits beendet)");
            }

            try {
                // Wir versuchen, das Ergebnis abzurufen (wirft CancellationException)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("CancellationException abgefangen: Die Operation wurde tatsächlich abgebrochen.");
            }
        }
    }
}

4. Best practices: so macht man es richtig

Geben Sie Ressourcen auch bei Fehlern frei

Verwenden Sie try-with-resources für das automatische Schließen von Kanälen:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

Wenn Sie CompletionHandler verwenden, vergessen Sie nicht, den Kanal beim Abschluss aller Operationen zu schließen. Das ist besonders wichtig, wenn Sie mehrere asynchrone Operationen hintereinander ausführen.

Blockieren Sie nicht den UI-/Haupt-Thread

Asynchrone Operationen sind dazu da, den Haupt-Thread nicht zu blockieren. Rufen Sie future.get() nicht im UI-Thread auf — sonst geht der Sinn der Asynchronität verloren.

Protokollieren Sie alle Fehler

Im CompletionHandler sollten Sie stets die Methode failed implementieren und alle Ausnahmen protokollieren (oder weitergeben).

Stellen Sie vor dem Beenden des Programms sicher, dass alle Operationen abgeschlossen sind

Wenn das Programm beendet wird, bevor die Operation abgeschlossen ist, kann das Ergebnis verloren gehen. Für Konsolendemos greift man manchmal zu Thread.sleep(500), in realen Anwendungen sollten Sie jedoch CountDownLatch, CompletableFuture oder andere Synchronisationsmechanismen verwenden.

Vergessen Sie den Abbruch nicht

Wenn eine Operation nicht mehr benötigt wird (z. B. der Benutzer hat das Fenster geschlossen), brechen Sie sie über Future.cancel ab. Das spart Ressourcen und verbessert die Reaktionsfähigkeit der Anwendung.

5. Typische Fehler bei Fehlerbehandlung und Abbruch im Async-IO

Fehler Nr. 1: Ignorieren der Methode failed in CompletionHandler.
Wenn Sie die Fehlerbehandlung nicht implementieren, verhält sich Ihre Anwendung unvorhersehbar: Fehler „gehen verloren“, und der Benutzer bleibt im Unklaren, warum nichts passiert.

Fehler Nr. 2: Kanal nach Abschluss der Operationen nicht geschlossen.
Vergessen, den AsynchronousFileChannel zu schließen — führt zu Ressourcenlecks und möglicherweise zur Sperre der Datei im Betriebssystem.

Fehler Nr. 3: Auf das Ergebnis einer asynchronen Operation im Haupt-Thread warten.
future.get() im UI-Thread aufgerufen — die Oberfläche „friert ein“, und die ganze Asynchronität ist dahin.

Fehler Nr. 4: Versuch, eine bereits abgeschlossene Operation abzubrechen.
cancel() zu spät aufgerufen — die Operation ist bereits beendet, der Abbruch greift nicht. Das ist nicht kritisch, kann aber bei der Fehlersuche verwirren.

Fehler Nr. 5: Ergebnis des Abbruchs nicht prüfen.
cancel() aufgerufen, aber den Rückgabewert nicht geprüft und CancellationException bei Aufruf von get() nicht behandelt — das Programm kann abstürzen oder sich merkwürdig verhalten.

Fehler Nr. 6: Ressourcen bei Fehler oder Abbruch nicht freigeben.
Wenn der Kanal nach einem Fehler oder Abbruch nicht geschlossen wird, kann es zu Lecks oder Dateisperren kommen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION