← Kursa Dön
📄 Text · 30 min

Spring Events

Giriş

Bir şirketteki duyuru panosunu düşünün. İK departmanı "Yeni çalışan katıldı" duyurusunu panoya asar. Bu duyuruyu gören IT departmanı laptop hazırlar, güvenlik departmanı kartını basar, muhasebe maaş hesabını açar. İK departmanı bu departmanların varlığından bile haberdar olmak zorunda değildir — duyuruyu asar, ilgilenen dinler. Yarın pazarlama departmanı da "hoşgeldin paketi hazırla" diye dinlemeye başlasa, İK'nın kodunda hiçbir değişiklik gerekmez.

Spring'in event (olay) sistemi, uygulama bileşenleri arasında gevşek bağlı (loosely coupled) iletişim sağlar. Observer (gözlemci) tasarım desenine dayanan bu mekanizma ile bir bileşen olay yayınlar, ilgilenen diğer bileşenler bu olayı dinler ve tepki verir. Yayınlayan ve dinleyen birbirini doğrudan tanımak zorunda değildir — bu, modüler ve bakımı kolay uygulamalar oluşturmanın temelidir.

Bu derste event sisteminin motivasyonunu, custom event tanımlamayı, ApplicationEventPublisher ile yayınlamayı, @EventListener ile dinlemeyi, event sıralama ve generic event'leri, built-in Spring event'lerini ve gerçek dünya kullanım kalıplarını derinlemesine öğreneceğiz.


Neden Event Kullanılır?

Event Olmadan: Sıkı Bağımlılık (Tight Coupling)

@Service
public class UserService {
    
    // ❌ 5 farklı servise doğrudan bağımlılık!
    private final EmailService emailService;
    private final AuditService auditService;
    private final NotificationService notificationService;
    private final AnalyticsService analyticsService;
    private final CacheService cacheService;
    
    // Constructor'da 5 parametre... test yazarken 5 mock...
    
    public User register(UserRegistrationDto dto) {
        User user = createUser(dto);
        
        emailService.sendWelcome(user);             // ❌ sıkı bağımlılık
        auditService.logRegistration(user);          // ❌ sıkı bağımlılık
        notificationService.notifyAdmins(user);      // ❌ sıkı bağımlılık
        analyticsService.trackSignup(user);          // ❌ sıkı bağımlılık
        cacheService.invalidateUserCount();           // ❌ sıkı bağımlılık
        
        return user;
    }
}

Bu yaklaşımın sorunları:

  1. UserService 5 servise bağımlı — constructor şişiyor, test yazmak zorlaşıyor

  2. Open/Closed Principle ihlali — yeni bir yan etki eklemek istediğinizde UserService'i değiştirmeniz gerekiyor

  3. Hata yayılımı — emailService hata fırlatırsa kullanıcı kaydı başarısız olabilir (ki e-posta gönderimi kritik değil)

  4. Performans — tüm yan etkiler senkron çalışır, kullanıcı tüm işlemler bitene kadar bekler

  5. Circular dependency riski — servisler arasında karşılıklı bağımlılık oluşabilir

Event ile: Gevşek Bağımlılık (Loose Coupling)

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    private final ApplicationEventPublisher eventPublisher;
    // Sadece 2 bağımlılık! E-posta, audit, bildirim... hiçbirini bilmiyor.
    
    @Transactional
    public User register(UserRegistrationDto dto) {
        User user = userRepository.save(createUser(dto));
        
        // Tek satır: "Yeni kullanıcı kaydedildi" olayını yayınla
        eventPublisher.publishEvent(new UserRegisteredEvent(user));
        
        return user;
        // ✅ UserService sadece kendi işini yapar: kullanıcı kaydet, olay yayınla
        // Kim dinliyor, kaç kişi dinliyor — UserService bilmez ve bilmek zorunda değil
    }
}

Artık her yan etki bağımsız bir listener tarafından gerçekleştirilir:

@Component
@Slf4j
public class WelcomeEmailListener {
    @EventListener
    public void handle(UserRegisteredEvent event) {
        emailService.sendWelcome(event.getUser());
    }
}

@Component
@Slf4j
public class AuditListener {
    @EventListener
    public void handle(UserRegisteredEvent event) {
        auditService.log("USER_REGISTERED", event.getUser().getId());
    }
}

@Component
@Slf4j
public class AnalyticsListener {
    @EventListener
    public void handle(UserRegisteredEvent event) {
        analyticsService.track("signup", event.getUser());
    }
}

Yeni bir yan etki eklemek mi istiyorsunuz? Sadece yeni bir listener yazın — UserService'e dokunmanıza gerek yok:

// Yeni eklenen — UserService hiç değişmedi!
@Component
public class LoyaltyPointsListener {
    @EventListener
    public void handle(UserRegisteredEvent event) {
        loyaltyService.giveWelcomePoints(event.getUser(), 100);
    }
}

Custom Event Tanımlama

Yöntem 1: ApplicationEvent Extend (Eski Yöntem)

public class UserRegisteredEvent extends ApplicationEvent {
    
    private final User user;
    
    public UserRegisteredEvent(Object source, User user) {
        super(source);  // source: olayı yayınlayan bean
        this.user = user;
    }
    
    public User getUser() { return user; }
}

// Yayınlama
eventPublisher.publishEvent(new UserRegisteredEvent(this, user));

Yöntem 2: POJO Event (Spring 4.2+ — Önerilen)

// Herhangi bir sınıf event olabilir — hiçbir sınıfı extend etmeye gerek yok!
public class UserRegisteredEvent {
    
    private final User user;
    private final Instant registeredAt;
    
    public UserRegisteredEvent(User user) {
        this.user = user;
        this.registeredAt = Instant.now();
    }
    
    public User getUser() { return user; }
    public Instant getRegisteredAt() { return registeredAt; }
}

// Yayınlama — aynı şekilde
eventPublisher.publishEvent(new UserRegisteredEvent(user));

Modern Spring uygulamalarında POJO event tercih edilir — daha temiz, framework'e bağımlılık yok, test etmesi kolay.

İyi Event Tasarımı

// ✅ İyi event: immutable, gerekli bilgiyi taşıyor
public record OrderCreatedEvent(
    Long orderId,
    Long customerId,
    BigDecimal totalAmount,
    Instant createdAt
) {
    public OrderCreatedEvent(Order order) {
        this(order.getId(), order.getCustomerId(), 
             order.getTotalAmount(), Instant.now());
    }
}

// ❌ Kötü event: tüm entity'yi taşıyor (lazy loading sorunları, gereksiz veri)
public class BadOrderEvent {
    private final Order order;  // JPA entity — lazy loading sorunları!
    // Listener'da order.getItems() çağrılırsa LazyInitializationException!
}

💡 İpucu: Event nesnelerinde JPA entity yerine gerekli alanları (id, amount, status) taşıyın. Event, transaction dışında tüketiliyorsa lazy loading sorunları yaşarsınız. Java record kullanmak immutability'yi garanti eder.


ApplicationEventPublisher

Event yayınlamak için ApplicationEventPublisher kullanılır. Spring, bu arayüzü otomatik olarak inject eder:

@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = orderRepository.save(mapToOrder(request));
        
        // Event yayınla — kim dinlerse dinlesin
        eventPublisher.publishEvent(new OrderCreatedEvent(order));
        
        return order;
    }
    
    @Transactional
    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
        
        eventPublisher.publishEvent(new OrderCancelledEvent(order));
    }
    
    @Transactional
    public void shipOrder(Long orderId) {
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus(OrderStatus.SHIPPED);
        orderRepository.save(order);
        
        eventPublisher.publishEvent(new OrderShippedEvent(order));
    }
}

Not: publishEvent() varsayılan olarak senkron çalışır. Yani tüm listener'lar tamamlanana kadar publishEvent() satırı geri dönmez. Asenkron event'ler için listener'a @Async ekleyin (bir sonraki derste detaylı).


@EventListener

Event'leri dinlemenin modern yolu @EventListener anotasyonudur. Herhangi bir Spring bean'indeki public metoda eklenebilir:

@Component
@Slf4j
public class EmailEventListener {
    
    private final EmailService emailService;
    
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        User user = event.getUser();
        log.info("📧 Welcome e-postası gönderiliyor: {}", user.getEmail());
        emailService.sendWelcomeEmail(user);
    }
}

@Component
@Slf4j
public class AuditEventListener {
    
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        log.info("📋 Audit log: kullanıcı kaydedildi — {}", event.getUser().getUsername());
        auditService.log("USER_REGISTERED", event.getUser().getId());
    }
}

@Component
@Slf4j
public class AnalyticsEventListener {
    
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        analyticsService.track("signup", Map.of(
            "userId", event.getUser().getId().toString(),
            "timestamp", event.getRegisteredAt().toString()
        ));
    }
}

Her listener bağımsızdır ve kendi sorumluluğuna odaklanır. Bir listener hata fırlatsa bile diğer listener'lar etkilenmez (senkron modda exception yayılır — buna dikkat).


Event Sıralama (@Order)

Birden fazla listener aynı event'i dinlediğinde, çalışma sırasını @Order ile belirleyebilirsiniz:

@Component
public class ValidationListener {
    
    @EventListener
    @Order(1)  // İlk çalışır
    public void validate(OrderCreatedEvent event) {
        log.info("1️⃣ Sipariş doğrulanıyor");
        if (event.totalAmount().compareTo(BigDecimal.ZERO) <= 0) {
            throw new InvalidOrderException("Sipariş tutarı pozitif olmalı");
        }
    }
}

@Component
public class InventoryListener {
    
    @EventListener
    @Order(2)  // İkinci çalışır
    public void reserveStock(OrderCreatedEvent event) {
        log.info("2️⃣ Stok ayrılıyor");
        inventoryService.reserve(event.orderId());
    }
}

@Component
public class NotificationListener {
    
    @EventListener
    @Order(3)  // Son çalışır
    public void notify(OrderCreatedEvent event) {
        log.info("3️⃣ Bildirim gönderiliyor");
        notificationService.sendOrderConfirmation(event.orderId());
    }
}

Düşük @Order değeri → daha erken çalışır. @Order belirtilmezse sıra belirsizdir.


Koşullu Event Dinleme

@EventListener'a SpEL (Spring Expression Language) ile koşul ekleyebilirsiniz:

// Sadece VIP müşterilerin siparişlerinde çalışır
@EventListener(condition = "#event.customer.vip == true")
public void handleVipOrder(OrderCreatedEvent event) {
    vipService.assignPrioritySupport(event.orderId());
}

// Sadece yüksek tutarlı siparişlerde
@EventListener(condition = "#event.totalAmount > 1000")
public void handleHighValueOrder(OrderCreatedEvent event) {
    fraudDetectionService.review(event.orderId());
}

// Birden fazla event tipi dinleme
@EventListener({OrderCreatedEvent.class, OrderUpdatedEvent.class})
public void handleOrderChange(Object event) {
    log.info("Sipariş değişikliği algılandı: {}", event);
}

Generic Events

Tip parametreli (generic) event'ler de oluşturabilirsiniz. Spring, ResolvableTypeProvider arayüzü ile generic tipi çözümleyebilir:

public class EntityCreatedEvent<T> implements ResolvableTypeProvider {
    
    private final T entity;
    private final Instant createdAt;
    
    public EntityCreatedEvent(T entity) {
        this.entity = entity;
        this.createdAt = Instant.now();
    }
    
    public T getEntity() { return entity; }
    public Instant getCreatedAt() { return createdAt; }
    
    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(
            getClass(), ResolvableType.forInstance(this.entity)
        );
    }
}

// Yayınlama — farklı entity tipleri
eventPublisher.publishEvent(new EntityCreatedEvent<>(user));
eventPublisher.publishEvent(new EntityCreatedEvent<>(order));
eventPublisher.publishEvent(new EntityCreatedEvent<>(product));

// Dinleme — her biri sadece kendi tipini dinler
@EventListener
public void handleUserCreated(EntityCreatedEvent<User> event) {
    log.info("User oluşturuldu: {}", event.getEntity().getUsername());
}

@EventListener
public void handleOrderCreated(EntityCreatedEvent<Order> event) {
    log.info("Order oluşturuldu: #{}", event.getEntity().getId());
}

@EventListener
public void handleProductCreated(EntityCreatedEvent<Product> event) {
    log.info("Product oluşturuldu: {}", event.getEntity().getName());
}

Built-in Spring Events

Spring, uygulama yaşam döngüsü ile ilgili hazır event'ler sağlar:

@Component
@Slf4j
public class ApplicationLifecycleListener {
    
    @EventListener
    public void onReady(ApplicationReadyEvent event) {
        log.info("✅ Uygulama tamamen hazır — trafik alınabilir");
        // Cache warming, başlangıç verileri yükleme
        cacheWarmupService.warmAll();
    }
    
    @EventListener
    public void onStarted(ApplicationStartedEvent event) {
        log.info("🚀 Uygulama başlatıldı (runner'lar çalışmadan önce)");
    }
    
    @EventListener
    public void onShutdown(ContextClosedEvent event) {
        log.info("🛑 Uygulama kapanıyor — kaynak temizliği");
        connectionPool.drain();
        scheduledTasks.cancelAll();
    }
    
    @EventListener
    public void onRefresh(ContextRefreshedEvent event) {
        log.info("🔄 ApplicationContext yenilendi");
    }
    
    // Servlet context hazır olduğunda (web uygulamaları için)
    @EventListener
    public void onServletReady(ServletWebServerInitializedEvent event) {
        int port = event.getWebServer().getPort();
        log.info("🌐 Web sunucusu hazır — port: {}", port);
    }
}
EventNe Zaman?Kullanım
ApplicationStartingEventUygulama başlamaya başladığındaÇok erken aşama, listener kaydedilmemiş olabilir
ApplicationEnvironmentPreparedEventEnvironment hazır, context oluşturulmamışKonfigürasyon doğrulama
ApplicationContextInitializedEventContext oluşturuldu, bean'ler yüklenmediErken müdahale
ApplicationPreparedEventBean tanımları yüklendi, refresh yapılmadıBean post-processing
ContextRefreshedEventContext refresh tamamlandıBean'ler hazır
ApplicationStartedEventContext refresh + runner'lar çalışmadan önceHazırlık
ApplicationReadyEventUygulama tamamen hazırCache warming, health check
ContextClosedEventUygulama kapanıyorKaynak temizliği

Senkron Event'lerin Tuzakları

Varsayılan olarak event'ler senkron çalışır. Bu önemli sonuçlar doğurur:

@Transactional
public User register(UserRegistrationDto dto) {
    User user = userRepository.save(createUser(dto));
    
    // publishEvent SENKRON — tüm listener'lar burada çalışır
    eventPublisher.publishEvent(new UserRegisteredEvent(user));
    // ↑ Eğer bir listener 5 saniye sürerse, register metodu da 5 saniye uzar!
    // ↑ Eğer bir listener exception fırlatırsa, transaction ROLLBACK olur!
    
    return user;
}

Bu tuzaklardan kaçınmak için:

  1. Listener'ları `@Async` yapın — publisher'ı bloklamaz

  2. `@TransactionalEventListener` kullanın — transaction commit sonrası çalışır

  3. Listener'da try-catch kullanın — bir listener'ın hatası diğerlerini etkilemesin

Bu konuların detayları bir sonraki derste (@EventListener Detaylı) incelenecek.


Yaygın Hatalar

1. JPA Entity'yi Doğrudan Event'e Koymak

// ❌ YANLIŞ — lazy loading, detached entity sorunları
public class OrderEvent {
    private final Order order;  // JPA entity!
}

// Listener'da: order.getItems() → LazyInitializationException
// Çünkü event senkronsa transaction içinde olabilir, ama async ise dışında

// ✅ DOĞRU — gerekli verileri DTO olarak taşıyın
public record OrderCreatedEvent(Long orderId, BigDecimal amount, String customerEmail) {}

2. Listener'da Exception Yönetimi

// ❌ YANLIŞ — exception publisher'a yayılır, transaction rollback olabilir
@EventListener
public void handle(OrderCreatedEvent event) {
    emailService.send(...); // Exception fırlatırsa sipariş kaydı da geri alınır!
}

// ✅ DOĞRU — yan etkiler için try-catch
@EventListener
public void handle(OrderCreatedEvent event) {
    try {
        emailService.send(...);
    } catch (Exception e) {
        log.error("E-posta gönderilemedi — sipariş etkilenmez", e);
    }
}

Gerçek Dünya Örneği: Sipariş Yaşam Döngüsü

// Event tanımları
public record OrderCreatedEvent(Long orderId, Long customerId, BigDecimal amount) {}
public record OrderPaidEvent(Long orderId, String paymentId) {}
public record OrderShippedEvent(Long orderId, String trackingNumber) {}
public record OrderCancelledEvent(Long orderId, String reason) {}

// Publisher — sadece event yayınlar
@Service
@RequiredArgsConstructor
public class OrderService {
    private final ApplicationEventPublisher publisher;
    
    @Transactional
    public Order createOrder(OrderRequest req) {
        Order order = orderRepository.save(new Order(req));
        publisher.publishEvent(new OrderCreatedEvent(order.getId(), req.customerId(), order.getTotal()));
        return order;
    }
}

// Listener'lar — her biri kendi işine odaklanır
@Component
public class OrderInventoryListener {
    @EventListener
    @Order(1)
    public void reserveStock(OrderCreatedEvent e) {
        inventoryService.reserve(e.orderId());
    }
}

@Component
public class OrderNotificationListener {
    @EventListener
    public void sendConfirmation(OrderCreatedEvent e) {
        notificationService.sendOrderConfirmation(e.customerId(), e.orderId());
    }
    
    @EventListener
    public void sendShippingNotification(OrderShippedEvent e) {
        notificationService.sendShippingUpdate(e.orderId(), e.trackingNumber());
    }
    
    @EventListener
    public void sendCancellationNotification(OrderCancelledEvent e) {
        notificationService.sendCancellationEmail(e.orderId(), e.reason());
    }
}

@Component
public class OrderAnalyticsListener {
    @EventListener
    public void trackOrder(OrderCreatedEvent e) {
        analyticsService.track("order_created", Map.of("amount", e.amount().toString()));
    }
    
    @EventListener
    public void trackCancellation(OrderCancelledEvent e) {
        analyticsService.track("order_cancelled", Map.of("reason", e.reason()));
    }
}

Bu yapıda sipariş yaşam döngüsündeki her olay bağımsız listener'lar tarafından karşılanır. Yeni bir gereksinim (örneğin "siparişi CRM'e kaydet") eklemek, sadece yeni bir listener yazmak demektir — mevcut hiçbir kod değişmez.


Özet

  • Spring event sistemi, Observer deseni ile bileşenler arası gevşek bağlı iletişim sağlar. Publisher, listener'ları bilmez — modüler ve bakımı kolay mimari.

  • POJO event (Spring 4.2+) tercih edin — ApplicationEvent extend etmeye gerek yok. Java record kullanarak immutable event'ler tanımlayın.

  • ApplicationEventPublisher ile event yayınlanır, @EventListener ile dinlenir. Her listener bağımsız ve tek sorumluluğa sahip olmalı.

  • `@Order` ile listener çalışma sırasını belirleyin. SpEL condition ile koşullu dinleme yapın. Generic event'ler ile tekrardan kaçının.

  • Varsayılan olarak event'ler senkron çalışır — listener yavaşsa publisher da yavaşlar, listener exception fırlatırsa transaction etkilenir. Asenkron ve transactional event'ler sonraki derste.

  • Event nesnelerinde JPA entity yerine gerekli alanları (id, amount, email) taşıyın. Lazy loading ve detached entity sorunlarından kaçının.