← Kursa Dön
📄 Text · 15 min

AOP Proxy: JDK Dynamic vs CGLIB

Bir ünlünün menajeri düşünün. Siz ünlüyle doğrudan konuşamazsınız — her şey menajeri üzerinden geçer. Menajeri telefonu açar, uygun görürse ünlüye bağlar, görüşme sonrası notları alır. Dışarıdan bakınca ünlüyle konuştuğunuzu sanırsınız ama aslında menajer araya girip ekstra işler yapmaktadır.

Spring AOP'deki proxy tam olarak bu menajerdir. Spring, @Aspect ile işaretlenen davranışları uygulamak için orijinal bean'in yerine bir proxy nesnesi oluşturur. Tüm metot çağrıları bu proxy üzerinden geçer — proxy gerekli advice'ları çalıştırır, sonra gerçek metodu çağırır.

Bu mekanizmayı anlamak kritiktir çünkü Spring AOP'nin en kafa karıştıran davranışları — self-invocation sorunu, final metot kısıtlaması, @Transactional'ın çalışmaması — hep proxy'nin nasıl çalıştığından kaynaklanır.


Proxy Nedir?

Proxy (vekil), başka bir nesnenin yerine geçerek onun adına işlem yapan nesnedir. Proxy Pattern, GoF Design Pattern'lerinden biridir.

Gerçek Dünya Analojileri

AnalojiGerçek NesneProxyEk Davranış
MenajerÜnlüMenajerRandevu kontrolü, ücret pazarlığı
ResepsiyonDoktorResepsiyonRandevu alma, sigorta kontrolü
CDNOrigin ServerEdge ServerCaching, coğrafi yönlendirme
Kredi KartıBanka hesabıKart terminaliLimit kontrolü, fraud detection

Spring AOP'de:

  • Gerçek nesne = Service bean'iniz (UserService)

  • Proxy = Spring'in oluşturduğu sarmalayıcı nesne

  • Ek davranış = @Before, @Around, @After advice'ları

Proxy Nasıl Devreye Girer?

// Spring Container başlarken:
1. UserService bean oluşturulur
2. Spring, UserService'e uygulanacak aspect var mı kontrol eder
3. Aspect varsa → UserService'in PROXY'sini oluşturur
4. Container'da orijinal bean yerine PROXY kaydedilir

// Runtime'da:
Controller → @Autowired UserService
          → Aslında UserService PROXY'si inject edilir
          → Proxy, advice'ları çalıştırır
          → Sonra gerçek UserService'in metodunu çağırır

Bunu kodla görelim:

@RestController
public class UserController {

    @Autowired
    private UserService userService;  // ← Bu proxy!

    @PostConstruct
    public void checkProxy() {
        System.out.println(userService.getClass().getName());
        // Çıktı: com.example.service.UserService$$SpringCGLIB$$0
        //                                         ↑ CGLIB proxy!

        System.out.println(AopUtils.isAopProxy(userService));     // true
        System.out.println(AopUtils.isCglibProxy(userService));   // true
    }
}

İki Proxy Mekanizması: JDK vs CGLIB

Spring AOP, proxy oluşturmak için iki mekanizma kullanır:

JDK Dynamic Proxy

Java'nın java.lang.reflect.Proxy sınıfını kullanır. Sadece interface'ler için çalışır:

// Interface
public interface UserService {
    UserResponse findById(Long id);
    UserResponse create(CreateUserRequest request);
}

// Implementation
@Service
public class UserServiceImpl implements UserService {
    @Override
    public UserResponse findById(Long id) { ... }

    @Override
    public UserResponse create(CreateUserRequest request) { ... }
}

// Spring, JDK Dynamic Proxy oluşturur:
// → Proxy, UserService interface'ini implement eder
// → Proxy, UserServiceImpl'ın metodlarını çağırır

Nasıl çalışır?

Caller → Proxy (UserService interface implement eder)
              → InvocationHandler.invoke()
                  → Advice'ları çalıştır (@Before, @Around...)
                  → target.method() çağır (gerçek UserServiceImpl)
                  → Advice'ları çalıştır (@AfterReturning, @After...)
              ← Sonucu döndür
         ← Caller'a ilet

Kısıtlamaları:

  • Sadece interface üzerinden çağrılar intercept edilir

  • Interface'i olmayan sınıflara uygulanamaz

  • instanceof kontrolü: proxy, sadece interface'e karşı true döner

CGLIB (Code Generation Library)

Bytecode manipulation ile hedef sınıfın subclass'ını oluşturur:

// Interface YOK — doğrudan sınıf
@Service
public class UserService {
    public UserResponse findById(Long id) { ... }
    public UserResponse create(CreateUserRequest request) { ... }
}

// Spring, CGLIB proxy oluşturur:
// → UserService$$SpringCGLIB$$0 extends UserService
// → Proxy, tüm public/protected metodları override eder

Nasıl çalışır?

Caller → Proxy (UserService'i extend eder)
              → MethodInterceptor.intercept()
                  → Advice'ları çalıştır
                  → super.method() çağır (parent = gerçek UserService)
                  → Advice'ları çalıştır
              ← Sonucu döndür
         ← Caller'a ilet

Kısıtlamaları:

  • `final` sınıflar proxy oluşturulamaz (subclass yapılamaz)

  • `final` metotlar override edilemez → advice çalışmaz

  • `private` metotlar override edilemez → advice çalışmaz

  • Subclass oluşturma, JDK proxy'den biraz daha yavaştır (ilk oluşturma)

Karşılaştırma Tablosu

ÖzellikJDK Dynamic ProxyCGLIB
GereksinimInterface zorunluInterface zorunlu değil
Mekanizmajava.lang.reflect.ProxyBytecode generation (subclass)
Spring Boot varsayılanıHayırEvet (2.0'dan beri)
final sınıfN/A❌ Çalışmaz
final metotN/A❌ Çalışmaz
private metot
İlk oluşturma hızıHızlıBiraz yavaş
Çağrı hızıBiraz yavaşHızlı
MemoryAzBiraz fazla

Spring Boot Varsayılanı

Spring Boot 2.0'dan itibaren spring.aop.proxy-target-class=true varsayılandır — yani CGLIB kullanılır:

# application.properties

# CGLIB (varsayılan) — interface olsa bile sınıf proxy'si oluşturur
spring.aop.proxy-target-class=true

# JDK Dynamic Proxy — sadece interface varsa proxy oluşturur
spring.aop.proxy-target-class=false

💡 İpucu: Varsayılan CGLIB'de bırakın. JDK proxy'ye geçmenin pratikte bir avantajı yoktur. CGLIB, interface zorunluluğunu kaldırarak daha esnek ve modern bir yaklaşım sunar.


Self-Invocation Problemi — AOP'nin En Büyük Tuzağı

Bu, Spring AOP ile çalışan herkesin mutlaka karşılaşacağı ve anlaması gereken en önemli konudur.

Problem

@Service
public class OrderService {

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        // Sipariş sonrası bildirimleri gönder
        sendNotifications(order);  // ❌ @Async ÇALIŞMAZ!

        return order;
    }

    @Async
    public void sendNotifications(Order order) {
        emailService.sendOrderConfirmation(order);
        smsService.sendOrderSms(order);
    }
}

sendNotifications() senkron olarak çalışacaktır — @Async etkisizdir. Neden?

Açıklama

Dışarıdan çağrı (Controller → Service):
    Controller → Proxy.createOrder() → Advice çalışır → Real.createOrder()
                 ↑ PROXY üzerinden         ✅ @Transactional aktif

İç çağrı (createOrder → sendNotifications):
    Real.createOrder() → this.sendNotifications()
                          ↑ THIS = gerçek nesne, PROXY DEĞİL
                            ❌ @Async çalışmaz!

Mesele şu: this.sendNotifications() çağrısı proxy'yi bypass eder. this keyword'ü Java'da her zaman gerçek nesneyi referans eder — proxy'yi değil. Ve advice'lar sadece proxy üzerinden geçen çağrılarda tetiklenir.

Görsel Açıklama

 ┌─────────────────────────────────┐
 │         PROXY                    │
 │  ┌───────────────────────────┐  │
 │  │      GERÇEK NESNE         │  │
 │  │                           │  │
 │  │  createOrder() {          │  │
 │  │    ...                    │  │
 │  │    this.sendNotif()  ──────┐ │  ← this = gerçek nesne
 │  │  }                    │    │ │    proxy'yi bypass eder!
 │  │                       │    │ │
 │  │  sendNotif() {   ←────┘    │ │  ← Advice çalışmaz
 │  │    ...                     │ │
 │  │  }                         │ │
 │  └───────────────────────────┘  │
 └─────────────────────────────────┘
           ↑
     Dışarıdan çağrı → Proxy üzerinden → Advice çalışır ✅

Bu Sorun Neleri Etkiler?

Self-invocation problemi sadece @Async değil, proxy-based tüm mekanizmaları etkiler:

AnnotationSelf-Invocation'daAçıklama
@Transactional❌ ÇalışmazYeni transaction açılmaz
@Cacheable❌ ÇalışmazCache kontrolü yapılmaz
@Async❌ ÇalışmazSenkron çalışır
@Retryable❌ ÇalışmazRetry yapılmaz
Custom AOP❌ ÇalışmazAdvice tetiklenmez

Çözüm 1: Self-Injection

@Service
public class OrderService {

    @Lazy
    @Autowired
    private OrderService self;  // Proxy referansı

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        self.sendNotifications(order);  // ✅ Proxy üzerinden çağrı
        return order;
    }

    @Async
    public void sendNotifications(Order order) {
        emailService.sendOrderConfirmation(order);
        smsService.sendOrderSms(order);
    }
}

@Lazy annotation'ı circular dependency sorununu önler — Spring, inject sırasında lazy proxy oluşturur.

Çözüm 2: AopContext.currentProxy()

@Service
public class OrderService {

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        // Mevcut proxy'yi al ve üzerinden çağır
        ((OrderService) AopContext.currentProxy())
            .sendNotifications(order);  // ✅ Proxy üzerinden

        return order;
    }

    @Async
    public void sendNotifications(Order order) { ... }
}

Bu yöntem için exposeProxy ayarı gerekir:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application { }

⚠️ Dikkat: AopContext.currentProxy() cast gerektirir ve type-safe değildir. Self-injection yöntemi daha tercih edilir.

Çözüm 3: Ayrı Bean'e Çıkarmak (En Temiz)

@Service
@RequiredArgsConstructor
public class OrderService {

    private final NotificationService notificationService;

    @Transactional
    public Order createOrder(CreateOrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);

        notificationService.sendOrderNotifications(order);  // ✅ Farklı bean
        return order;
    }
}

@Service
public class NotificationService {

    @Async
    public void sendOrderNotifications(Order order) {
        emailService.sendOrderConfirmation(order);
        smsService.sendOrderSms(order);
    }
}

Bu en temiz çözümdür — Single Responsibility Principle'a da uyar.


final Sınıf ve Metot Kısıtlaması

CGLIB, subclass oluşturarak çalıştığı için final ile engellenebilir:

// ❌ final sınıf — CGLIB proxy oluşturamaz
@Service
public final class UserService {
    public UserResponse findById(Long id) { ... }
}
// → BeanCreationException: Could not generate CGLIB subclass

// ❌ final metot — Proxy override edemez, advice çalışmaz
@Service
public class UserService {
    @Transactional
    public final UserResponse findById(Long id) { ... }
    // → @Transactional ETKİSİZ — metot override edilemediği için
    //   proxy advice'ı uygulayamaz. Hata FIRLATILMAZ, sessizce çalışmaz!
}

⚠️ Kritik: final metotta AOP çalışmaması hata fırlatmaz — sessizce devre dışı kalır. Bu, debug edilmesi çok zor bir sorundur. @Transactional final metotta çalışmazsa, transaction açılmaz ve veri tutarsızlığı oluşabilir.

Kotlin Dikkat

Kotlin'de tüm sınıflar ve metotlar varsayılan olarak final'dir. Spring Boot + Kotlin için kotlin-spring compiler plugin gereklidir — bu plugin, @Service, @Component, @Transactional gibi annotation'larla işaretlenmiş sınıfları otomatik olarak open yapar.

// Kotlin — varsayılan final
class UserService {  // final!
    fun findById(id: Long): User { ... }  // final!
}

// kotlin-spring plugin ile:
// @Service class → open class
// @Transactional fun → open fun

Proxy Davranışını Debug Etme

Proxy Olup Olmadığını Kontrol

@Component
public class ProxyDebugger implements CommandLineRunner {

    @Autowired
    private ApplicationContext context;

    @Override
    public void run(String... args) {
        // Belirli bir bean'in proxy olup olmadığını kontrol et
        Object userService = context.getBean("userService");

        System.out.println("Class: " + userService.getClass().getName());
        System.out.println("Is AOP Proxy: " + AopUtils.isAopProxy(userService));
        System.out.println("Is CGLIB: " + AopUtils.isCglibProxy(userService));
        System.out.println("Is JDK: " + AopUtils.isJdkDynamicProxy(userService));
        System.out.println("Target class: " + AopUtils.getTargetClass(userService));

        // Proxy'nin advice'larını listele
        if (userService instanceof Advised advised) {
            for (Advisor advisor : advised.getAdvisors()) {
                System.out.println("Advisor: " + advisor.getAdvice().getClass().getName());
            }
        }
    }
}

Çıktı:

Class: com.example.service.UserService$$SpringCGLIB$$0
Is AOP Proxy: true
Is CGLIB: true
Is JDK: false
Target class: class com.example.service.UserService
Advisor: org.springframework.transaction.interceptor.TransactionInterceptor
Advisor: com.example.aspect.LoggingAspect

Spring Debug Loglama

# application.properties
logging.level.org.springframework.aop=DEBUG
logging.level.org.springframework.cglib=DEBUG

Spring AOP vs AspectJ — Tam Karşılaştırma

Spring AOP, proxy-based bir mekanizmadır. AspectJ ise derleme zamanı (compile-time) veya yükleme zamanı (load-time) weaving kullanır. Farkları:

ÖzellikSpring AOP (Proxy)AspectJ (Weaving)
MekanizmaRuntime proxyCompile/Load-time weaving
Join PointSadece metot çağrısıMetot, field, constructor, exception
Self-Invocation❌ Çalışmaz✅ Çalışır
final metot❌ Çalışmaz✅ Çalışır
private metot❌ Çalışmaz✅ Çalışır
PerformanceBiraz overheadSıfır overhead (weaved)
KurulumBasit (starter yeterli)Karmaşık (AspectJ compiler/weaver)
Spring bean gerekli mi?EvetHayır
Öğrenme eğrisiDüşükYüksek

Ne zaman AspectJ?

  • Self-invocation sorunu çözülemiyorsa

  • Private metotlara aspect uygulamak gerekiyorsa

  • Non-Spring nesnelerine (new ile oluşturulan) aspect gerekiyorsa

  • Ekstra performance kritik ise

Gerçekte: Spring AOP, projelerin %95'i için yeterlidir. AspectJ'ye geçmek nadiren gerekir.


Proxy İle İlgili Yaygın Hatalar

1. ❌ instanceof Kontrolü Yanlış Çalışması

@Autowired
private UserService userService;

// CGLIB proxy'de:
System.out.println(userService instanceof UserService);        // true ✅
System.out.println(userService instanceof UserServiceImpl);    // Compile error (interface yok)

// JDK proxy'de (interface varsa):
System.out.println(userService instanceof UserService);          // true ✅ (interface)
System.out.println(userService instanceof UserServiceImpl);      // false ❌ (impl değil!)

2. ❌ Field'lara Doğrudan Erişim

@Service
public class UserService {
    public String serviceName = "UserService";  // public field

    @Loggable
    public void doSomething() { ... }
}

// Controller'da:
System.out.println(userService.serviceName);
// CGLIB proxy'de → null olabilir!
// Çünkü proxy subclass'tır, parent'ın field'ını inherit etmez

💡 İpucu: Bean'lerin field'larına doğrudan erişmeyin, getter kullanın.

3. ❌ @PostConstruct'ta Proxy Olmaması

@Service
public class UserService {

    @PostConstruct
    public void init() {
        this.doSomething();  // ❌ Proxy henüz tam oluşmamış olabilir
        // @Transactional, @Cacheable çalışmayabilir
    }

    @Transactional
    public void doSomething() { ... }
}

// ✅ Çözüm: ApplicationReadyEvent kullanın
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
    // Tüm proxy'ler hazır
    self.doSomething();  // ✅ Güvenli
}

Proxy Oluşturma Süreci (Detaylı)

Spring Container Başlatılıyor...
    │
    ├─ 1. Bean tanımları (BeanDefinition) okunur
    │
    ├─ 2. Bean nesneleri oluşturulur (constructor)
    │
    ├─ 3. Dependency injection yapılır (@Autowired)
    │
    ├─ 4. BeanPostProcessor'lar çalışır
    │     │
    │     └─ AbstractAutoProxyCreator (AOP)
    │           │
    │           ├─ Bu bean'e uygulanacak advisor (aspect) var mı?
    │           │
    │           ├─ Evet → Proxy oluştur
    │           │   ├─ Interface var mı?
    │           │   │   ├─ Evet + proxyTargetClass=false → JDK Dynamic Proxy
    │           │   │   └─ Evet + proxyTargetClass=true  → CGLIB (varsayılan)
    │           │   └─ Interface yok → CGLIB
    │           │
    │           └─ Hayır → Orijinal bean'i döndür
    │
    ├─ 5. @PostConstruct çalışır
    │
    └─ 6. Bean container'a kaydedilir (proxy veya orijinal)

Özet

  • Spring AOP proxy-based çalışır — orijinal bean yerine proxy nesnesi oluşturulur

  • CGLIB Spring Boot'un varsayılanıdır — subclass oluşturarak çalışır, interface zorunlu değil

  • JDK Dynamic Proxy sadece interface'ler için çalışır, Spring Boot'ta artık varsayılan değil

  • Self-invocation AOP'nin en büyük tuzağıdır: this.method() çağrısı proxy'yi bypass eder → çözüm: self-injection, ayrı bean, veya AopContext

  • `final` sınıf/metot CGLIB proxy oluşturulamaz/override edilemez — advice sessizce devre dışı kalır

  • Private metotlar proxy'lenemez — AOP çalışmaz

  • Debug için AopUtils.isAopProxy(), Advised interface'i ve Spring AOP debug logging kullanın

  • Projelerin %95'inde Spring AOP yeterlidir — AspectJ'ye geçiş nadiren gerekir