← Kursa Dön
📄 Text · 30 min

@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

PhaseNe Zaman?Aynı TX?Kullanım
AFTER_COMMIT (varsayılan)Commit sonrasıHayırE-posta, bildirim, cache invalidation
AFTER_ROLLBACKRollback sonrasıHayırHata raporlama, kompensasyon
AFTER_COMPLETIONHer durumdaHayırMetrik, monitoring
BEFORE_COMMITCommit öncesiEvetAudit 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 = true kullanı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:

KriterDirect CallEvent
BağımlılıkSıkı (tight coupling)Gevşek (loose coupling)
TestMock gerektirirBağımsız test edilebilir
TransactionAynı transactionAFTER_COMMIT ile ayrı
Hata etkisiÇağıranı etkilerİzole (async ise)
DebugStack trace netEvent akışını takip etmek zor
PerformansSenkron, tahmin edilebilirAsync ile non-blocking
KullanımZorunlu 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 event

Gerç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_COMMIT en yaygın kullanımdır — veri güvenli kaydedildikten sonra yan etkileri gerçekleştirin. BEFORE_COMMIT ile 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.