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ı:
UserService 5 servise bağımlı — constructor şişiyor, test yazmak zorlaşıyor
Open/Closed Principle ihlali — yeni bir yan etki eklemek istediğinizde UserService'i değiştirmeniz gerekiyor
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)
Performans — tüm yan etkiler senkron çalışır, kullanıcı tüm işlemler bitene kadar bekler
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
recordkullanmak 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 kadarpublishEvent()satırı geri dönmez. Asenkron event'ler için listener'a@Asyncekleyin (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);
}
}| Event | Ne Zaman? | Kullanım |
|---|---|---|
ApplicationStartingEvent | Uygulama başlamaya başladığında | Çok erken aşama, listener kaydedilmemiş olabilir |
ApplicationEnvironmentPreparedEvent | Environment hazır, context oluşturulmamış | Konfigürasyon doğrulama |
ApplicationContextInitializedEvent | Context oluşturuldu, bean'ler yüklenmedi | Erken müdahale |
ApplicationPreparedEvent | Bean tanımları yüklendi, refresh yapılmadı | Bean post-processing |
ContextRefreshedEvent | Context refresh tamamlandı | Bean'ler hazır |
ApplicationStartedEvent | Context refresh + runner'lar çalışmadan önce | Hazırlık |
ApplicationReadyEvent | Uygulama tamamen hazır | Cache warming, health check |
ContextClosedEvent | Uygulama kapanıyor | Kaynak 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:
Listener'ları `@Async` yapın — publisher'ı bloklamaz
`@TransactionalEventListener` kullanın — transaction commit sonrası çalışır
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 —
ApplicationEventextend etmeye gerek yok. Javarecordkullanarak immutable event'ler tanımlayın.ApplicationEventPublisherile event yayınlanır,@EventListenerile 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.
AI Asistan
Sorularını yanıtlamaya hazır