İçeriğe geç

Spring'in İçindeki Kalıplar — Strategy, Template Method, Proxy, Observer

Orta 10 dk Çok sık karşılaşılır

Ö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.

  1. Bayt: Müşteri siparişin alındı e-postası almış, ama sipariş hiç kaydedilmemiş!

  2. Sen: Nasıl olur? Kodda önce kaydet, sonra olay yayınla yazıyor.

  3. Bayt: Yazıyor. Asıl soru, dinleyicinin kayıt kesinleşmeden önce mi sonra mı çalıştığı.

  4. 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:

Checkout.java
public interface PaymentMethod {
String key(); // "card", "wallet", "bnpl"
Receipt pay(Order order);
}
@Component class CardPayment implements PaymentMethod { … }
@Component class WalletPayment implements PaymentMethod { … }
@Service
class 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.

Hızlı kontrolBaşlangıç

Checkout, kurucusunda List<PaymentMethod> alıyor. Yeni bir ödeme yöntemi eklemek için ne yapman gerekir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Map<String, PaymentMethod> enjekte ettiğinde Spring anahtar olarak neyi kullanır ve bunun riski nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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)
JdbcTemplateBağlantı al/bırak, statement kapat, SQLException → DataAccessExceptionSQL ve RowMapper
TransactionTemplateBaşlat, commit, hata olursa rollbackTransaction 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.

Hızlı kontrolOrta

JdbcTemplate.query(sql, rowMapper) çağırdığında Template Method kalıbının "sabit iskeleti" hangi işleri senin yerine yapar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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.

Aynı olay, iki dinleyici: biri kesinleşmeden koşar, öteki bekler.
Adım adım oku
  1. Sipariş aynı transaction içinde kaydedilir ve bir olay yayınlanır.
  2. Düz @EventListener aynı anda, transaction'ın içinde çalışır ve e-postayı hemen gönderir.
  3. Ardından ödeme reddedilir ve transaction geri alınır. Sipariş yok, ama e-posta çoktan gitti.
  4. @TransactionalEventListener ise commit'i bekler. Commit hiç olmadığı için e-posta hiç gönderilmez.

E-posta ödemeden önce gidiyor

OrderService.java
1@Service
2class OrderService {
3 @Transactional
şu an çalışan satır public void place(Order order) {
5 orders.save(order);
6 events.publishEvent(new OrderPlaced(order.id()));
7 payment.charge(order);
8 }
9}
10
11@Component
12class OrderConfirmationListener {
13 @EventListener
14 void on(OrderPlaced event) {
15 mail.send(confirmationFor(event.orderId()));
16 }
17}

Debug

Adım 1/7

main Proxy transaction'ı başlattı. Bundan sonraki her şey aynı transaction içinde.

tx
= aktif
Java 21UTF-8LF4:1

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
Proje dosyaları

src/main/java/com/bank/transfer/ TransferSent.java Olay, olanı anlatan küçük bir kayıt: hangi transfer, kime, ne kadar.

src/main/java/com/bank/transfer/TransferSent.java
public record TransferSent(TransferId transferId, String customerNo, Money amount, String maskedToIban) {}

src/main/java/com/bank/transfer/ SendFastTransfer.java Servis parayı düşer, olayı yayınlar, sonra fraud kontrolünü çalıştırır; hepsi tek transaction.

src/main/java/com/bank/transfer/SendFastTransfer.java
@Service
class SendFastTransfer {
private final Accounts accounts;
private final Transfers transfers;
private final FraudCheck fraud;
private final ApplicationEventPublisher events;
SendFastTransfer(Accounts accounts, Transfers transfers, FraudCheck fraud, ApplicationEventPublisher events) {
this.accounts = accounts;
this.transfers = transfers;
this.fraud = fraud;
this.events = events;
}
@Transactional
public TransferId send(TransferRequest request) {
Account source = accounts.lockByIban(request.fromIban()); // SELECT ... FOR UPDATE
source.debit(request.amount());
Transfer transfer = transfers.save(Transfer.pending(request));
events.publishEvent(new TransferSent(
transfer.id(), request.customerNo(), request.amount(), request.toIban().masked()));
fraud.approveOrThrow(transfer); // a fraud hold throws and rolls everything back
return transfer.id();
}
}

src/main/java/com/bank/notification/ TransferSmsListener.java SMS dinleyicisi commit'i bekler ve ayrı bir thread'de çalışır.

src/main/java/com/bank/notification/TransferSmsListener.java
@Component
class TransferSmsListener {
private final SmsGateway sms;
TransferSmsListener(SmsGateway sms) {
this.sms = sms;
}
// AFTER_COMMIT is the default phase: a rolled-back transfer never reaches this method.
// @Async (enabled with @EnableAsync) keeps an SMS outage away from the transfer itself.
@Async
@TransactionalEventListener
void on(TransferSent event) {
sms.sendTransferNotice(event.customerNo(), event.amount(), event.maskedToIban());
}
}

src/main/java/com/bank/notification/ EarlyTransferSmsListener.java Şöyle de yazılabilirdi: düz @EventListener. Bak, fraud kontrolü transferi geri alsa da SMS çoktan gitmiş oluyor.

src/main/java/com/bank/notification/EarlyTransferSmsListener.java
// Another way to write it: a plain listener.
@Component
class EarlyTransferSmsListener {
private final SmsGateway sms;
EarlyTransferSmsListener(SmsGateway sms) {
this.sms = sms;
}
// Runs inside publishEvent: before the fraud check and before the commit.
// If the fraud check then rolls the transfer back, this SMS has already gone out.
@EventListener
void on(TransferSent event) {
sms.sendTransferNotice(event.customerNo(), event.amount(), event.maskedToIban());
}
}
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.

Hızlı kontrolOrta

@Transactional bir metot içinde publishEvent çağrıldı. Düz bir @EventListener ne zaman ve nerede çalışır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

@TransactionalEventListener (varsayılan faz) ile işaretli bir dinleyici, yayıncının transaction'ı rollback olursa ne yapar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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
OrderConfirmationListener.java
1@Component
2class OrderConfirmationListener {
3 @EventListener
4 void on(OrderPlaced event) {
5 mailSender.send(confirmationFor(event.orderId()));
6 }
7}
Java 21UTF-8LF
  1. Oynat veya adımla: place() çalışmaya başlayacak.
Hız
Adım 0

Ş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.

  1. Varsayılanla oynat. E-posta main thread’de, ödemeden önce gidiyor. Sonunda sipariş yok, e-posta var: hayalet e-posta.
  2. Ö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.
  3. @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.
  4. @Async ekle. Sipariş kaydediliyor, istemci 201 alıyor, e-posta hatası ayrı thread’de loglanıyor. Dört görevi de tamamla.
Hızlı kontrolOrta

Simülatörde senkron bir AFTER_COMMIT dinleyicisi mail sunucusu düşükken istisna attı. Sonuç ve çözüm nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

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

Şimşek turu1/5

Spring'de Strategy, bir arayüz ve onu uygulayan birden çok @Component ile kurulabilir.

Soru 1/3İleri

Bir @TransactionalEventListener, olay @Transactional OLMAYAN bir metottan yayınlandığında ne yapar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 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. 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. 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.
Sonraki kapı Veritabanını değiştirdin ve iş kuralların da değişmek zorunda kaldı. Ok yanlış yöne mi bakıyor? Hexagonal Mimari — Bağımlılık Oku Hangi Yöne Bakıyor · 10 dk

5 kart sonraki derste seni bekliyor

0/5 kart bu dersten toplandı