@EventListener Detaylı
Giriş
Önceki derste Spring Event sisteminin temellerini öğrendik — event yayınlama, @EventListener ile dinleme, gevşek bağlı mimari. Bu derste bir üst seviyeye geçiyoruz: SpEL ile koşullu filtreleme, @TransactionalEventListener ile transaction entegrasyonu, asenkron event'ler, event zinciri (propagation) ve production'da karşılaşılan gerçek sorunları çözmek.
Bir e-ticaret uygulamasını düşünün: sipariş oluşturulduğunda e-posta göndermek istiyorsunuz. Ama e-posta, sipariş veritabanına kesinlikle kaydedildikten sonra gönderilmeli. Transaction rollback olursa e-posta gönderilmemeli. İşte @TransactionalEventListener tam olarak bu sorunu çözer. Ve e-posta gönderimi 3 saniye sürüyorsa, kullanıcıyı bekletmek istemezsiniz — @Async ile listener'ı asenkron yaparsınız.
@EventListener Condition (SpEL ile Filtreleme)
@EventListener'a Spring Expression Language (SpEL) ile koşul ekleyerek, listener'ın sadece belirli koşulları sağlayan event'lerde çalışmasını sağlayabilirsiniz:
// Event tanımı
public record OrderStatusChangedEvent(
Long orderId,
OrderStatus oldStatus,
OrderStatus newStatus,
BigDecimal totalAmount,
String customerEmail
) {}@Component
@Slf4j
public class OrderEventListeners {
// Sadece iptal edilen siparişlerde çalışır
@EventListener(condition = "#event.newStatus.name() == 'CANCELLED'")
public void handleCancellation(OrderStatusChangedEvent event) {
refundService.processRefund(event.orderId());
log.info("Sipariş #{} iptal edildi — iade başlatıldı", event.orderId());
}
// Sadece kargoya verilen siparişlerde çalışır
@EventListener(condition = "#event.newStatus.name() == 'SHIPPED'")
public void handleShipment(OrderStatusChangedEvent event) {
trackingService.initTracking(event.orderId());
notificationService.sendShippedEmail(event.customerEmail());
}
// Sadece yüksek tutarlı siparişlerde (1000 TL üzeri)
@EventListener(condition = "#event.totalAmount > 1000")
public void handleHighValueOrder(OrderStatusChangedEvent event) {
vipService.assignPrioritySupport(event.orderId());
log.info("VIP sipariş #{} — öncelikli destek atandı", event.orderId());
}
// Birden fazla koşul (AND)
@EventListener(condition = "#event.newStatus.name() == 'COMPLETED' AND #event.totalAmount > 500")
public void handleLargeCompletedOrder(OrderStatusChangedEvent event) {
loyaltyService.awardPoints(event.orderId(), event.totalAmount());
}
}SpEL ifadesinde #event (veya #root.event, #root.args[0]) olay nesnesini temsil eder. Event nesnesinin getter metotlarına erişebilir, karşılaştırmalar ve mantıksal operatörler (AND, OR, NOT) kullanabilirsiniz.
SpEL Örnekleri
// String karşılaştırma
@EventListener(condition = "#event.source == 'WEB'")
// Null kontrolü
@EventListener(condition = "#event.userId != null")
// Collection kontrolü
@EventListener(condition = "#event.tags.contains('urgent')")
// Regex
@EventListener(condition = "#event.email matches '.*@company\\.com'")
// Metot çağrısı
@EventListener(condition = "#event.createdAt.isAfter(T(java.time.Instant).now().minusSeconds(3600))")@TransactionalEventListener
Çoğu zaman event'lerin transaction ile uyumlu çalışması gerekir. Veritabanına sipariş kaydedildikten sonra (transaction commit'lendikten sonra) e-posta göndermek istersiniz. Transaction rollback olursa e-posta gönderilmemelidir.
@TransactionalEventListener, event işleme zamanlamasını transaction fazına bağlar:
@Component
@Slf4j
public class OrderTransactionalListeners {
// ═══════════════════════════════════════════
// AFTER_COMMIT — En yaygın kullanım
// ═══════════════════════════════════════════
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendConfirmationEmail(OrderCreatedEvent event) {
// Transaction BAŞARIYLA commit'lendikten SONRA çalışır
// Sipariş kesinlikle veritabanına kaydedilmiştir
log.info("✅ Transaction commit'lendi — e-posta gönderiliyor");
emailService.sendOrderConfirmation(event.orderId(), event.customerEmail());
}
// ═══════════════════════════════════════════
// AFTER_ROLLBACK — Hata sonrası temizlik
// ═══════════════════════════════════════════
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void handleRollback(OrderCreatedEvent event) {
// Transaction ROLLBACK'ten sonra çalışır
log.warn("⚠️ Transaction rollback — sipariş #{} kaydedilemedi", event.orderId());
errorTrackingService.reportFailedOrder(event.orderId());
metricsService.incrementCounter("orders.failed");
}
// ═══════════════════════════════════════════
// AFTER_COMPLETION — Her durumda
// ═══════════════════════════════════════════
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
public void recordMetrics(OrderCreatedEvent event) {
// Commit VEYA rollback — her durumda çalışır
metricsService.recordOrderAttempt();
log.info("📊 Sipariş denemesi kaydedildi (başarılı/başarısız)");
}
// ═══════════════════════════════════════════
// BEFORE_COMMIT — Aynı transaction içinde
// ═══════════════════════════════════════════
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void auditBeforeCommit(OrderCreatedEvent event) {
// Commit öncesi çalışır — AYNI transaction içinde!
// Bu kayıt, ana siparişle birlikte commit/rollback olur
auditRepository.save(new AuditLog("ORDER_CREATED", event.orderId()));
log.info("📋 Audit log kaydedildi (commit öncesi, aynı tx)");
}
}Transaction Phase Özeti
| Phase | Ne Zaman? | Aynı TX? | Kullanım |
|---|---|---|---|
AFTER_COMMIT (varsayılan) | Commit sonrası | Hayır | E-posta, bildirim, cache invalidation |
AFTER_ROLLBACK | Rollback sonrası | Hayır | Hata raporlama, kompensasyon |
AFTER_COMPLETION | Her durumda | Hayır | Metrik, monitoring |
BEFORE_COMMIT | Commit öncesi | Evet | Audit log, validasyon, tutarlılık kontrolleri |
AFTER_COMMIT'te Yeni Transaction
AFTER_COMMIT fazında veritabanı işlemi yapmanız gerekiyorsa, yeni bir transaction başlatmanız gerekir (önceki zaten commit'lenmiş):
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW) // Yeni transaction aç
public void updateReadModel(OrderCreatedEvent event) {
// Önceki transaction commit'lendi, bu yeni bir transaction
orderReadModelRepository.updateDenormalizedView(event.orderId());
}fallbackExecution
@TransactionalEventListener, varsayılan olarak sadece aktif bir transaction varken çalışır. Transaction yoksa event sessizce görmezden gelinir. Bu davranışı değiştirmek için:
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT,
fallbackExecution = true // Transaction yoksa bile çalış
)
public void handleEvent(UserRegisteredEvent event) {
// Transaction varsa → commit sonrası çalışır
// Transaction yoksa → hemen çalışır (normal @EventListener gibi)
}⚠️ Dikkat:
fallbackExecution = truekullanırken, listener'ın her iki durumda da (tx var/yok) doğru çalıştığından emin olun.
Asenkron Events
Varsayılan olarak event'ler senkron çalışır — publisher, tüm listener'lar tamamlanana kadar bekler. Uzun süren listener'lar publisher'ı yavaşlatabilir.
@Async ile Asenkron Listener
@Component
@Slf4j
public class AsyncEventListeners {
// Asenkron — publisher'ı bloklamaz
@Async
@EventListener
public void sendWelcomeEmail(UserRegisteredEvent event) {
log.info("[{}] Welcome e-postası gönderiliyor",
Thread.currentThread().getName());
emailService.sendWelcomeEmail(event.getUser());
// 3 saniye sürer ama publisher beklemez
}
// Belirli executor ile asenkron
@Async("notificationExecutor")
@EventListener
public void sendPushNotification(UserRegisteredEvent event) {
pushService.sendWelcomeNotification(event.getUser());
}
// Asenkron + TransactionalEventListener — EN GÜÇLÜ KOMBİNASYON
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void processAfterCommit(OrderCreatedEvent event) {
// 1. Transaction commit'lenmiş (veri güvenli)
// 2. Ayrı thread'de çalışıyor (publisher'ı bloklamıyor)
// Bu, production'daki en yaygın pattern'dır
emailService.sendOrderConfirmation(event.orderId());
analyticsService.trackOrder(event.orderId());
}
}Asenkron Event'lerde Exception Handling
// ❌ Asenkron listener'da exception KAYBOLUR — çağırana iletilmez
@Async
@EventListener
public void riskyHandler(OrderCreatedEvent event) {
throw new RuntimeException("Bu hata nereye gider?");
// Cevap: AsyncUncaughtExceptionHandler'a (tanımlıysa)
// Tanımlı değilse sessizce kaybolur!
}
// ✅ Her asenkron listener'da try-catch kullanın
@Async
@EventListener
public void safeHandler(OrderCreatedEvent event) {
try {
emailService.sendConfirmation(event.orderId());
} catch (Exception e) {
log.error("❌ E-posta gönderimi başarısız — orderId={}", event.orderId(), e);
// Retry queue'ya ekle, alert gönder, metrik kaydet
retryService.scheduleRetry("email-confirmation", event.orderId());
}
}Event Propagation — Zincir Event'ler
Bir event listener, başka bir event yayınlayarak event zinciri oluşturabilir:
Manuel Event Zincirleme
@Component
@RequiredArgsConstructor
public class OrderWorkflow {
private final ApplicationEventPublisher eventPublisher;
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
paymentService.charge(event.orderId());
eventPublisher.publishEvent(new PaymentCompletedEvent(event.orderId()));
}
@EventListener
public void onPaymentCompleted(PaymentCompletedEvent event) {
inventoryService.reserve(event.orderId());
eventPublisher.publishEvent(new InventoryReservedEvent(event.orderId()));
}
@EventListener
public void onInventoryReserved(InventoryReservedEvent event) {
shippingService.schedule(event.orderId());
}
}Otomatik Event Propagation (Return ile)
@EventListener metodu bir nesne döndürürse, bu nesne otomatik olarak yeni event olarak yayınlanır:
// Dönüş değeri otomatik olarak publish edilir
@EventListener
public PaymentCompletedEvent onOrderCreated(OrderCreatedEvent event) {
paymentService.charge(event.orderId());
return new PaymentCompletedEvent(event.orderId()); // Otomatik publish!
}
// Birden fazla event döndürmek için koleksiyon kullanın
@EventListener
public List<Object> onOrderCreated(OrderCreatedEvent event) {
return List.of(
new AuditEvent("ORDER_CREATED", event.orderId()),
new NotificationEvent("Yeni sipariş", event.customerEmail()),
new AnalyticsEvent("order_created", event.totalAmount())
);
// Üç event de otomatik olarak publish edilir
}
// void döndürmek → event yayınlamama
// null döndürmek → event yayınlamama⚠️ Dikkat: Event zincirleri döngüye girebilir! A → B → C → A gibi bir zincir sonsuz döngüye yol açar. Event tasarımında buna dikkat edin.
Event vs Direct Service Call — Ne Zaman Hangisi?
Bu soru, her Spring geliştiricisinin karşılaştığı önemli bir mimari karardır:
| Kriter | Direct Call | Event |
|---|---|---|
| Bağımlılık | Sıkı (tight coupling) | Gevşek (loose coupling) |
| Test | Mock gerektirir | Bağımsız test edilebilir |
| Transaction | Aynı transaction | AFTER_COMMIT ile ayrı |
| Hata etkisi | Çağıranı etkiler | İzole (async ise) |
| Debug | Stack trace net | Event akışını takip etmek zor |
| Performans | Senkron, tahmin edilebilir | Async ile non-blocking |
| Kullanım | Zorunlu iş mantığı | Yan etkiler (side effects) |
Karar Kuralı
Metot dönüş değerine ihtiyacınız var mı?
→ Evet → Direct Call (paymentService.charge(order) → PaymentResult)
→ Hayır (yan etki) → Event
İşlem başarısız olursa ana işlem de başarısız olmalı mı?
→ Evet → Direct Call (stok yoksa sipariş da oluşmamalı)
→ Hayır → Event (e-posta gönderilmese de sipariş oluşsun)
İşlem aynı transaction'da olmalı mı?
→ Evet → Direct Call veya BEFORE_COMMIT
→ Hayır → AFTER_COMMIT eventGerçek Dünya Örneği: Tam Sipariş Akışı
// ═══════════════════════════════════════════
// 1. SERVİS — Sipariş oluştur + event yayınla
// ═══════════════════════════════════════════
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService; // Direct call — sonuç gerekiyor
private final InventoryService inventoryService; // Direct call — başarısız olursa sipariş da iptal
private final ApplicationEventPublisher publisher;
@Transactional
public Order createOrder(OrderRequest request) {
// 1. Kritik iş mantığı — direct call (aynı transaction)
inventoryService.reserve(request.getItems());
PaymentResult payment = paymentService.charge(request);
// 2. Sipariş kaydet
Order order = orderRepository.save(Order.from(request, payment));
// 3. Event yayınla — yan etkiler listener'larda
publisher.publishEvent(new OrderCreatedEvent(
order.getId(),
request.getCustomerEmail(),
order.getTotalAmount()
));
return order;
}
}
// ═══════════════════════════════════════════
// 2. LISTENER'LAR — Bağımsız, izole
// ═══════════════════════════════════════════
// E-posta — commit sonrası, asenkron
@Component
public class OrderEmailListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendConfirmation(OrderCreatedEvent event) {
try {
emailService.sendOrderConfirmation(event.customerEmail(), event.orderId());
} catch (Exception e) {
log.error("E-posta gönderilemedi — orderId={}", event.orderId(), e);
}
}
}
// Audit — commit öncesi, aynı transaction
@Component
public class OrderAuditListener {
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void audit(OrderCreatedEvent event) {
auditRepository.save(new AuditLog("ORDER_CREATED", event.orderId()));
}
}
// Analitik — commit sonrası, asenkron
@Component
public class OrderAnalyticsListener {
@Async("analyticsExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void track(OrderCreatedEvent event) {
try {
analyticsService.track("order_created", Map.of(
"orderId", event.orderId().toString(),
"amount", event.totalAmount().toString()
));
} catch (Exception e) {
log.warn("Analitik kaydedilemedi", e);
}
}
}
// Cache — commit sonrası
@Component
public class OrderCacheListener {
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void invalidateCache(OrderCreatedEvent event) {
cacheManager.getCache("orders").evict(event.orderId());
cacheManager.getCache("order-count").clear();
}
}Özet
SpEL condition ile listener'ları filtreleyerek, aynı event'in farklı koşullarda farklı tepkiler vermesini sağlayın. Bu, if-else zincirinden çok daha temizdir.
@TransactionalEventListener, event işlemeyi transaction fazına bağlar.
AFTER_COMMITen yaygın kullanımdır — veri güvenli kaydedildikten sonra yan etkileri gerçekleştirin.BEFORE_COMMITile aynı transaction'da audit log kaydedin.@Async + @TransactionalEventListener kombinasyonu production'daki en güçlü pattern'dır: transaction güvenliği + non-blocking performans.
Asenkron listener'larda exception sessizce kaybolur. Her asenkron listener'da try-catch kullanın ve hataları loglayın/metrik kaydedin.
Event listener dönüş değeri otomatik olarak yeni event olarak yayınlanır — event zinciri oluşturmak için kullanışlıdır. Ancak sonsuz döngüye dikkat edin.
Direct call zorunlu iş mantığı ve dönüş değeri gereken senaryolar için, event yan etkiler (e-posta, bildirim, loglama, cache) için kullanın. Bu ayrım, kodunuzu Single Responsibility ve Open/Closed prensiplerine uygun tutar.
fallbackExecution = true ayarı, transaction olmayan ortamlarda da çalışmayı sağlar. Test ve non-transactional senaryolarda event'lerin kaybolmasını önlemek için kullanışlıdır.
Event sistemi, Domain-Driven Design (DDD) pratiğinde domain event'lerin temelidir. Aggregate'ler arası iletişimi direct call yerine event ile yapmak, bounded context'ler arasında gevşek bağlılık sağlar ve microservice'e geçiş yolunu hazırlar.
AI Asistan
Sorularını yanıtlamaya hazır