CodeGym /Kursy /JAVA 25 SELF /Obsługa błędów w asynchronicznym IO, anulowanie operacji

Obsługa błędów w asynchronicznym IO, anulowanie operacji

JAVA 25 SELF
Poziom 56 , Lekcja 3
Dostępny

1. Obsługa błędów w operacjach asynchronicznych

W kodzie synchronicznym wszystko jest proste: jeśli plik nie został znaleziony albo brak uprawnień, od razu przechwytujesz wyjątek w try-catch. W kodzie asynchronicznym, zwłaszcza gdy używasz callbacków (CompletionHandler), błąd może wystąpić już po tym, jak Twoja metoda się zakończy — gdzieś w czeluściach puli wątków. Jeśli nie obsłużysz go poprawnie, program może zachowywać się nieprzewidywalnie: od „cichej” utraty danych po awarię całej aplikacji.

Jak błędy są przekazywane do CompletionHandler?

Interfejs CompletionHandler<V, A> ma dwie metody:

  • completed(V result, A attachment) — wywoływana, jeśli operacja zakończyła się powodzeniem.
  • failed(Throwable exc, A attachment) — wywoływana, jeśli wystąpił błąd.

Przykład użycia:

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("Pomyślnie odczytano " + result + " bajtów");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Błąd odczytu pliku: " + exc.getMessage());
                    // Można zalogować, powiadomić użytkownika, przekazać błąd dalej
                }
            });
        } catch (IOException ex) {
            System.out.println("Błąd otwierania pliku: " + ex.getMessage());
        }

        // Damy czas, aby operacja asynchroniczna się zakończyła (w prawdziwych aplikacjach używaj CountDownLatch lub innych mechanizmów)
        Thread.sleep(500);
    }
}

Co się tutaj dzieje?

  • Jeśli plik nie istnieje, metoda failed zostanie wywołana z odpowiednim wyjątkiem (NoSuchFileException).
  • Jeśli operacja zakończyła się powodzeniem — zadziała completed.

Przykłady typowych błędów

  • Nie znaleziono pliku: NoSuchFileException
  • Brak dostępu: AccessDeniedException
  • Błąd odczytu/zapisu: różne podklasy IOException
  • Problemy z buforem: BufferOverflowException, BufferUnderflowException

Logowanie i informowanie użytkownika

Błąd w asynchronicznym callbacku — to nie powód do paniki, ale też nie do udawania, że nic się nie stało. Dobrą praktyką jest logowanie błędu (np. przez Logger), a jeśli ma to znaczenie dla użytkownika — pokazanie komunikatu lub wywołanie obsługi w UI.

Przykład logowania:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Błąd operacji asynchronicznej: " + exc);
    exc.printStackTrace();
}

W kodzie produkcyjnym używaj normalnych loggerów (np. java.util.logging lub Log4j), a nie System.err.

2. Anulowanie operacji asynchronicznych

Kiedy może być potrzebne anulowanie operacji?

Czasem asynchroniczne zadanie trzeba zatrzymać w trakcie działania. Na przykład użytkownik się rozmyślił i kliknął „Anuluj” podczas pobierania pliku. Albo okno programu zostało zamknięte i operacja nie ma już sensu. Bywa też, że przy zamykaniu aplikacji po prostu trzeba porządnie zwolnić zasoby.

W takich przypadkach asynchroniczne wejście/wyjście w Javie wspiera anulowanie przez interfejs Future. Dzięki niemu można w dowolnym momencie przerwać wykonywane zadanie i nie marnować zasobów.

Jak anulować operację za pomocą Future?

Metoda read lub write w AsynchronousFileChannel zwraca obiekt Future<Integer>. Obiekt ten ma metodę cancel(boolean mayInterruptIfRunning).

Przykład: anulowanie asynchronicznego odczytu

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);

            // Zaczekajmy chwilę, a potem anulujmy operację
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Operacja odczytu została anulowana!");
            } else {
                System.out.println("Nie udało się anulować operacji (być może już się zakończyła)");
            }
        }
    }
}

Ważne szczegóły:

  • Anulowanie działa tylko dla operacji, które jeszcze się nie zakończyły.
  • Jeśli operacja już się skończyła — nie da się jej anulować.
  • Po anulowaniu, przy próbie wywołania get() na tym Future, zostanie rzucony wyjątek CancellationException.

Kiedy operacji nie da się już anulować?

Jeśli zadanie zdążyło się zakończyć — czy to pomyślnie, czy z błędem — zatrzymanie go nie jest już możliwe, pociąg odjechał.

Ponadto nie wszystkie implementacje potrafią faktycznie przerywać operacje na poziomie systemu operacyjnego. Na przykład przy pracy z niektórymi systemami plików „anulowanie” będzie czysto symboliczne: operacja będzie nadal wykonywana, ale wynik po prostu zignorujesz.

3. Praktyka: obsługa błędów i anulowanie

Przykład 1: obsługa błędu przy odczycie nieistniejącego pliku

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("Operacja zakończyła się pomyślnie");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Błąd podczas odczytu pliku: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("Błąd podczas otwierania pliku: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

Co zobaczymy w konsoli?

Błąd podczas otwierania pliku: no_such_file.txt

albo, jeśli błąd wystąpi właśnie przy odczycie, a nie przy otwieraniu:

Błąd podczas odczytu pliku: NoSuchFileException - no_such_file.txt

Przykład 2: anulowanie długiej operacji i poprawne zakończenie

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);

            // Po 50 ms anulujemy operację (dla eksperymentu)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Operacja odczytu została anulowana!");
            } else {
                System.out.println("Nie udało się anulować operacji (prawdopodobnie już się zakończyła)");
            }

            try {
                // Spróbujemy pobrać wynik (rzuci CancellationException)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("Złapano CancellationException: operacja faktycznie została anulowana.");
            }
        }
    }
}

4. Dobre praktyki: jak robić to właściwie

Zwalniaj zasoby nawet w przypadku błędów

Używaj try-with-resources do automatycznego zamykania kanałów:

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

Jeśli używasz CompletionHandler, nie zapomnij zamknąć kanału po zakończeniu wszystkich operacji. Jest to szczególnie ważne, gdy wykonujesz kilka operacji asynchronicznych pod rząd.

Nie blokuj UI/głównego wątku

Operacje asynchroniczne są po to, by nie blokować głównego wątku. Nie wywołuj future.get() w wątku UI — inaczej sens asynchroniczności znika.

Loguj wszystkie błędy

W CompletionHandler zawsze implementuj metodę failed i loguj (lub przekazuj dalej) wszystkie wyjątki.

Sprawdzaj zakończenie wszystkich operacji przed zamknięciem programu

Jeśli program zakończy się przed końcem operacji, wynik może zostać utracony. Do demonstracji w konsoli czasem trzeba użyć Thread.sleep(500), ale w prawdziwych aplikacjach używaj CountDownLatch, CompletableFuture lub innych mechanizmów synchronizacji.

Nie zapominaj o anulowaniu

Jeśli operacja nie jest już potrzebna (np. użytkownik zamknął okno), anuluj ją przez Future.cancel. To oszczędzi zasoby i przyspieszy reakcję aplikacji.

5. Typowe błędy przy obsłudze błędów i anulowaniu w asynchronicznym IO

Błąd nr 1: Ignorowanie metody failed w CompletionHandler.
Jeśli nie zaimplementujesz obsługi błędów, aplikacja może zachowywać się nieprzewidywalnie: błędy „zaginą”, a użytkownik nie będzie wiedział, dlaczego nic się nie dzieje.

Błąd nr 2: Nie zamknięto kanału po zakończeniu operacji.
Zapomniano zamknąć AsynchronousFileChannel — pojawi się wyciek zasobów i być może blokada pliku w systemie operacyjnym.

Błąd nr 3: Oczekiwanie na wynik operacji asynchronicznej w głównym wątku.
Wywołano future.get() w wątku UI — interfejs się zawiesił, a cała asynchroniczność poszła na marne.

Błąd nr 4: Próba anulowania już zakończonej operacji.
Wywołano cancel() zbyt późno — operacja już się zakończyła, anulowanie nie zadziała. To nie jest krytyczne, ale może wprowadzać w błąd podczas debugowania.

Błąd nr 5: Brak sprawdzenia wyniku anulowania.
Wywołano cancel(), ale nie sprawdzono wartości zwracanej i nie obsłużono CancellationException przy wywołaniu get() — program może się wywrócić albo zachowywać dziwnie.

Błąd nr 6: Brak zwolnienia zasobów przy błędzie lub anulowaniu.
Jeśli kanał nie zostanie zamknięty po błędzie lub anulowaniu, mogą wystąpić wycieki lub blokady plików.

1
Zadanie
JAVA 25 SELF, poziom 56, lekcja 3
Niedostępne
Przełączanie kanałów: anulowanie pobierania wideo 🎬
Przełączanie kanałów: anulowanie pobierania wideo 🎬
1
Zadanie
JAVA 25 SELF, poziom 56, lekcja 3
Niedostępne
Monitorowanie dzienników systemowych: równoległa kontrola i awaryjne zatrzymanie 🚨
Monitorowanie dzienników systemowych: równoległa kontrola i awaryjne zatrzymanie 🚨
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION