← Kursa Dön
📄 Text · 35 min

Spring AOP Advice Türleri

Bir binanın güvenlik sistemini düşünün. Bina girişinde farklı güvenlik mekanizmaları çalışır: kapıdan geçmeden önce kimlik kontrolü (@Before), binadan çıktıktan sonra çıkış kaydı (@After), randevuya başarıyla ulaşıldığında misafir defterine not düşülmesi (@AfterReturning), yangın alarmı çaldığında acil tahliye prosedürü (@AfterThrowing), ve VIP misafirler için özel kapıdan giriş-çıkışın tamamen kontrol edilmesi (@Around).

Spring AOP'de 5 farklı advice türü vardır ve her biri farklı bir zamanlamada devreye girer. Bu derste her birini detaylı örneklerle inceleyeceğiz.


Dependency ve Temel Kurulum

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Bu starter, Spring AOP ve AspectJ annotation'larını içerir. Ek bir konfigürasyon gerekmez.

@Aspect + @Component

Bir sınıfın aspect olabilmesi için iki annotation gerekir:

@Aspect     // "Bu sınıf AOP advice'ları içerir"
@Component  // "Spring bean olarak kaydet" (yoksa AOP ÇALIŞMAZ!)
@Slf4j
public class LoggingAspect {
    // advice metotları burada
}

⚠️ Dikkat: @Aspect tek başına yetmez! @Component (veya @Service, @Configuration) olmadan Spring bu sınıfı bean olarak yönetmez ve advice'lar çalışmaz.


5 Advice Türü — Genel Bakış

AdviceNe Zaman ÇalışırDönüş Değerine ErişimException'a ErişimMetodu Engelleyebilir mi?
@BeforeMetot öncesindeSadece exception fırlatarak
@AfterMetot sonrasında (her durumda)
@AfterReturningMetot başarıyla döndükten sonra
@AfterThrowingMetot exception fırlattığında
@AroundMetot öncesinde VE sonrasında✅ Tam kontrol

Görsel Akış

@Around (before proceed)
  → @Before
    → Hedef Metot Çalışır
  → @AfterReturning (başarılıysa)
  → @AfterThrowing (hata varsa)
  → @After (her durumda)
→ @Around (after proceed)

JoinPoint — Metot Bilgi Sağlayıcı

Tüm advice metotları (except @Around) JoinPoint parametresi alabilir. Bu nesne, çağrılan metot hakkında bilgi sağlar:

@Before("execution(* com.example.service.*.*(..))")
public void logMethodInfo(JoinPoint joinPoint) {
    // Metot imzası
    String methodName = joinPoint.getSignature().getName();
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String fullSignature = joinPoint.getSignature().toShortString();

    // Parametreler
    Object[] args = joinPoint.getArgs();

    // Proxy vs gerçek nesne
    Object target = joinPoint.getTarget();   // Gerçek nesne
    Object proxy = joinPoint.getThis();       // Proxy nesnesi

    log.info("{}.{}() — args: {}", className, methodName, Arrays.toString(args));
}

JoinPoint Metotları:

MetotDönüşAçıklama
getSignature()SignatureMetot imzası (isim, dönüş tipi, parametre tipleri)
getArgs()Object[]Metot parametreleri
getTarget()ObjectGerçek nesne (proxy değil)
getThis()ObjectProxy nesnesi
getSignature().getName()StringMetot adı
getSignature().toShortString()StringKısa imza: UserService.findById(..)
getSignature().toLongString()StringTam imza: public User com.example.service.UserService.findById(Long)

@Before — Metot Öncesi

@Before advice, hedef metot çalışmadan önce tetiklenir. Genellikle şu amaçlarla kullanılır:

  • Giriş loglama

  • Parametre doğrulama

  • Güvenlik kontrolü

  • Audit kaydı başlatma

@Aspect
@Component
@Slf4j
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logMethodEntry(JoinPoint joinPoint) {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        log.info("→ {}.{}() çağrılıyor, parametreler: {}",
            className, methodName, formatArgs(args));
    }

    private String formatArgs(Object[] args) {
        if (args == null || args.length == 0) return "[]";
        return Arrays.stream(args)
            .map(arg -> arg == null ? "null" : arg.toString())
            .collect(Collectors.joining(", ", "[", "]"));
    }
}

@Before ile Parametre Doğrulama

@Aspect
@Component
@Slf4j
public class ValidationAspect {

    @Before("execution(* com.example.service.UserService.create*(..)) && args(request,..)")
    public void validateBeforeCreate(JoinPoint joinPoint, CreateUserRequest request) {
        log.info("Kullanıcı oluşturma isteği doğrulanıyor: {}", request.email());

        // İş kuralı kontrolü (validation annotation'larının ötesinde)
        if (request.email().endsWith("@temp-mail.com")) {
            throw new BusinessRuleException(
                "Geçici email adresleri ile kayıt yapılamaz");
        }
    }
}

@Before'un Sınırlılıkları

  • Dönüş değerine erişemez (metot henüz çalışmadı)

  • Metodu engelleyemez (sadece exception fırlatarak engelleyebilir)

  • Süre ölçümü yapamaz (öncesi ve sonrası gerekli → @Around kullanın)


@After — Metot Sonrası (Her Durumda)

@After advice, hedef metot her durumda (başarılı veya exception) çalıştıktan sonra tetiklenir. Java'daki finally bloğuna benzer.

@Aspect
@Component
@Slf4j
public class ResourceCleanupAspect {

    @After("execution(* com.example.service.*.*(..))")
    public void logMethodExit(JoinPoint joinPoint) {
        log.debug("← {}.{}() tamamlandı (başarılı veya başarısız)",
            joinPoint.getTarget().getClass().getSimpleName(),
            joinPoint.getSignature().getName());
    }
}

Gerçek Dünya Kullanımı: Kaynak Temizleme

@Aspect
@Component
@Slf4j
public class ContextCleanupAspect {

    // Her service çağrısından sonra MDC'yi temizle
    @After("execution(* com.example.service.*.*(..))")
    public void cleanupMDC(JoinPoint joinPoint) {
        MDC.remove("operationId");
        MDC.remove("userId");
    }

    // ThreadLocal temizleme
    @After("execution(* com.example.controller.*.*(..))")
    public void cleanupThreadLocal() {
        RequestContext.clear();
    }
}

@After'ın Özellikleri

  • Metot başarılı olsa da, exception fırlatsa da çalışır

  • Dönüş değerine ve exception'a erişemez (sadece metot tamamlandığını bilir)

  • finally bloğu gibi davranır — cleanup işlemleri için ideal


@AfterReturning — Başarılı Dönüş Sonrası

@AfterReturning advice, metot başarıyla (exception fırlatmadan) döndükten sonra çalışır. returning parametresi ile dönüş değerine erişebilirsiniz:

@Aspect
@Component
@Slf4j
public class AuditAspect {

    @AfterReturning(
        pointcut = "execution(* com.example.service.OrderService.createOrder(..))",
        returning = "order"  // Dönüş değerini "order" parametresine bağla
    )
    public void afterOrderCreated(JoinPoint joinPoint, Order order) {
        log.info("✅ Yeni sipariş oluşturuldu — ID: {}, Toplam: {} TL",
            order.getId(), order.getTotalAmount());

        // Metrik kaydet
        metricsService.incrementOrderCount();
        metricsService.recordOrderAmount(order.getTotalAmount());
    }
}

Dönüş Değeri Tipine Göre Filtreleme

returning parametresinin tipi, advice'ın hangi metotlar için çalışacağını da filtreler:

// Sadece List döndüren metotlar için çalışır
@AfterReturning(
    pointcut = "execution(* com.example.repository.*.*(..))",
    returning = "result"
)
public void logCollectionResult(JoinPoint joinPoint, List<?> result) {
    log.info("{} sorgusu {} kayıt döndürdü",
        joinPoint.getSignature().getName(), result.size());
}

// Sadece User döndüren metotlar için çalışır
@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "user"
)
public void logUserResult(JoinPoint joinPoint, User user) {
    log.info("User döndürüldü: {} ({})", user.getName(), user.getEmail());
}

// Object ile — TÜM dönüş tipleri yakalanır
@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "result"
)
public void logAllResults(JoinPoint joinPoint, Object result) {
    if (result instanceof Collection<?> c) {
        log.info("{} → {} kayıt", joinPoint.getSignature().getName(), c.size());
    } else if (result != null) {
        log.info("{} → {}", joinPoint.getSignature().getName(),
            result.getClass().getSimpleName());
    }
}

@AfterReturning'in Sınırlılıkları

  • Dönüş değerini değiştiremez (sadece okuyabilir)

  • Exception durumunda çalışmaz (bunun için @AfterThrowing var)

  • Dönüş değerini değiştirmek için @Around kullanın


@AfterThrowing — Exception Sonrası

@AfterThrowing advice, metot exception fırlattığında çalışır. throwing parametresi ile exception nesnesine erişebilirsiniz:

@Aspect
@Component
@Slf4j
public class ErrorMonitoringAspect {

    @AfterThrowing(
        pointcut = "execution(* com.example.service.*.*(..))",
        throwing = "ex"  // Exception'ı "ex" parametresine bağla
    )
    public void handleServiceException(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().toShortString();

        log.error("❌ {} hata fırlattı: {} — {}",
            methodName,
            ex.getClass().getSimpleName(),
            ex.getMessage());

        // Alert gönder
        alertService.sendAlert(
            "Service Error",
            String.format("%s failed: %s", methodName, ex.getMessage()),
            AlertSeverity.HIGH
        );
    }
}

Exception Tipine Göre Filtreleme

throwing parametresinin tipi, hangi exception'ların yakalanacağını belirler:

// Sadece BusinessException ve alt sınıfları
@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))",
    throwing = "ex"
)
public void handleBusinessException(JoinPoint joinPoint, BusinessException ex) {
    log.warn("⚠️ İş kuralı ihlali [{}]: {}", ex.getErrorCode(), ex.getMessage());
    metricsService.incrementBusinessError(ex.getErrorCode());
}

// Sadece DataAccessException (veritabanı hataları)
@AfterThrowing(
    pointcut = "execution(* com.example.repository.*.*(..))",
    throwing = "ex"
)
public void handleDbException(JoinPoint joinPoint, DataAccessException ex) {
    log.error("🗄️ Veritabanı hatası: {} — {}",
        joinPoint.getSignature().toShortString(), ex.getMessage());
    alertService.sendDatabaseAlert(ex);
}

⚠️ @AfterThrowing Exception'ı YAKALAMAZ

Kritik bir nokta: @AfterThrowing, exception'ı gözlemler ama yakalamaz. Exception, çağırana iletilmeye devam eder:

@AfterThrowing(pointcut = "...", throwing = "ex")
public void observe(Exception ex) {
    log.error("Hata gözlemlendi: {}", ex.getMessage());
    // Exception HÂLÂ çağırana fırlatılır!
    // Bu advice sadece "bilgilendirme" amaçlıdır
}

Exception'ı yakalayıp:

  • Farklı bir exception'a çevirmek istiyorsanız → @Around kullanın

  • Varsayılan değer döndürmek istiyorsanız → @Around kullanın

  • Retry yapmak istiyorsanız → @Around kullanın


@Around — Tam Kontrol

@Around, en güçlü advice türüdür. Diğer dördünün yapabildiği her şeyi tek başına yapabilir. ProceedingJoinPoint ile hedef metodu tamamen kontrol eder:

@Aspect
@Component
@Slf4j
public class PerformanceAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();

        // ── @Before bölgesi ──
        log.info("→ {} başlıyor", methodName);
        long startTime = System.nanoTime();

        try {
            // ── Hedef metodu çalıştır ──
            Object result = joinPoint.proceed();

            // ── @AfterReturning bölgesi ──
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(
                System.nanoTime() - startTime);
            log.info("← {} tamamlandı: {}ms", methodName, elapsedMs);

            return result;  // ⚠️ Sonucu MUTLAKA döndür!

        } catch (Throwable ex) {
            // ── @AfterThrowing bölgesi ──
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(
                System.nanoTime() - startTime);
            log.error("✕ {} hata: {}ms — {}", methodName, elapsedMs, ex.getMessage());

            throw ex;  // Exception'ı tekrar fırlat

        } finally {
            // ── @After bölgesi (her durumda) ──
            MDC.remove("methodName");
        }
    }
}

proceed() ile Tam Kontrol

@Around("execution(* com.example.service.*.*(..))")
public Object fullControl(ProceedingJoinPoint jp) throws Throwable {

    // 1. Parametreleri değiştirme
    Object[] args = jp.getArgs();
    if (args.length > 0 && args[0] instanceof String s) {
        args[0] = s.trim().toLowerCase();  // String parametreyi normalize et
    }
    Object result = jp.proceed(args);  // Değiştirilmiş parametrelerle çağır

    // 2. Dönüş değerini değiştirme
    if (result instanceof String s) {
        return s.toUpperCase();  // String sonucu büyük harfe çevir
    }
    return result;

    // 3. Metodu hiç çağırmama (cache, circuit breaker)
    // return cachedValue;  // proceed() çağrılmadı → metot çalışmadı
}

Advice Çalışma Sırası

Tek Aspect İçindeki Sıra

İstek geldi
    │
    ├─ @Around (proceed öncesi)
    │   ├─ @Before
    │   │   └─ Hedef Metot ── başarılıysa ──→ @AfterReturning
    │   │                   ── hata varsa ──→ @AfterThrowing
    │   │                   ── her durumda ──→ @After
    │   └─ @Around (proceed sonrası)
    │
    └─ Sonuç döner

Birden Fazla Aspect — @Order ile Sıralama

@Aspect
@Component
@Order(1)  // En düşük değer = EN DIŞTA, en önce başlar
public class SecurityAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void checkSecurity(JoinPoint jp) {
        log.info("1. Güvenlik kontrolü");
    }
}

@Aspect
@Component
@Order(2)
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logEntry(JoinPoint jp) {
        log.info("2. Giriş logu");
    }
}

@Aspect
@Component
@Order(3)  // En yüksek değer = EN İÇTE, hedefe en yakın
public class ValidationAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void validate(JoinPoint jp) {
        log.info("3. Validation");
    }
}

Çalışma sırası (@Before için):

SecurityAspect.checkSecurity()   ← @Order(1) — ilk çalışır
  → LoggingAspect.logEntry()     ← @Order(2)
    → ValidationAspect.validate() ← @Order(3) — son çalışır
      → Hedef Metot

@After/@AfterReturning için ters sıra:

      ← Hedef Metot döner
    ← ValidationAspect.after()    ← @Order(3) — ilk döner
  ← LoggingAspect.after()        ← @Order(2)
← SecurityAspect.after()         ← @Order(1) — son döner

Bu, soğan modeli (onion model) olarak bilinir — her aspect bir katman, en dıştaki ilk girer son çıkar.

@Order Belirtilmezse?

@Order belirtilmeyen aspect'lerin sırası belirsizdir (implementation-dependent). Sıralamanın önemli olduğu durumlarda her zaman @Order kullanın.


Gerçek Dünya Senaryosu: Katmanlı Aspect'ler

// ─── 1. Güvenlik (en dışta) ───
@Aspect @Component @Order(1) @Slf4j
public class SecurityAspect {

    @Before("@annotation(requireRole)")
    public void checkRole(JoinPoint jp, RequireRole requireRole) {
        var auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null || !hasRequiredRole(auth, requireRole.value())) {
            throw new ForbiddenException("Yetki yetersiz");
        }
        log.debug("🔐 Güvenlik kontrolü geçti: {}", auth.getName());
    }
}

// ─── 2. Loglama ───
@Aspect @Component @Order(2) @Slf4j
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logEntry(JoinPoint jp) {
        log.info("→ {}.{}()",
            jp.getTarget().getClass().getSimpleName(),
            jp.getSignature().getName());
    }

    @AfterReturning(
        pointcut = "execution(* com.example.service.*.*(..))",
        returning = "result"
    )
    public void logExit(JoinPoint jp, Object result) {
        log.info("← {}.{}() → {}",
            jp.getTarget().getClass().getSimpleName(),
            jp.getSignature().getName(),
            result != null ? result.getClass().getSimpleName() : "void");
    }
}

// ─── 3. Performance (en içte) ───
@Aspect @Component @Order(3) @Slf4j
public class PerformanceAspect {

    @Around("@annotation(monitored)")
    public Object measure(ProceedingJoinPoint jp, Monitored monitored) throws Throwable {
        long start = System.nanoTime();
        try {
            return jp.proceed();
        } finally {
            long ms = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            if (ms > monitored.warnThresholdMs()) {
                log.warn("🐢 Yavaş: {} — {}ms", jp.getSignature().toShortString(), ms);
            }
        }
    }
}

// ─── 4. Hata İzleme ───
@Aspect @Component @Order(2) @Slf4j
public class ErrorTrackingAspect {

    @AfterThrowing(
        pointcut = "execution(* com.example.service.*.*(..))",
        throwing = "ex"
    )
    public void trackError(JoinPoint jp, Exception ex) {
        if (!(ex instanceof BusinessException)) {
            // Beklenmedik hatalar için alert gönder
            log.error("🚨 Beklenmedik hata: {}.{}() — {}",
                jp.getTarget().getClass().getSimpleName(),
                jp.getSignature().getName(),
                ex.getMessage());
            alertService.notify(jp.getSignature().toShortString(), ex);
        }
    }
}

Advice Türünü Seçme Rehberi

Ne yapmak istiyorsun?
│
├─ Sadece log/audit (metottan önce)?
│   └─ @Before ✅
│
├─ Kaynak temizleme (her durumda)?
│   └─ @After ✅
│
├─ Dönüş değerini okumak/loglamak?
│   └─ @AfterReturning ✅
│
├─ Exception'ı gözlemlemek/loglamak?
│   └─ @AfterThrowing ✅
│
├─ Süre ölçümü?
│   └─ @Around ✅ (before + after gerekli)
│
├─ Dönüş değerini değiştirmek?
│   └─ @Around ✅ (sadece Around yapabilir)
│
├─ Retry mekanizması?
│   └─ @Around ✅ (proceed() tekrar çağırılır)
│
├─ Cache kontrolü (metodu çağırmama)?
│   └─ @Around ✅ (proceed() çağrılmaz)
│
└─ Emin değilim...
    └─ @Around ✅ (hepsini yapabilir)
        ⚠️ Ama gereksiz karmaşıklık eklemeyin

💡 Principle of Least Power: Sadece loglama gerekiyorsa @Before yeterli — @Around kullanmak gereksiz karmaşıklıktır. Her zaman en basit advice türünü tercih edin.


Yaygın Hatalar

1. ❌ @Component Unutmak

// ❌ YANLIŞ — aspect çalışmaz
@Aspect
public class LoggingAspect { }

// ✅ DOĞRU
@Aspect
@Component
public class LoggingAspect { }

2. ❌ @Around'da proceed() Sonucunu Döndürmemek

// ❌ YANLIŞ — caller null alır
@Around("...")
public void broken(ProceedingJoinPoint jp) throws Throwable {
    jp.proceed();  // Return yok!
}

// ✅ DOĞRU
@Around("...")
public Object correct(ProceedingJoinPoint jp) throws Throwable {
    return jp.proceed();
}

3. ❌ @AfterThrowing ile Exception Yakalamaya Çalışmak

// ❌ YANLIŞ — exception hâlâ fırlatılır
@AfterThrowing(pointcut = "...", throwing = "ex")
public void handle(Exception ex) {
    // Bu advice exception'ı durduramaz!
}

// ✅ Exception'ı yakalamak için @Around kullanın
@Around("...")
public Object catchException(ProceedingJoinPoint jp) throws Throwable {
    try {
        return jp.proceed();
    } catch (SpecificException ex) {
        return defaultValue;  // Exception yakalandı, varsayılan değer döndürüldü
    }
}

Özet

  • `@Before`: Metot öncesinde çalışır — loglama, güvenlik kontrolü, parametre doğrulama

  • `@After`: Metot sonrasında her durumda çalışır — kaynak temizleme (finally gibi)

  • `@AfterReturning`: Metot başarıyla döndükten sonra çalışır — dönüş değerine erişim, audit

  • `@AfterThrowing`: Metot exception fırlattığında çalışır — hata izleme, alert. Exception'ı yakalamaz!

  • `@Around`: Metodu tamamen sarar — süre ölçümü, retry, cache, dönüş değeri değiştirme

  • @Order ile aspect sıralaması belirlenir — düşük değer = önce çalışır (soğan modeli)

  • Principle of Least Power: En basit advice türünü tercih edin

  • @Aspect + @Component ikisi birlikte gereklidir