İçeriğe geç

AOP, Proxy ve Self-Invocation Tuzağı

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

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

Sekreter yalnızca dışarıdan gelen çağrıları duyar.
Adım adım oku
  1. Dışarıdan gelen çağrı asıl nesneye değil, önündeki proxy'ye, yani sekretere düşer.
  2. Sekreter transaction'ı açar ve çağrıyı müdüre, asıl nesnenin placeOrder metoduna bağlar.
  3. placeOrder, aynı sınıftaki @Transactional save metodunu this üzerinden, kendi telefonundan çağırır.
  4. Bu çağrı sekreterden geçmez; save'in @Transactional etiketi hiç okunmaz ve kendi transaction'ı açılmaz.
  1. Bayt: save metoduna @Transactional yazdım. Metot çalışıyor, test de geçiyor.

  2. Sen: O zaman sorun ne?

  3. Bayt: Transaction hiç açılmamış ve bunu bana kimse söylememiş!

  4. 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ır

Buradan tek bir kural çıkar ve dersin tamamı bu kuraldır:

Proxy yalnızca dışarıdan gelen çağrıları görebilir.

Aspect
Her yerde lazım olan ortak iş: transaction, loglama, süre ölçümü.
Oriented
Odaklı: bu ortak işi ayrı bir yere toplamak.
Programming
Programlama: sen yalnızca etiketi yazarsın, işi proxy yapar.
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.

Hızlı kontrolBaşlangıç

Spring bir bean'e `@Transactional` gördüğünde ne yapar?

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

Kendin gör

Spring proxy — @Transactional neden sessizce çalışmıyor

Tohum 9
public void placeOrder(Order order) {
  validate(order);
  this.chargePayment(order);
}

@Transactional
public void chargePayment(Order order) {
  payments.save(order.payment());
  auditService.record(order);
}
  • ApplicationContextOrderService
  • OrderControllerplaceOrder()
  • 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ı

Hız
Adım 0

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

  1. this.chargePayment(). Proxy atlandı, @Transactional hiç uygulanmadı. Hata yok, uyarı yok.
  2. Ayrı bean üzerindene geç. Aynı kod, artık transaction açılıyor. Korunan yazma 1/2’den 2/2’ye çıkıyor.
  3. Metodu private yap. Yine atlanıyor — ve uyarı notunu oku.
  4. public final + CGLIB. Atlanıyor. Proxy tipini JDK yap — aynı metot artık sarmalanıyor.
  5. 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.

Hızlı kontrolOrta

Bu kod ne yazdırır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
ProxyKimlik.java
1@Service
2class ReportService {
3
4 @Autowired
5 private ApplicationContext ctx;
6
7 public void kontrol() {
8 ReportService bean = ctx.getBean(ReportService.class);
9 System.out.println("this sinifi : " + this.getClass().getSimpleName());
10 System.out.println("bean sinifi : " + bean.getClass().getSimpleName());
11 System.out.println("ayni nesne mi : " + (this == bean));
12 }
13}
Java 21UTF-8LF

Tuzaklar

DurumNeden çalışmazBelirti
this.method()Çağrı nesneden hiç çıkmaz, proxy yolda değildirSessiz
private metotOverride edilemez, arayüze konamazSessiz
final metot + CGLIBCGLIB alt sınıf üretir, final override edilemezSessiz

Üçü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
Proje dosyaları

src/main/java/com/bank/metrics/ Measured.java İşaret: hangi metotların süresinin ölçüleceğini söyler.

src/main/java/com/bank/metrics/Measured.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Measured {
/** Metric name, e.g. "loan.scoring". */
String value();
}

src/main/java/com/bank/metrics/ MeasuredAspect.java Aspect: işaretli her public metodun etrafına bir Timer koyar.

src/main/java/com/bank/metrics/MeasuredAspect.java
@Aspect
@Component
class MeasuredAspect {
private final MeterRegistry meters;
MeasuredAspect(MeterRegistry meters) {
this.meters = meters;
}
// Runs only when the call enters through the proxy: a call from another bean.
// Private, final and self-invoked methods never reach this advice.
@Around("@annotation(measured)")
Object time(ProceedingJoinPoint call, Measured measured) throws Throwable {
Timer.Sample sample = Timer.start(meters);
try {
return call.proceed();
} finally {
sample.stop(meters.timer(measured.value()));
}
}
}

src/main/java/com/bank/loan/ LoanApplicationServiceBefore.java Sorunlu hâl: apply, aynı sınıftaki score'u this ile çağırıyor; kredi bürosu sorgusunun süresi hiç ölçülmüyor.

src/main/java/com/bank/loan/LoanApplicationServiceBefore.java
// BEFORE: looks right, measures nothing. The dashboard shows no scoring latency,
// so nobody notices when the credit bureau call slows down.
@Service
class LoanApplicationServiceBefore {
private final CreditBureauClient bureau;
LoanApplicationServiceBefore(CreditBureauClient bureau) {
this.bureau = bureau;
}
public Decision apply(LoanApplication application) {
CreditScore score = this.score(application); // `this` is the raw object, not the proxy
return Decision.of(application, score);
}
@Measured("loan.scoring")
@Transactional(readOnly = true) // skipped for the same reason
public CreditScore score(LoanApplication application) {
return bureau.fetchScore(application.nationalId());
}
}

src/main/java/com/bank/loan/ LoanApplicationService.java Düzeltme: skorlama ayrı bir bean'de. Çağrı proxy'den geçiyor, ölçüm ve @Transactional çalışıyor.

src/main/java/com/bank/loan/LoanApplicationService.java
// AFTER: the measured work lives in its own bean.
@Service
class LoanApplicationService {
private final LoanApplicationRepository applications;
private final CreditScoring scoring; // injected: this reference IS the proxy
LoanApplicationService(LoanApplicationRepository applications, CreditScoring scoring) {
this.applications = applications;
this.scoring = scoring;
}
public Decision apply(long applicationId) {
LoanApplication application = applications.findById(applicationId).orElseThrow();
CreditScore score = scoring.score(application); // through the proxy: timed
return Decision.of(application, score);
}
}
@Component
class CreditScoring {
private final CreditBureauClient bureau;
CreditScoring(CreditBureauClient bureau) {
this.bureau = bureau;
}
@Measured("loan.scoring")
public CreditScore score(LoanApplication application) {
return bureau.fetchScore(application.nationalId());
}
}

src/test/java/com/bank/loan/ CreditScoringProxyTest.java Kanıt: bean bir proxy mi, metrik gerçekten yazıldı mı?

src/test/java/com/bank/loan/CreditScoringProxyTest.java
@SpringBootTest
class CreditScoringProxyTest {
@Autowired CreditScoring scoring;
@Autowired LoanApplicationService service;
@Autowired MeterRegistry meters;
@MockitoBean LoanApplicationRepository applications;
@MockitoBean CreditBureauClient bureau; // never call the real bureau from a test
@Test
void theInjectedBeanIsAProxy() {
assertThat(AopUtils.isAopProxy(scoring)).isTrue(); // CGLIB subclass: CreditScoring$$SpringCGLIB$$0
}
@Test
void scoringIsTimed() {
given(applications.findById(1L)).willReturn(Optional.of(LoanApplications.sample()));
given(bureau.fetchScore(any())).willReturn(new CreditScore(1450));
service.apply(1L);
assertThat(meters.timer("loan.scoring").count()).isEqualTo(1);
}
}
CGLIB ve JDK proxy· istersen atla
JDK dynamic proxyCGLIB
NasılArayüzü implemente ederSınıfın alt sınıfını üretir
GereksinimBean bir arayüz implemente etmeliSınıf final olmamalı
final metotEtkilenmez — sarmalanırSarmalanamaz
Somut sınıfla enjeksiyonPatlarÇ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.

Hızlı kontrolOrta

JDK dynamic proxy ile CGLIB proxy arasındaki fark nedir?

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

Her durumu, proxy'nin devreye girip girmediğine göre ayır.

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

Sınıflandırılmamış

Proxy devrede

Anotasyon çalışır

    Proxy atlanır

    Anotasyon sessizce devre dışı

      Nasıl düzeltilir?

      1. Metodu ayrı bir bean’e taşı (tercih edilen)

      Çalışan hâli — çağrı dışarıdan geliyor
      @Service
      class PaymentService {
      @Transactional
      public void charge(Order order) { ... }
      }
      @Service
      class 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

      @Service
      class 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, @PreAuthorize ve 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

      Şimşek turu1/5

      Spring, @Transactional'ı çalıştırmak için sınıfının içine kod ekler.

      Soru 1/4Orta

      `placeOrder` çağrıldığında `save` bir transaction içinde mi çalışır?

      Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
      OrderService.java
      1@Service
      2public class OrderService {
      3
      4 public void placeOrder(Order order) {
      5 validate(order);
      6 this.chargePayment(order);
      7 }
      8
      9 @Transactional
      10 public void chargePayment(Order order) {
      11 payments.save(order.payment());
      12 throw new PaymentException();
      13 }
      14}
      Java 21UTF-8LF

      Aklında kalacak üç şey

      1. 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. 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. 3 Çözüm sırası: metodu ayrı bir bean'e taşı; olmuyorsa sınıfa kendisini enjekte et; son çare AopContext.currentProxy().
      Sonraki kapı Sunucu yeniden başladı ve bütün kullanıcılar çıkış yaptı. Oturumlar nerede duruyordu? Oturum Yönetimi ve Stateless Ölçekleme · 10 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı

      Bu dersin üstüne kurulanlar

      Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.