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.
GO TO FULL VERSION