AOP, Proxy ve Self-Invocation Tuzağı
Önce şunu oku: Transaction Propagation ve Rollback Yayılımı
30 saniyede özet
Spring, @Transactional gibi etiketleri sınıfın etrafına bir aracı (proxy) koyarak çalıştırır. Aracı yalnızca dışarıdan gelen çağrıları görür; sınıf kendi metodunu çağırırsa etiket sessizce hiçbir şey yapmaz.
Bir müdürü dışarıdan arayan herkes önce sekretere düşer. Ama müdür kendi odasından bir şey yaparsa sekreterin haberi olmaz.
Adım adım oku
- Dışarıdan gelen çağrı asıl nesneye değil, önündeki proxy'ye, yani sekretere düşer.
- Sekreter transaction'ı açar ve çağrıyı müdüre, asıl nesnenin placeOrder metoduna bağlar.
- placeOrder, aynı sınıftaki @Transactional save metodunu this üzerinden, kendi telefonundan çağırır.
- Bu çağrı sekreterden geçmez; save'in @Transactional etiketi hiç okunmaz ve kendi transaction'ı açılmaz.
-
Bayt: save metoduna @Transactional yazdım. Metot çalışıyor, test de geçiyor.
-
Sen: O zaman sorun ne?
-
Bayt: Transaction hiç açılmamış ve bunu bana kimse söylememiş!
-
Bayt: Kim çağırdı? Dışarıdan biri mi, yoksa sınıf kendi kendini mi?
@Transactional yazdın, metot çalışıyor, test geçiyor. Ama transaction hiç açılmadı ve sana bunu kimse söylemedi.
Kapıdaki sekreter: proxy
reserve() metodunda @Transactional var. Aynı sınıftaki placeOrder() onu this.reserve() diye çağırıyor. reserve() için transaction açılır mı? Cevabı göster
Hayır. Çağrı sınıfın içinden geldiği için proxy’ye hiç uğramaz; transaction’ı açan da proxy’dir.
Spring, @Transactional gibi etiketleri çalıştırmak için sınıfının içine kod eklemez. Onun yerine nesnenin önüne bir 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 →, yani bir sekreter koyar; başkaları asıl nesneye değil, ona ulaşır.
Controller ──> [ Proxy ] ──> OrderService ↑ transaction burada açılırBuradan tek bir kural çıkar ve dersin tamamı bu kuraldır:
Proxy yalnızca dışarıdan gelen çağrıları görebilir.
Kafam karıştı, daha basit anlat
Proxy bir sekreterdir. Dışarıdan gelen her çağrı önce ona uğrar: transaction’ı o açar, sonra asıl metoda geçirir.
Spring bir bean'e `@Transactional` gördüğünde ne yapar?
Kendin gör
Spring proxy — @Transactional neden sessizce çalışmıyor
Tohum 9public void placeOrder(Order order) {
validate(order);
this.chargePayment(order);
}
@Transactional
public void chargePayment(Order order) {
payments.save(order.payment());
auditService.record(order);
}- ApplicationContext
OrderService - OrderController
placeOrder() - placeOrder()
chargePayment() - chargePayment()
auditService.record() - —
—
Açılan transaction
0
her iki durumda da aynı
Korunan yazma
—/ 2
asıl fark burada
Advice durumu
atlandı
Self-invocation — çağrı nesneden hiç çıkmadı
Şu an ne oldu?
İstek başlıyor
Spring AOP sınıfın içine kod dokumaz; bean’i bir proxy ile sarar. Bu yüzden advice’ın çalışması, çağrının proxy’den geçmesine bağlıdır.
Aklında kalsın: Proxy tabanlı AOP ile AspectJ weaving arasındaki fark tam olarak budur: biri çağrı yolunu, diğeri bytecode’u değiştirir.
Olay günlüğü (0)
Henüz olay yok. Oynat veya adımla.
Varsayılan kurulum zaten hatalı — sonuna kadar oynat:
this.chargePayment(). Proxy atlandı,@Transactionalhiç uygulanmadı. Hata yok, uyarı yok.Ayrı bean üzerindene geç. Aynı kod, artık transaction açılıyor. Korunan yazma 1/2’den 2/2’ye çıkıyor.- Metodu
privateyap. Yine atlanıyor — ve uyarı notunu oku. public final+ CGLIB. Atlanıyor. Proxy tipini JDK yap — aynı metot artık sarmalanıyor.- JDK + somut sınıf enjeksiyonu. Context hiç ayağa kalkmıyor.
“Açılan transaction” kutusuna dikkat: iki durumda da 1 yazıyor. Yani transaction sayısına bakarak bu hatayı bulamazsın; bakman gereken şey, hangi kaydın transaction’ın içinde kaldığı.
Kafam karıştı, daha basit anlat
Sınıf kendi metodunu çağırırsa sekreteri atlar, doğrudan kendi odasına gider. O zaman @Transactional gibi etiketler hiç çalışmaz.
Bu kod ne yazdırır?
Tuzaklar
| Durum | Neden çalışmaz | Belirti |
|---|---|---|
this.method() | Çağrı nesneden hiç çıkmaz, proxy yolda değildir | Sessiz |
private metot | Override edilemez, arayüze konamaz | Sessiz |
final metot + CGLIB | CGLIB alt sınıf üretir, final override edilemez | Sessiz |
Üçünün ortak yanı: hiçbiri hata vermez. Kod derlenir, uygulama çalışır, testler geçer. Sorun ancak bir rollback gerektiğinde ortaya çıkar — yani en kötü anda.
Aşağıdaki örnek bir bankanın kredi başvuru servisinden. Kendi yazdığın bir aspect de aynı proxy kuralına tabi: sessizce atlanan ölçüm, düzeltmesi ve bunu kanıtlayan test.
Derinleş · Kredi başvurusunda süre ölçümü: kendi aspect'in ve self-invocation 5 dosya · ~108 satır · ilk okumada atlayabilirsin
CGLIB ve JDK proxy· istersen atla
| JDK dynamic proxy | CGLIB | |
|---|---|---|
| Nasıl | Arayüzü implemente eder | Sınıfın alt sınıfını üretir |
| Gereksinim | Bean bir arayüz implemente etmeli | Sınıf final olmamalı |
final metot | Etkilenmez — sarmalanır | Sarmalanamaz |
| Somut sınıfla enjeksiyon | Patlar | Çalışır |
Spring Boot varsayılanı CGLIB’dir (spring.aop.proxy-target-class=true). Bu yüzden çoğu projede arayüz olmadan da her şey çalışır.
Simülatördeki dördüncü adım ince bir ayrım: aynı final metot CGLIB ile sarmalanamaz, JDK proxy ile sarmalanır, çünkü JDK proxy alt sınıf üretmez, arayüzü implemente eder. “Neden bazı ortamlarda çalışıyor?” sorusunun cevabı çoğu zaman budur.
JDK dynamic proxy ile CGLIB proxy arasındaki fark nedir?
Her durumu, proxy'nin devreye girip girmediğine göre ayır.
Nasıl düzeltilir?
1. Metodu ayrı bir bean’e taşı (tercih edilen)
@Serviceclass PaymentService { @Transactional public void charge(Order order) { ... }}
@Serviceclass OrderService { private final PaymentService payments; // proxy enjekte edilir
public void placeOrder(Order order) { payments.charge(order); // dışarıdan çağrı → advice çalışır }}Bu yalnızca tuzağı çözmez, genelde tasarımı da düzeltir. Bir sınıfın kendi metodunu “dışarıdan çağrılmış gibi” çağırma ihtiyacı, çoğu zaman o sınıfın iki ayrı iş yaptığının işaretidir.
2. Kendine enjeksiyon
@Serviceclass OrderService { @Lazy private final OrderService self; // proxy, kendisi değil
public void placeOrder(Order order) { self.chargePayment(order); }}Çalışır ama bir kod kokusudur. @Lazy gerekir, yoksa döngüsel bağımlılık oluşur.
3. AopContext.currentProxy()
((OrderService) AopContext.currentProxy()).chargePayment(order);Son çare. exposeProxy = true gerektirir, kodu AOP altyapısına bağlar ve test edilmesi zorlaşır.
Aynı tuzak diğer anotasyonlarda da var. Bu bir @Transactional özelliği değil, proxy tabanlı AOP’nin özelliği. Aynısı şunlar için de geçerlidir:
@Cacheable— self-invocationBir bean'in kendi metodunu `this` üzerinden çağırması. Proxy devre dışı kalır ve anotasyon sessizce uygulanmaz.Sözlükte gör →’da önbellek hiç devreye girmez, her çağrı hesaplar@Async— metot çağıran thread’de senkron çalışır@Retryable,@PreAuthorizeve kendi yazdığın her aspect
@Cacheable’ın sessizce çalışmaması özellikle sinsidir: sonuç doğrudur, sadece yavaştır.
Peki AspectJ?· istersen atla
Proxy kapıda bekleyen bir sekreterdir; AspectJ ise sınıfın derlenmiş kodunu doğrudan değiştirir. Bu yüzden sınıfın kendi içindeki çağrıları da yakalar.
Bedeli: daha karmaşık bir build, ayrı bir ajan ya da derleyici eklentisi ve daha zor hata ayıklama. Çoğu projede metodu ayrı bir bean’e taşımak yeterlidir.
Kendini sına
Spring, @Transactional'ı çalıştırmak için sınıfının içine kod ekler.
`placeOrder` çağrıldığında `save` bir transaction içinde mi çalışır?
Aklında kalacak üç şey
- 1 Spring bean'i bir proxy ile sarar ve proxy yalnızca dışarıdan gelen çağrıları görebilir. Bütün tuzakları bu tek cümle açıklar.
- 2 Asıl tehlike sessizlik: kendi kendini çağırmada hata yok, uyarı yok, test geçiyor. Sorun ancak bir geri alma gerektiğinde ortaya çıkıyor.
- 3 Çözüm sırası: metodu ayrı bir bean'e taşı; olmuyorsa sınıfa kendisini enjekte et; son çare AopContext.currentProxy().
5 kart sonraki derste seni bekliyor
Bu dersin üstüne kurulanlar
Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.