@Async Anotasyonu
Giriş
Bir şirket düşünün: CEO her departmanla birebir ilgileniyor — önce muhasebe, sonra pazarlama, sonra İK. Tüm gün sürer. Akıllı CEO ise toplantıda kararları verir ve uygulama görevlerini departmanlara delege eder. Kendisi bir sonraki konuya geçerken, departmanlar kendi işlerini paralel olarak yapar. Spring'deki @Async anotasyonu tam olarak bu "akıllı CEO" modelidir — bir metodu çağırdığınızda işi başka bir thread'e delege eder, siz hemen bir sonraki işe geçersiniz.
Spring Framework'te bir metodu asenkron olarak çalıştırmanın en kolay yolu @Async anotasyonudur. Bu anotasyon, metodu çağıran thread'i bloke etmeden, metodun ayrı bir thread'de çalışmasını sağlar. Arka planda Spring, AOP (Aspect-Oriented Programming) mekanizmasını kullanarak metot çağrısını bir proxy üzerinden yönlendirir ve belirtilen TaskExecutor üzerinde çalıştırır.
Bu derste @Async'in nasıl etkinleştirildiğini, void ve dönüş değerli kullanımlarını, proxy mekanizmasının nasıl çalıştığını, self-invocation tuzağını, exception handling stratejilerini, @Transactional ile birlikte kullanımını ve production best practice'lerini derinlemesine öğreneceğiz.
@EnableAsync ile Aktivasyon
@Async anotasyonunun çalışabilmesi için uygulamanın asenkron desteğini açıkça etkinleştirmesi gerekir. Bu adım atlanırsa @Async sessizce görmezden gelinir — hata almaz ama metot senkron çalışır.
// Yöntem 1: Ayrı konfigürasyon sınıfı (önerilen)
@Configuration
@EnableAsync
public class AsyncConfig {
// @Async artık aktif!
}
// Yöntem 2: Ana uygulama sınıfında (basit projeler için)
@SpringBootApplication
@EnableAsync
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}@EnableAsync'in yaptığı şey: Spring context'inde bir AsyncAnnotationBeanPostProcessor kaydeder. Bu processor, @Async anotasyonlu metot içeren bean'leri tespit eder ve onları proxy ile sarar.
@EnableAsync Parametreleri
@EnableAsync(
annotation = Async.class, // Hangi anotasyonu tarayacak (varsayılan: @Async)
proxyTargetClass = false, // true → CGLIB proxy, false → JDK dynamic proxy
mode = AdviceMode.PROXY, // PROXY veya ASPECTJ
order = Ordered.LOWEST_PRECEDENCE // AOP advice sırası
)💡 İpucu:
proxyTargetClass = trueyaparsanız CGLIB proxy kullanılır. Bu, interface olmayan sınıfları da proxy'leyebilir. Spring Boot varsayılan olarak CGLIB kullanır.
Temel Kullanım: void Metotlar (Fire-and-Forget)
En basit ve en yaygın senaryo: dönüş değeri olmayan asenkron metotlar. "Yap ve unut" — sonucuyla ilgilenmiyorsunuz.
@Service
@Slf4j
public class NotificationService {
@Async
public void sendEmail(String to, String subject, String body) {
log.info("E-posta gönderiliyor: thread={}", Thread.currentThread().getName());
try {
emailClient.send(to, subject, body);
log.info("E-posta başarıyla gönderildi: to={}", to);
} catch (Exception e) {
log.error("E-posta gönderilemedi: to={}", to, e);
// Exception çağırana İLETİLMEZ — kaybolur!
// AsyncUncaughtExceptionHandler ile yakalanmalı
}
}
@Async
public void sendSms(String phone, String message) {
log.info("SMS gönderiliyor: thread={}", Thread.currentThread().getName());
smsClient.send(phone, message);
}
@Async
public void sendPushNotification(String userId, String title, String body) {
log.info("Push gönderiliyor: thread={}", Thread.currentThread().getName());
pushClient.send(userId, title, body);
}
}Çağıran tarafta:
@Service
@RequiredArgsConstructor
public class OrderService {
private final NotificationService notificationService;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderResult processOrder(Order order) {
// 1. Senkron — Kritik iş mantığı
PaymentResult payment = paymentService.charge(order);
inventoryService.reserve(order);
// 2. Asenkron — Fire-and-forget bildirimler
// Bu üç metot AYNI ANDA, farklı thread'lerde çalışır
notificationService.sendEmail(order.getEmail(), "Sipariş Onayı", "...");
notificationService.sendSms(order.getPhone(), "Siparişiniz alındı");
notificationService.sendPushNotification(order.getUserId(), "Sipariş", "...");
// 3. Bildirimler arka planda devam ederken HEMEN dönüyoruz
// Kullanıcı bildirim gönderimleri bitmeden yanıtını alır
return new OrderResult(payment, "SUCCESS");
}
}Log çıktısı:
10:30:45 [http-nio-8080-exec-1] OrderService - Sipariş işleniyor
10:30:47 [http-nio-8080-exec-1] OrderService - Ödeme alındı, bildirimler gönderiliyor
10:30:47 [async-1] NotificationService - E-posta gönderiliyor: thread=async-1
10:30:47 [async-2] NotificationService - SMS gönderiliyor: thread=async-2
10:30:47 [async-3] NotificationService - Push gönderiliyor: thread=async-3
10:30:47 [http-nio-8080-exec-1] OrderService - Yanıt döndürülüyor (bildirimler devam ediyor)
10:30:49 [async-1] NotificationService - E-posta gönderildi
10:30:48 [async-2] NotificationService - SMS gönderildi
10:30:48 [async-3] NotificationService - Push gönderildiDikkat edin: http-nio-8080-exec-1 (istek thread'i) hemen dönerken, async-1, async-2, async-3 thread'leri arka planda çalışmaya devam ediyor.
Dönüş Değeri: Future ve CompletableFuture
Asenkron metodun sonucuna ihtiyacınız varsa, Future<T> veya CompletableFuture<T> dönüş tipi kullanırsınız:
Future (Eski Yöntem)
@Service
public class ReportService {
@Async
public Future<Report> generateReport(String type) {
Report report = heavyReportGeneration(type); // 30 saniye sürebilir
return new AsyncResult<>(report); // Spring'in Future wrapper'ı
}
}
// Çağıran tarafta
Future<Report> future = reportService.generateReport("SALES");
// Seçenek 1: Bloke ederek bekle
Report report = future.get(); // Tamamlanana kadar BLOKE OLUR
// Seçenek 2: Timeout ile bekle
Report report = future.get(30, TimeUnit.SECONDS); // 30 saniye timeout
// Seçenek 3: Hazır mı kontrol et
if (future.isDone()) {
Report report = future.get();
}CompletableFuture (Önerilen)
@Service
public class ReportService {
@Async
public CompletableFuture<Report> generateReportAsync(String type) {
Report report = heavyReportGeneration(type);
return CompletableFuture.completedFuture(report);
}
}
// Çağıran tarafta — non-blocking zincir
reportService.generateReportAsync("SALES")
.thenApply(report -> enrichReport(report)) // Raporu zenginleştir
.thenAccept(report -> emailService.send(report)) // E-posta gönder
.exceptionally(ex -> {
log.error("Rapor oluşturulamadı", ex);
return null;
});Birden Fazla Asenkron Sonucu Birleştirme
@RestController
@RequiredArgsConstructor
public class DashboardController {
private final DashboardService dashboardService;
@GetMapping("/dashboard/{userId}")
public DashboardResponse getDashboard(@PathVariable Long userId) {
// 3 servis PARALEL çağrılır
CompletableFuture<UserProfile> profileFuture =
dashboardService.fetchProfile(userId);
CompletableFuture<List<Order>> ordersFuture =
dashboardService.fetchOrders(userId);
CompletableFuture<WalletInfo> walletFuture =
dashboardService.fetchWallet(userId);
// Hepsi tamamlanana kadar bekle
CompletableFuture.allOf(profileFuture, ordersFuture, walletFuture).join();
return new DashboardResponse(
profileFuture.join(), // Artık bloke etmez (zaten tamamlandı)
ordersFuture.join(),
walletFuture.join()
);
}
}
@Service
public class DashboardService {
@Async
public CompletableFuture<UserProfile> fetchProfile(Long userId) {
UserProfile profile = userClient.getProfile(userId); // 200ms
return CompletableFuture.completedFuture(profile);
}
@Async
public CompletableFuture<List<Order>> fetchOrders(Long userId) {
List<Order> orders = orderClient.getOrders(userId); // 150ms
return CompletableFuture.completedFuture(orders);
}
@Async
public CompletableFuture<WalletInfo> fetchWallet(Long userId) {
WalletInfo wallet = walletClient.getWallet(userId); // 100ms
return CompletableFuture.completedFuture(wallet);
}
}Senkron: 200 + 150 + 100 = 450ms → Asenkron: max(200, 150, 100) = 200ms 🚀
Proxy Mekanizması — @Async Nasıl Çalışır?
@Async anotasyonu, Spring AOP proxy mekanizmasına dayanır. Bu mekanizmayı anlamadan @Async kullanmak, kaçınılmaz olarak hatalara yol açar.
Akış
Çağıran kod → Proxy (intercept) → TaskExecutor.submit() → Yeni Thread → Gerçek metotSpring, @Async annotasyonlu metot içeren bean'i bir proxy ile sarar. Dışarıdan bu bean'in bir metodu çağrıldığında:
Çağrı önce proxy'ye gider
Proxy,
@Asyncanotasyonunu kontrol ederMetodu bir
TaskExecutorüzerinden submit eder (ayrı thread)Çağırana hemen
null(void) veyaCompletableFuturedönerGerçek metot arka planda çalışır
Self-Invocation Tuzağı ⚠️
Bu mekanizmanın en kritik sonucu: aynı sınıf içinden çağrı proxy'yi bypass eder!
@Service
public class MyService {
@Async
public void asyncMethod() {
log.info("Bu asenkron çalışmalı — ama çalışacak mı?");
}
public void callerMethod() {
asyncMethod(); // ⚠️ Bu SENKRON çalışır!
// Çünkü this.asyncMethod() proxy'yi bypass eder
// this → gerçek nesne (proxy değil)
}
}Neden? callerMethod() çağrıldığında akış proxy üzerinden geçer. Ancak callerMethod içindeki asyncMethod() çağrısı this.asyncMethod() şeklinde çalışır — buradaki this proxy değil, gerçek nesnedir.
Çözüm 1: Ayrı Servis (En İyi)
@Service
public class AsyncTaskService {
@Async
public void asyncMethod() {
log.info("Asenkron çalışıyor! thread={}", Thread.currentThread().getName());
}
}
@Service
@RequiredArgsConstructor
public class MyService {
private final AsyncTaskService asyncTaskService;
public void callerMethod() {
asyncTaskService.asyncMethod(); // ✅ Proxy üzerinden geçer → asenkron çalışır
}
}Çözüm 2: Self-Injection
@Service
public class MyService {
@Autowired
private MyService self; // Proxy'yi inject et (dikkatli kullanın!)
@Async
public void asyncMethod() {
log.info("Asenkron çalışıyor!");
}
public void callerMethod() {
self.asyncMethod(); // ✅ Proxy üzerinden geçer
}
}Çözüm 3: ApplicationContext
@Service
public class MyService implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.context = applicationContext;
}
@Async
public void asyncMethod() {
log.info("Asenkron çalışıyor!");
}
public void callerMethod() {
context.getBean(MyService.class).asyncMethod(); // ✅ Proxy üzerinden
}
}💡 İpucu: En temiz yaklaşım Çözüm 1'dir. Asenkron metotları ayrı bir servis sınıfına taşımak, Single Responsibility prensibine de uygundur.
Asenkron Metotlarda Exception Handling
void Metotlarda — Exception Kaybolur!
void dönüş tipli asenkron metotlarda fırlatılan exception'lar sessizce kaybolur. Çağıran thread artık o metotla ilgilenmez — exception'ı yakalayacak kimse yoktur.
@Async
public void riskyOperation() {
throw new RuntimeException("Bu hata nereye gider?");
// Cevap: HİÇBİR YERE! Sessizce kaybolur.
// Log bile yazılmaz (kendi try-catch'iniz yoksa)
}Bu durumu yönetmek için AsyncUncaughtExceptionHandler kullanılır:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, params) -> {
log.error("🚨 Async exception — method: {}.{}, message: {}",
method.getDeclaringClass().getSimpleName(),
method.getName(),
throwable.getMessage(),
throwable);
// Metrik kaydet
meterRegistry.counter("async.errors",
"method", method.getName(),
"exception", throwable.getClass().getSimpleName()
).increment();
// Kritik hatalarda alert gönder
if (throwable instanceof CriticalException) {
alertService.sendAlert("Async Critical Error: " + throwable.getMessage());
}
};
}
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}CompletableFuture Metotlarda — Exception Future İçinde
@Async
public CompletableFuture<Report> generateReport(String type) {
try {
Report report = heavyGeneration(type);
return CompletableFuture.completedFuture(report);
} catch (Exception e) {
CompletableFuture<Report> failed = new CompletableFuture<>();
failed.completeExceptionally(e);
return failed;
}
}
// Çağıran tarafta — exception yakalanabilir
reportService.generateReport("SALES")
.thenAccept(report -> process(report))
.exceptionally(ex -> {
log.error("Rapor oluşturulamadı: {}", ex.getMessage());
alertService.notify("Report generation failed");
return null;
});@Async + @Transactional Birlikteliği
@Async ve @Transactional birlikte kullanıldığında dikkat edilmesi gereken önemli noktalar vardır.
Temel Kural
// ❌ YANLIŞ — @Async metot yeni thread'de çalışır, çağıranın transaction'ına KATILMAZ
@Async
@Transactional
public void updateOrderStatus(Long orderId, OrderStatus status) {
// Bu metot KENDİ transaction'ını açar (yeni thread = yeni transaction)
// Çağıranın transaction'ı ile ilişkisi YOKTUR
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus(status);
orderRepository.save(order);
}
// Çağıran tarafta
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// Bu asenkron metot farklı thread'de, FARKLI transaction'da çalışır
// Ana transaction rollback olsa bile bu değişiklik GERİ ALINMAZ
updateOrderStatus(order.getId(), OrderStatus.PROCESSING);
}Doğru Kullanım
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final AsyncNotificationService asyncNotificationService;
@Transactional
public Order processOrder(OrderRequest request) {
// 1. Ana iş mantığı — aynı transaction
Order order = orderRepository.save(createOrder(request));
orderItemRepository.saveAll(createItems(order, request));
// 2. Transaction commit'lendikten SONRA asenkron işlem
// @TransactionalEventListener(phase = AFTER_COMMIT) kullanmak daha güvenli
return order;
}
}
// Transaction commit'lendikten sonra asenkron bildirim
@Component
public class OrderCreatedHandler {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleOrderCreated(OrderCreatedEvent event) {
// Transaction başarıyla commit'lendikten SONRA çalışır
// Rollback olursa bu metot ÇALIŞMAZ
notificationService.sendConfirmation(event.getOrder());
}
}Belirli Executor Kullanma
Farklı iş yükleri için farklı executor'lar tanımlayıp @Async anotasyonunda belirtebilirsiniz:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "emailExecutor")
public TaskExecutor emailExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("email-");
executor.initialize();
return executor;
}
@Bean(name = "reportExecutor")
public TaskExecutor reportExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.setThreadNamePrefix("report-");
executor.initialize();
return executor;
}
}
@Service
public class NotificationService {
@Async("emailExecutor") // email thread pool'unu kullan
public void sendEmail(String to, String body) { /* ... */ }
@Async("reportExecutor") // report thread pool'unu kullan
public CompletableFuture<Report> generateReport(String type) { /* ... */ }
}⚠️ Dikkat:
@Asyncile executor adı belirtmezseniz, varsayılanTaskExecutorbean'i kullanılır. Eğer hiçbir executor tanımlanmamışsa Spring,SimpleAsyncTaskExecutorkullanır — bu, her çağrıda yeni thread oluşturur ve production için uygun DEĞİLDİR!
Yaygın Hatalar
1. @EnableAsync Unutmak
// ❌ @EnableAsync yok — @Async sessizce görmezden gelinir
@Service
public class MyService {
@Async
public void doWork() {
// Bu SENKRON çalışır! Hata almaz, sadece beklediğiniz gibi çalışmaz.
}
}2. Private Metotta @Async
@Service
public class MyService {
@Async
private void secretWork() {
// ❌ ÇALIŞMAZ — Proxy, private metotları intercept edemez
}
@Async
protected void protectedWork() {
// ❌ ÇALIŞMAZ — JDK dynamic proxy ile (CGLIB ile çalışabilir ama önerilmez)
}
@Async
public void publicWork() {
// ✅ Sadece public metotlar desteklenir
}
}3. Yanlış Dönüş Tipi
@Service
public class MyService {
@Async
public String getResult() {
// ❌ YANLIŞ — String dönemez!
// @Async metotlar: void, Future<T> veya CompletableFuture<T> dönmeli
return "result"; // Bu değer kaybolur, çağıran null alır
}
@Async
public CompletableFuture<String> getResultAsync() {
// ✅ DOĞRU
return CompletableFuture.completedFuture("result");
}
}4. SecurityContext ve MDC Kaybı
@Async
public void processWithUser() {
// ❌ SecurityContext KAYBOLUR — farklı thread!
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// auth = null veya Anonymous
// ❌ MDC değerleri de kaybolur — farklı thread!
String requestId = MDC.get("requestId");
// requestId = null
}
// ✅ ÇÖZÜM: TaskDecorator ile context propagation
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.initialize();
return executor;
}
}
public class ContextCopyingDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// Ana thread'den context'leri yakala
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
SecurityContext securityContext = SecurityContextHolder.getContext();
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return () -> {
try {
// Yeni thread'e context'leri aktar
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes);
}
SecurityContextHolder.setContext(securityContext);
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
runnable.run();
} finally {
// Temizle (thread reuse nedeniyle zorunlu)
RequestContextHolder.resetRequestAttributes();
SecurityContextHolder.clearContext();
MDC.clear();
}
};
}
}@Async Kuralları Özet Tablosu
| Kural | Açıklama |
|---|---|
@EnableAsync zorunlu | Konfigürasyon sınıfında açıkça etkinleştirilmeli |
public metot olmalı | Private/protected metotlarda çalışmaz |
| Self-invocation çalışmaz | Aynı sınıf içinden çağrı proxy'yi bypass eder |
| Dönüş tipi | void, Future<T> veya CompletableFuture<T> olmalı |
| Exception handling | void metotlarda AsyncUncaughtExceptionHandler kullanın |
| Transaction | @Async metot kendi transaction'ını açar, çağıranınkine katılmaz |
| SecurityContext | Farklı thread'e aktarılmaz, TaskDecorator gerekir |
| MDC | Farklı thread'e aktarılmaz, TaskDecorator gerekir |
| Executor | Belirtilmezse varsayılan kullanılır; production'da özel executor tanımlayın |
Gerçek Dünya Örneği: Kullanıcı Kayıt Akışı
@Service
@RequiredArgsConstructor
@Slf4j
public class UserRegistrationService {
private final UserRepository userRepository;
private final AsyncOnboardingService onboardingService;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public User register(RegistrationRequest request) {
// 1. Validasyon ve kayıt (senkron — kritik)
validateUniqueEmail(request.getEmail());
User user = userRepository.save(User.from(request));
// 2. Event yayınla — listener'lar asenkron çalışır
eventPublisher.publishEvent(new UserRegisteredEvent(user));
log.info("Kullanıcı kaydedildi: {} — onboarding arka planda başlıyor", user.getId());
return user;
}
}
@Component
@Slf4j
public class UserOnboardingListener {
@Async("onboardingExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onUserRegistered(UserRegisteredEvent event) {
User user = event.getUser();
log.info("Onboarding başlıyor: userId={}", user.getId());
// Bu işlemler arka planda, paralel çalışır
try {
welcomeEmailService.send(user);
log.info("Welcome e-postası gönderildi: {}", user.getEmail());
} catch (Exception e) {
log.error("Welcome e-postası gönderilemedi", e);
}
try {
avatarService.generateDefault(user);
log.info("Varsayılan avatar oluşturuldu");
} catch (Exception e) {
log.error("Avatar oluşturulamadı", e);
}
try {
analyticsService.trackSignup(user);
log.info("Signup analitik kaydedildi");
} catch (Exception e) {
log.error("Analitik kaydedilemedi", e);
}
}
}Bu yapıda kullanıcı kaydı milisaniyeler içinde tamamlanır. Hoşgeldin e-postası, avatar oluşturma ve analitik kaydı arka planda paralel çalışır. Herhangi biri başarısız olsa bile kullanıcı kaydı etkilenmez.
Özet
@Asyncanotasyonu, bir metodu ayrı thread'de çalıştırmanın en kolay yoludur.@EnableAsyncile aktifleştirilmeli, aksi halde sessizce görmezden gelinir.void metotlar fire-and-forget senaryolar için, CompletableFuture sonuca ihtiyaç duyulan senaryolar için kullanılır.
Future<T>eski usuldür,CompletableFuture<T>tercih edin.@Async, Spring AOP proxy mekanizmasına dayanır. Self-invocation (aynı sınıf içinden çağrı) proxy'yi bypass eder ve metot senkron çalışır. Çözüm: asenkron metodu ayrı servise taşıyın.void metotlarda exception sessizce kaybolur.
AsyncUncaughtExceptionHandlerile global hata yakalama tanımlayın. CompletableFuture'da.exceptionally()kullanın.@Asyncmetot kendi thread'inde çalıştığı için SecurityContext, MDC ve RequestAttributes aktarılmaz.TaskDecoratorile context propagation yapın.Production'da mutlaka özel executor tanımlayın. Varsayılan
SimpleAsyncTaskExecutorher çağrıda yeni thread oluşturur — bellek tükenmesine yol açar.
AI Asistan
Sorularını yanıtlamaya hazır