Dlaczego asynchroniczność jest ważna?
W dzisiejszych aplikacjach często wykonujemy operacje które zajmują dużo czasu – wywołania REST API, zapytania do bazy danych, przetwarzanie plików. Bez asynchroniczności każda taka operacja blokuje wątek, co dramatycznie ogranicza skalowalność aplikacji.
CompletableFuture rozwiązuje ten problem oferując elegancki sposób na komponowanie operacji asynchronicznych. Zamiast zagnieżdżonych callbacków otrzymujemy czytelny kod który można łatwo testować i debugować.
Co się nauczysz:
- Jak tworzyć i komponować operacje asynchroniczne z CompletableFuture
- Różnice między supplyAsync(), runAsync() i thenApply()
- Jak obsługiwać błędy w operacjach asynchronicznych
- Wzorce łączenia wielu CompletableFuture w jeden pipeline
- Best practices i typowe pułapki w programowaniu asynchronicznym
- Praktyczne przykłady z aplikacji webowych i mikrousług
Wymagania wstępne:
- Dobra znajomość Javy 8 (lambda expressions, streams)
- Podstawy programowania wielowątkowego
- Znajomość interfejsu Future i jego ograniczeń
- Doświadczenie z REST API i bazami danych
Co to jest CompletableFuture?
CompletableFuture to implementacja interfejsu Future wprowadzona w Javie 8, która rozwiązuje główne problemy tradycyjnego Future:
Future (Java 5) | CompletableFuture (Java 8) |
---|---|
Tylko get() – blokujący | Nieblokujące transformacje |
Brak komponowania | Fluent API do łączenia |
Trudna obsługa błędów | Eleganckie handle() i exceptionally() |
Jeden wynik | Można łączyć wiele Future |
Podstawowe operacje
Zacznijmy od prostego przykładu – asynchroniczne pobieranie danych użytkownika:
import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; public class UserService { private final ExecutorService executor = Executors.newFixedThreadPool(10); // Asynchroniczne pobieranie użytkownika public CompletableFuturegetUserAsync(Long userId) { return CompletableFuture.supplyAsync(() -> { // Symulacja wywołania bazy danych sleep(1000); return userRepository.findById(userId); }, executor); } // Asynchroniczne pobieranie profilu public CompletableFuture getProfileAsync(Long userId) { return CompletableFuture.supplyAsync(() -> { sleep(800); return profileRepository.findByUserId(userId); }, executor); } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
Komponowanie operacji asynchronicznych
Prawdziwa siła CompletableFuture ujawnia się przy komponowaniu operacji:
public class UserController { @GetMapping("/users/{id}/dashboard") public CompletableFuturegetUserDashboard(@PathVariable Long id) { CompletableFuture userFuture = userService.getUserAsync(id); CompletableFuture profileFuture = userService.getProfileAsync(id); CompletableFuture > ordersFuture = orderService.getOrdersAsync(id); // Łączenie wyników z trzech różnych źródeł return userFuture .thenCombine(profileFuture, (user, profile) -> new UserWithProfile(user, profile)) .thenCombine(ordersFuture, (userProfile, orders) -> new DashboardData(userProfile.getUser(), userProfile.getProfile(), orders)) .exceptionally(throwable -> { log.error("Error loading dashboard for user: " + id, throwable); return DashboardData.empty(); }); } }
Obsługa błędów
CompletableFuture oferuje eleganckie mechanizmy obsługi błędów:
public CompletableFutureprocessUserData(Long userId) { return CompletableFuture .supplyAsync(() -> fetchUserFromDatabase(userId)) .thenCompose(user -> validateUser(user)) .thenApply(user -> enrichUserData(user)) .thenApply(user -> user.getName()) .handle((result, throwable) -> { if (throwable != null) { log.error("Error processing user: " + userId, throwable); return "Unknown User"; } return result; }); } // Alternatywnie - używając exceptionally() public CompletableFuture processUserDataWithFallback(Long userId) { return getUserAsync(userId) .thenApply(User::getName) .exceptionally(throwable -> { log.warn("Falling back to cache for user: " + userId); return userCache.getUserName(userId); }); }
Wzorce zaawansowane
### Timeout i fallback
public CompletableFuturegetDataWithTimeout(String url) { CompletableFuture apiCall = CompletableFuture .supplyAsync(() -> httpClient.get(url)); CompletableFuture timeout = CompletableFuture .supplyAsync(() -> { sleep(5000); // 5 sekund timeout throw new TimeoutException("API call timeout"); }); return apiCall .applyToEither(timeout, result -> result) .exceptionally(throwable -> "Fallback data"); }
### Przetwarzanie wszystkich wyników
public CompletableFuture> getAllProductsData(List
productIds) { List > futures = productIds.stream() .map(this::getProductDataAsync) .collect(Collectors.toList()); // Czeka na wszystkie wyniki return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) .collect(Collectors.toList())); }
Performance considerations
Porównanie wydajności dla 3 wywołań API (każde 1 sekunda):
Podejście | Czas wykonania | Zużycie wątków |
---|---|---|
Synchroniczne | 3 sekundy | 1 wątek przez 3s |
CompletableFuture | 1 sekunda | 3 wątki przez 1s |
Sekwencyjne Future | 3 sekundy | 3 wątki przez 3s |
Integracja ze Spring Boot
W Spring Boot możesz użyć adnotacji @Async z CompletableFuture:
@Service @EnableAsync public class NotificationService { @Async public CompletableFuturesendEmail(String email, String message) { return CompletableFuture.runAsync(() -> { // Wysyłanie email emailClient.send(email, message); }); } @Async public CompletableFuture sendSms(String phone, String message) { return CompletableFuture.supplyAsync(() -> { // Wysyłanie SMS return smsClient.send(phone, message); }); } }
CompletableFuture gdy potrzebujesz wyniku z operacji asynchronicznej. Thread gdy robisz fire-and-forget operations. CompletableFuture to wyższy poziom abstrakcji.
Tak, CompletableFuture jest thread-safe. Możesz bezpiecznie wywoływać metody z różnych wątków. Wynik jest publikowany bezpiecznie.
Wyjątki są opakowywane w CompletionException. Używaj handle() lub exceptionally() do obsługi. Nigdy nie rzucaj checked exceptions z lambda.
Używaj logowania z nazwą wątku, IDE może nie pokazywać pełnego stack trace. CompletableFuture.whenComplete() pomaga w debugowaniu.
join() nie rzuca checked exceptions, get() rzuca. W lambda expressions używaj join(). W normalnym kodzie – get() z obsługą wyjątków.
Używaj thenCompose() zamiast thenApply() gdy lambda zwraca CompletableFuture. To spłaszcza zagnieżdżenia. Dziel długie chains na mniejsze metody.
Przydatne zasoby:
- Oracle CompletableFuture Documentation
- Spring Async Method Guide
- Spotify CompletableFutures Library
- Baeldung Guide to CompletableFuture
🚀 Zadanie dla Ciebie
Stwórz mikrousługę która asynchronicznie pobiera dane użytkownika z 3 różnych źródeł (baza danych, cache, zewnętrzne API), łączy je w jeden obiekt i zwraca wynik. Dodaj obsługę timeoutów i fallback na cache gdy API nie odpowiada. Zmierz różnicę w wydajności między podejściem synchronicznym a asynchronicznym.