Spring'in İçindeki Kalıplar — Strategy, Template Method, Proxy, Observer
Önce şunu oku: Tasarım Kalıpları — Ne Zaman Kazandırır
30 saniyede özet
Spring Boot'ta tasarım kalıplarını çoğu zaman kendin yazmazsın; Spring onları senin için kurar. Asıl iş, hangisinin ne zaman çalıştığını bilmek; özellikle bir olay dinleyicisinin kayıttan önce mi sonra mı çalıştığını.
Bir Spring Boot servisinde her gün en az dört tasarım kalıbı kullanıyorsun, çoğunu adını anmadan. Sorun kalıbı bilmemek değil. Sorun, çerçevenin onu nerede ve ne zaman çalıştırdığını bilmemek.
-
Bayt: Müşteri siparişin alındı e-postası almış, ama sipariş hiç kaydedilmemiş!
-
Sen: Nasıl olur? Kodda önce kaydet, sonra olay yayınla yazıyor.
-
Bayt: Yazıyor. Asıl soru, dinleyicinin kayıt kesinleşmeden önce mi sonra mı çalıştığı.
-
Bayt: Restoranda tatlı hesaptan önce gelirse ne olur? Önce tahmin et, sonra izle.
Strategy: yeni davranış, yeni bir sınıf
StrategyAynı işin farklı yapılış biçimlerini ayrı nesnelere taşıyan kalıp. Çağıran hangi stratejinin çalıştığını bilmez; yeni bir biçim yeni bir sınıftır.Sözlükte gör →, aynı işin farklı yapılış biçimlerini ayrı nesnelere taşır. Spring’de bunu kurmak için ayrı bir fabrika bile gerekmez:
public interface PaymentMethod { String key(); // "card", "wallet", "bnpl" Receipt pay(Order order);}
@Component class CardPayment implements PaymentMethod { … }@Component class WalletPayment implements PaymentMethod { … }
@Serviceclass Checkout { private final Map<String, PaymentMethod> methods;
Checkout(List<PaymentMethod> all) { this.methods = all.stream().collect(toUnmodifiableMap(PaymentMethod::key, m -> m)); }
Receipt pay(Order order, String method) { return Optional.ofNullable(methods.get(method)) .orElseThrow(() -> new UnsupportedPaymentMethod(method)) .pay(order); }}Spring bu tipteki her bean’i listeye koyar. Yeni bir ödeme yöntemi yeni bir sınıftır; Checkout hiç değişmez.
Anahtarı key()’den almak bilinçli: Map<String, PaymentMethod> enjekte etseydin anahtar bean adı olurdu ve sınıfı yeniden adlandırmak API’yi sessizce değiştirirdi.
toUnmodifiableMap ise aynı anahtar iki kez gelirse uygulamanın açılmasını engeller.
Kafam karıştı, daha basit anlat
Her ödeme yöntemi kendi sınıfıdır ve Spring hepsini bir listede sana verir. Yeni bir yöntem eklemek, yeni bir sınıf yazmak kadar kolaydır.
Checkout, kurucusunda List<PaymentMethod> alıyor. Yeni bir ödeme yöntemi eklemek için ne yapman gerekir?
Map<String, PaymentMethod> enjekte ettiğinde Spring anahtar olarak neyi kullanır ve bunun riski nedir?
Spring’in senin yerine yazdığı kalıplar
Template MethodBir işin değişmeyen iskeletini tek yerde tutup değişen adımları dışarıdan (alt sınıf ya da callback olarak) alan kalıp. Spring'de JdbcTemplate ve TransactionTemplate bunun örneği.Sözlükte gör → bir işin değişmeyen iskeletini tek yerde tutar. Değişen adımı sen verirsin:
| Şablon | İskelet (Spring yapar) | Değişen adım (sen verirsin) |
|---|---|---|
JdbcTemplate | Bağlantı al/bırak, statement kapat, SQLException → DataAccessException | SQL ve RowMapper |
TransactionTemplate | Başlat, commit, hata olursa rollback | Transaction içinde çalışacak lambda |
@Transactional, @Cacheable ve @Async ise proxySpring'in bean'in yerine koyduğu sarmalayıcı nesne. `@Transactional` gibi anotasyonlar burada çalışır — çağrı proxy'den geçmezse hiçbir şey olmaz.Sözlükte gör → kalıbıdır. Spring bean’ini bir sarmalayıcıyla değiştirir ve çağrı sarmalayıcıdan geçerken ek davranışı uygular.
Sınırı da buradan gelir: aynı sınıf içinden this. ile yapılan çağrı proxy’den geçmez. Ayrıntısı AOP ve self-invocation dersinde.
Kafam karıştı, daha basit anlat
Spring’in birçok özelliği aslında bildiğin kalıplardır. @Transactional bir proxy’dir, JdbcTemplate ise değişmeyen iskeleti senin yerine yazan bir şablondur.
JdbcTemplate.query(sql, rowMapper) çağırdığında Template Method kalıbının "sabit iskeleti" hangi işleri senin yerine yapar?
Satır satır: Observer ve transaction
ObserverBir olayı yayınlayanın, onu dinleyenleri tanımadan haber verdiği kalıp. Spring'de ApplicationEventPublisher ve @EventListener ile kurulur.Sözlükte gör → kalıbında yayıncı, onu kimin dinlediğini bilmez. Spring’de bu ApplicationEventPublisher ve @EventListener ile kurulur. Kolay görünür; ama dinleyici ne zaman çalışıyor?
Sipariş kaydedildi, olay yayınlandı, sonra ödeme reddedildi ve transaction geri alındı. Müşteri 'Siparişin alındı' e-postasını alır mı? Cevabı göster
Alır. Düz @EventListener, publishEvent çağrısının içinde, aynı thread’de ve commit’ten önce çalışır. E-posta ödeme denenmeden önce gider; transaction’ın geri alınması onu geri çağıramaz.
Adım adım oku
- Sipariş aynı transaction içinde kaydedilir ve bir olay yayınlanır.
- Düz @EventListener aynı anda, transaction'ın içinde çalışır ve e-postayı hemen gönderir.
- Ardından ödeme reddedilir ve transaction geri alınır. Sipariş yok, ama e-posta çoktan gitti.
- @TransactionalEventListener ise commit'i bekler. Commit hiç olmadığı için e-posta hiç gönderilmez.
E-posta ödemeden önce gidiyor
@Serviceclass OrderService { @Transactional public void place(Order order) { orders.save(order); events.publishEvent(new OrderPlaced(order.id())); payment.charge(order); }} @Componentclass OrderConfirmationListener { @EventListener void on(OrderPlaced event) { mail.send(confirmationFor(event.orderId())); }}Debug
main Proxy transaction'ı başlattı. Bundan sonraki her şey aynı transaction içinde.
- tx
- = aktif
Sol/sağ ok tuşlarıyla da gezebilirsin.
Çözüm dinleyiciyi transaction’a bağlamak: @TransactionalEventListener. Varsayılan fazı AFTER_COMMIT’tir: dinleyici commit’i bekler, rollback olursa hiç çalışmaz.
Bir karar daha kalıyor: dinleyicinin hatası kime ulaşıyor? Senkron bir dinleyicinin istisnası yayıncıya döner. @Async onu başka bir thread’e taşır ve ana akışı korur.
Bankada bunun karşılığı transfer SMS’i. Kısa bir örnekte SMS’in yalnızca kesinleşen transferde gittiğini görelim.
Derinleş · FAST transferi ve SMS bildirimi 4 dosya · ~63 satır · ilk okumada atlayabilirsin
Kafam karıştı, daha basit anlat
Düz bir dinleyici, işlem kaydedilmeden önce çalışır. Ödeme sonra geri alınsa bile gönderilen e-posta geri gelmez. Kayıttan sonra çalışmasını istiyorsan @TransactionalEventListener kullan.
@Transactional bir metot içinde publishEvent çağrıldı. Düz bir @EventListener ne zaman ve nerede çalışır?
@TransactionalEventListener (varsayılan faz) ile işaretli bir dinleyici, yayıncının transaction'ı rollback olursa ne yapar?
Kendin gör
Dinleyici anotasyonunu, @Async’i, ödemeyi ve mail sunucusunu değiştir. Üstte dinleyicinin kodu, altta iki thread’in zaman çizelgesi var.
Spring olayları ve transaction — e-posta ne zaman gidiyor?
Tohum 122454@Componentclass OrderConfirmationListener { @EventListener void on(OrderPlaced event) { mailSender.send(confirmationFor(event.orderId())); }}- Oynat veya adımla: place() çalışmaya başlayacak.
Şu an ne oldu?
OrderService.place() başlıyor
Kaydet, olay yayınla, ödemeyi al, commit. Dinleyici "Siparişin alındı" e-postasını gönderiyor.
Aklında kalsın: Soru şu: e-posta commit'ten önce mi gidiyor, sonra mı — ve biri patlarsa diğeri de patlıyor mu?
Görevler0/4
Veritabanında olmayan bir sipariş için e-posta gönderaçık
İpucu
Varsayılan ayarlarla sonuna kadar oynat. E-posta ne zaman gitti, ödeme ne zaman reddedildi?
Mail sunucusu yüzünden sağlam bir siparişi kaybetaçık
İpucu
Ödemeyi başarılı yap, mail sunucusunu düşür. Senkron bir @EventListener istisnası nereye gider?
Ödeme reddedildiğinde hiç e-posta gitmesinaçık
İpucu
E-posta commit'i beklemeli. Hangi anotasyon dinleyiciyi transaction'a bağlar?
Mail sunucusu düşükken sipariş kaydedilsin ve istemci başarılı cevap alsınaçık
İpucu
Dinleyicinin hatası ana akışa hiç ulaşmamalı. Commit'ten sonra ve başka bir thread'de.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
- Varsayılanla oynat. E-posta
mainthread’de, ödemeden önce gidiyor. Sonunda sipariş yok, e-posta var: hayalet e-posta. - Ödemeyi başarılı yap, mail sunucusunu düşür. Bu kez tersi oluyor: yan bir iş olan e-posta, sağlam siparişi geri alıyor.
@TransactionalEventListener’a geç. Ödeme reddedilince dinleyici hiç çalışmıyor. Ama mail sunucusu düşükken sipariş commit olduğu hâlde istemci hata görüyor.@Asyncekle. Sipariş kaydediliyor, istemci 201 alıyor, e-posta hatası ayrı thread’de loglanıyor. Dört görevi de tamamla.
Simülatörde senkron bir AFTER_COMMIT dinleyicisi mail sunucusu düşükken istisna attı. Sonuç ve çözüm nedir?
Tuzaklar
Transaction olmadan @TransactionalEventListener. Olay @Transactional olmayan bir metottan yayınlanırsa dinleyici varsayılan olarak hiç çağrılmaz, ve bunu söyleyen bir hata da çıkmaz.
Bir refactor’da @Transactional kalkınca e-postalar sessizce kesilir. Gerekirse fallbackExecution = true ver.
AFTER_COMMIT içinde veritabanına yazmak. Asıl transaction zaten commit oldu; bu noktadaki yazma ona katılır ama bir daha commit edilmez. Kalıcı bir şey yazacaksan @Transactional(propagation = REQUIRES_NEW) kullan.
Commit ile gönderim arasındaki boşluk. AFTER_COMMIT’ten hemen sonra uygulama çökerse e-posta hiç gitmez. Kaybolmaması gereken olaylar için olayı siparişle aynı transaction’da bir tabloya yazıp sonradan gönderen bir yaklaşım (transactional outbox) gerekir.
Ana akışı olaylara gömmek. Siparişin tamamlanması için şart olan bir adım (stok düşmek gibi) olay dinleyicisine konursa akış kodda görünmez olur. Observer yan etkiler içindir; zorunlu adımlar doğrudan çağrı olarak kalmalı.
Kendini sına
Spring'de Strategy, bir arayüz ve onu uygulayan birden çok @Component ile kurulabilir.
Bir @TransactionalEventListener, olay @Transactional OLMAYAN bir metottan yayınlandığında ne yapar?
Aklında kalacak üç şey
- 1 Spring'de Strategy bir arayüz, birden çok @Component ve List ya da Map olarak enjeksiyondur. Yeni davranış yeni bir sınıftır.
- 2 JdbcTemplate ve TransactionTemplate Template Method'dur, @Transactional ve @Cacheable ise Proxy. Proxy'nin sınırı sınıfın kendini çağırmasıdır.
- 3 Düz @EventListener kayıt tamamlanmadan, aynı transaction içinde çalışır. Dış dünyaya giden işler @TransactionalEventListener ile kaydın bitmesini beklemelidir.
5 kart sonraki derste seni bekliyor