← Kursa Dön
📄 Text · 18 min

@Around Advice ile Logging ve Performance

Bir arabanın dashboard'unu düşünün. Motor çalışırken hız, devir, yakıt, motor sıcaklığı — hepsini gerçek zamanlı izlersiniz. Ama bu sensörler motorun içine gömülü değildir. Motor sadece işini yapar; sensörler dışarıdan sararak (around) ölçüm alır. Motor bozulursa sensör alarm verir, motor yavaşsa uyarır — ama motorun çalışma mantığını hiç değiştirmez.

Spring AOP'deki @Around advice, tam olarak bu sensör sistemidir. Bir metodu tamamen sarar — çalışmadan önce, çalıştıktan sonra ve hata durumunda müdahale edebilir. Hatta metodu hiç çalıştırmama seçeneğiniz bile vardır (circuit breaker gibi). Bu, @Before + @AfterReturning + @AfterThrowing'in hepsini tek bir yerde birleştirmektir.

Bu derste @Around advice'ın gerçek dünya kullanımlarını detaylıca inceleyeceğiz: performance monitoring, structured logging, caching, retry mekanizması ve rate limiting.


@Around Neden En Güçlü Advice?

Diğer advice türleri ile karşılaştırma:

Özellik@Before@After@AfterReturning@AfterThrowing@Around
Metot öncesi kod
Metot sonrası kod
Dönüş değerine erişim
Exception'a erişim
Dönüş değerini değiştirme
Metodu çağırmama
Parametreleri değiştirme
Süre ölçümü

@Around tek başına diğer dördünün yapabildiği her şeyi yapabilir. Peki neden hep @Around kullanmıyoruz? Çünkü en az yetki prensibi (principle of least power) geçerlidir — sadece loglama için @Before yeterliyse, @Around kullanmak gereksiz karmaşıklıktır.


ProceedingJoinPoint — Kontrol Paneli

@Around advice'ın diğerlerinden farkı ProceedingJoinPoint parametresidir. Normal JoinPoint sadece bilgi verir, ProceedingJoinPoint ise kontrol sağlar:

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

    // ─── BİLGİ ALMA (JoinPoint'ten miras) ───
    String className = joinPoint.getTarget().getClass().getSimpleName();
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();

    // ─── KONTROL (ProceedingJoinPoint'e özel) ───

    // Hedef metodu orijinal parametrelerle çağır
    Object result = joinPoint.proceed();

    // VEYA parametreleri değiştirerek çağır
    Object[] modifiedArgs = new Object[]{sanitize(args[0])};
    Object result2 = joinPoint.proceed(modifiedArgs);

    // VEYA hiç çağırma (circuit breaker)
    // return cachedValue;

    return result;  // ⚠️ Sonucu döndürmeyi UNUTMA!
}

⚠️ Kritik Kural: @Around advice'ta joinPoint.proceed() çağrısının sonucunu mutlaka return etmelisiniz. Aksi halde hedef metot çalışsa bile çağırana null döner:

// ❌ YANLIŞ — sonuç kaybolur
@Around("execution(* com.example.service.*.*(..))")
public Object broken(ProceedingJoinPoint jp) throws Throwable {
    jp.proceed();  // Sonuç atanmadı!
    // return yok → null döner → NullPointerException
}

// ✅ DOĞRU
@Around("execution(* com.example.service.*.*(..))")
public Object correct(ProceedingJoinPoint jp) throws Throwable {
    Object result = jp.proceed();
    return result;  // Sonucu çağırana ilet
}

Kullanım 1: Performance Monitoring

Production'daki en önemli cross-cutting concern'lerden biri, metot çalışma sürelerini izlemektir. Yavaş metotlar tespit edilmezse zamanla kullanıcı deneyimi kötüleşir.

Temel Performance Aspect

@Aspect
@Component
@Slf4j
public class PerformanceAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodSignature = joinPoint.getSignature().toShortString();
        long startTime = System.nanoTime();

        try {
            Object result = joinPoint.proceed();

            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
            log.info("✅ {} completed in {}ms", methodSignature, elapsedMs);

            // Yavaş metot uyarısı
            if (elapsedMs > 1000) {
                log.warn("🐢 SLOW METHOD: {} took {}ms (threshold: 1000ms)",
                    methodSignature, elapsedMs);
            }

            return result;

        } catch (Throwable ex) {
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime);
            log.error("❌ {} failed after {}ms: {}", methodSignature, elapsedMs,
                ex.getMessage());
            throw ex;
        }
    }
}

💡 İpucu: System.nanoTime() kullanın, System.currentTimeMillis() değil. currentTimeMillis() duvar saati zamanıdır ve NTP senkronizasyonu sırasında geriye sıçrayabilir. nanoTime() monotonik zamandır — sadece süre ölçümü için tasarlanmıştır.

Custom Annotation ile Selektif Monitoring

Her metodu izlemek gereksiz log kirliliği yaratır. Bunun yerine sadece izlemek istediğiniz metotları annotation ile işaretleyin:

// ─── Annotation Tanımı ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
    String value() default "";              // Özel etiket
    long warnThresholdMs() default 500;     // Uyarı eşiği (ms)
    long errorThresholdMs() default 3000;   // Hata eşiği (ms)
}

// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class MonitoringAspect {

    @Around("@annotation(monitored)")
    public Object monitor(ProceedingJoinPoint joinPoint, Monitored monitored) throws Throwable {
        String label = monitored.value().isEmpty()
            ? joinPoint.getSignature().toShortString()
            : monitored.value();

        long start = System.nanoTime();

        try {
            Object result = joinPoint.proceed();
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

            // Eşik seviyelerine göre farklı log level
            if (elapsedMs >= monitored.errorThresholdMs()) {
                log.error("🔴 {} took {}ms (ERROR threshold: {}ms)",
                    label, elapsedMs, monitored.errorThresholdMs());
            } else if (elapsedMs >= monitored.warnThresholdMs()) {
                log.warn("🟡 {} took {}ms (WARN threshold: {}ms)",
                    label, elapsedMs, monitored.warnThresholdMs());
            } else {
                log.debug("🟢 {} completed in {}ms", label, elapsedMs);
            }

            return result;

        } catch (Throwable ex) {
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            log.error("💥 {} failed after {}ms: {}", label, elapsedMs, ex.getMessage());
            throw ex;
        }
    }
}

// ─── Kullanım ───
@Service
public class OrderService {

    @Monitored(value = "Create Order", warnThresholdMs = 200, errorThresholdMs = 1000)
    public OrderResponse createOrder(CreateOrderRequest request) {
        // İş mantığı...
    }

    @Monitored("Fetch User Orders")
    public List<OrderResponse> getUserOrders(Long userId) {
        // İş mantığı...
    }
}

Micrometer Entegrasyonu

Production'da loglardan daha etkili bir yol, metrics toplamaktır. Micrometer ile metot sürelerini Prometheus/Grafana'ya aktarabilirsiniz:

@Aspect
@Component
public class MetricsAspect {

    private final MeterRegistry meterRegistry;

    public MetricsAspect(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Around("@annotation(monitored)")
    public Object recordMetrics(ProceedingJoinPoint joinPoint,
                                  Monitored monitored) throws Throwable {
        String metricName = monitored.value().isEmpty()
            ? joinPoint.getSignature().getName()
            : monitored.value().replace(" ", "_").toLowerCase();

        Timer.Sample sample = Timer.start(meterRegistry);

        try {
            Object result = joinPoint.proceed();

            sample.stop(Timer.builder("method.execution")
                .tag("method", metricName)
                .tag("outcome", "success")
                .register(meterRegistry));

            return result;

        } catch (Throwable ex) {
            sample.stop(Timer.builder("method.execution")
                .tag("method", metricName)
                .tag("outcome", "error")
                .tag("exception", ex.getClass().getSimpleName())
                .register(meterRegistry));

            meterRegistry.counter("method.errors",
                "method", metricName,
                "exception", ex.getClass().getSimpleName()
            ).increment();

            throw ex;
        }
    }
}

Kullanım 2: Structured Logging

Modern uygulamalarda log mesajlarının yapılandırılmış (structured) olması önemlidir. JSON formatında loglar, ELK Stack / Grafana Loki gibi araçlarla kolayca aranabilir:

@Aspect
@Component
@Slf4j
public class StructuredLoggingAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object logWithContext(ProceedingJoinPoint joinPoint) throws Throwable {
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();
        String fullMethod = className + "." + methodName;

        // MDC'ye bağlam bilgisi ekle (her log satırında görünür)
        MDC.put("method", fullMethod);
        MDC.put("args", summarizeArgs(joinPoint.getArgs()));

        long start = System.nanoTime();

        try {
            log.info("Method entry");

            Object result = joinPoint.proceed();

            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            MDC.put("duration_ms", String.valueOf(elapsedMs));
            MDC.put("outcome", "success");
            log.info("Method exit");

            return result;

        } catch (Throwable ex) {
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
            MDC.put("duration_ms", String.valueOf(elapsedMs));
            MDC.put("outcome", "error");
            MDC.put("error_type", ex.getClass().getSimpleName());
            log.error("Method failed: {}", ex.getMessage());

            throw ex;

        } finally {
            MDC.remove("method");
            MDC.remove("args");
            MDC.remove("duration_ms");
            MDC.remove("outcome");
            MDC.remove("error_type");
        }
    }

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

⚠️ Dikkat: finally bloğunda MDC'yi temizlemeyi asla unutmayın. Thread pool'larda aynı thread farklı istekler için yeniden kullanılır — MDC temizlenmezse önceki isteğin bilgileri sonraki isteğe sızar.


Kullanım 3: Retry Mekanizması

Geçici hatalar (network timeout, database connection lost) için otomatik yeniden deneme:

// ─── Annotation ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
    int maxAttempts() default 3;
    long initialDelayMs() default 100;
    double backoffMultiplier() default 2.0;
    Class<? extends Throwable>[] retryOn() default {Exception.class};
    Class<? extends Throwable>[] noRetryOn() default {};
}

// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class RetryAspect {

    @Around("@annotation(retryable)")
    public Object retry(ProceedingJoinPoint joinPoint, Retryable retryable) throws Throwable {
        int maxAttempts = retryable.maxAttempts();
        long delay = retryable.initialDelayMs();
        String methodName = joinPoint.getSignature().toShortString();

        Throwable lastException = null;

        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                if (attempt > 1) {
                    log.info("🔄 {} — Retry attempt {}/{}", methodName, attempt, maxAttempts);
                }

                return joinPoint.proceed();

            } catch (Throwable ex) {
                lastException = ex;

                // Bu exception retry edilmeli mi?
                if (!shouldRetry(ex, retryable)) {
                    log.warn("🚫 {} — Exception {} is not retryable, throwing immediately",
                        methodName, ex.getClass().getSimpleName());
                    throw ex;
                }

                if (attempt < maxAttempts) {
                    log.warn("⚠️ {} — Attempt {}/{} failed: {}. Retrying in {}ms...",
                        methodName, attempt, maxAttempts,
                        ex.getMessage(), delay);
                    Thread.sleep(delay);
                    delay = (long) (delay * retryable.backoffMultiplier());
                } else {
                    log.error("💀 {} — All {} attempts failed", methodName, maxAttempts);
                }
            }
        }

        throw lastException;
    }

    private boolean shouldRetry(Throwable ex, Retryable retryable) {
        // noRetryOn listesinde mi?
        for (Class<? extends Throwable> noRetry : retryable.noRetryOn()) {
            if (noRetry.isInstance(ex)) return false;
        }
        // retryOn listesinde mi?
        for (Class<? extends Throwable> retry : retryable.retryOn()) {
            if (retry.isInstance(ex)) return true;
        }
        return false;
    }
}

// ─── Kullanım ───
@Service
public class PaymentService {

    @Retryable(
        maxAttempts = 3,
        initialDelayMs = 200,
        backoffMultiplier = 2.0,
        retryOn = {TransientDataAccessException.class, SocketTimeoutException.class},
        noRetryOn = {BusinessRuleException.class}
    )
    public PaymentResult processPayment(PaymentRequest request) {
        return paymentGateway.charge(request);
    }
}

Log çıktısı:

⚠️ PaymentService.processPayment(..) — Attempt 1/3 failed: Connection timed out. Retrying in 200ms...
🔄 PaymentService.processPayment(..) — Retry attempt 2/3
⚠️ PaymentService.processPayment(..) — Attempt 2/3 failed: Connection timed out. Retrying in 400ms...
🔄 PaymentService.processPayment(..) — Retry attempt 3/3
✅ PaymentService.processPayment(..) completed in 623ms

Kullanım 4: Rate Limiting

Belirli metotların saniyede/dakikada kaç kez çağrılabileceğini sınırlamak:

// ─── Annotation ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimited {
    int maxRequests() default 10;
    int windowSeconds() default 60;
    String key() default "";  // Boşsa metot adı kullanılır
}

// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    // Basit in-memory rate limiter (production'da Redis kullanın)
    private final Map<String, Deque<Instant>> requestLog = new ConcurrentHashMap<>();

    @Around("@annotation(rateLimited)")
    public Object rateLimit(ProceedingJoinPoint joinPoint,
                              RateLimited rateLimited) throws Throwable {

        String key = rateLimited.key().isEmpty()
            ? joinPoint.getSignature().toShortString()
            : rateLimited.key();

        int maxRequests = rateLimited.maxRequests();
        Duration window = Duration.ofSeconds(rateLimited.windowSeconds());

        Deque<Instant> timestamps = requestLog.computeIfAbsent(key,
            k -> new ConcurrentLinkedDeque<>());

        // Pencere dışındaki eski istekleri temizle
        Instant cutoff = Instant.now().minus(window);
        while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(cutoff)) {
            timestamps.pollFirst();
        }

        // Limit kontrolü
        if (timestamps.size() >= maxRequests) {
            log.warn("🚫 Rate limit exceeded for {}: {}/{} in {}s",
                key, timestamps.size(), maxRequests, rateLimited.windowSeconds());
            throw new RateLimitExceededException(
                String.format("Rate limit exceeded. Max %d requests per %d seconds",
                    maxRequests, rateLimited.windowSeconds()));
        }

        timestamps.addLast(Instant.now());
        return joinPoint.proceed();
    }
}

// ─── Kullanım ───
@RestController
public class AuthController {

    @PostMapping("/api/auth/login")
    @RateLimited(maxRequests = 5, windowSeconds = 60, key = "login")
    public TokenResponse login(@Valid @RequestBody LoginRequest request) {
        return authService.authenticate(request);
    }
}

Kullanım 5: Caching

@Around ile basit bir cache mekanizması:

@Aspect
@Component
@Slf4j
public class SimpleCacheAspect {

    private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();

    @Around("@annotation(cached)")
    public Object cacheResult(ProceedingJoinPoint joinPoint,
                                Cached cached) throws Throwable {
        String cacheKey = buildCacheKey(joinPoint);

        // Cache'de var mı ve süresi dolmamış mı?
        CacheEntry entry = cache.get(cacheKey);
        if (entry != null && !entry.isExpired()) {
            log.debug("📦 Cache HIT: {}", cacheKey);
            return entry.value();
        }

        // Cache miss — metodu çalıştır
        log.debug("🔍 Cache MISS: {}", cacheKey);
        Object result = joinPoint.proceed();

        // Sonucu cache'le
        cache.put(cacheKey, new CacheEntry(result,
            Instant.now().plusSeconds(cached.ttlSeconds())));

        return result;
    }

    private String buildCacheKey(ProceedingJoinPoint joinPoint) {
        return joinPoint.getSignature().toShortString() + ":" +
            Arrays.toString(joinPoint.getArgs());
    }

    private record CacheEntry(Object value, Instant expiresAt) {
        boolean isExpired() { return Instant.now().isAfter(expiresAt); }
    }
}

💡 İpucu: Bu basit cache örneği öğretim amaçlıdır. Production'da Spring'in @Cacheable annotation'ını veya Redis gibi harici cache çözümlerini kullanın.


@Around Advice'ta Yaygın Hatalar

1. ❌ proceed() Sonucunu Return Etmemek

// ❌ YANLIŞ — void metot gibi davranıyor
@Around("execution(* com.example.service.*.*(..))")
public void badAdvice(ProceedingJoinPoint jp) throws Throwable {
    jp.proceed();  // Sonuç kayboldu! Caller null alır.
}

// ✅ DOĞRU
@Around("execution(* com.example.service.*.*(..))")
public Object goodAdvice(ProceedingJoinPoint jp) throws Throwable {
    return jp.proceed();
}

2. ❌ Dönüş Tipini Yanlış Belirlemek

// ❌ YANLIŞ — String dönen metot için Object dönemiyor?
// Aslında Object dönmek doğru, ama cast hatası olabilir
@Around("execution(String com.example.service.*.*(..))")
public Object badCast(ProceedingJoinPoint jp) throws Throwable {
    Object result = jp.proceed();
    return result.toString().toUpperCase();  // result null olabilir! → NPE
}

// ✅ DOĞRU — null kontrolü
@Around("execution(String com.example.service.*.*(..))")
public Object safeCast(ProceedingJoinPoint jp) throws Throwable {
    Object result = jp.proceed();
    return result != null ? result.toString().toUpperCase() : null;
}

3. ❌ Exception'ı Yutmak

// ❌ YANLIŞ — exception sessizce yok oldu
@Around("execution(* com.example.service.*.*(..))")
public Object swallowException(ProceedingJoinPoint jp) throws Throwable {
    try {
        return jp.proceed();
    } catch (Exception e) {
        log.error("Error: {}", e.getMessage());
        return null;  // Caller hata yerine null alır → daha kötü hatalar
    }
}

// ✅ DOĞRU — logla ve tekrar fırlat
@Around("execution(* com.example.service.*.*(..))")
public Object logAndRethrow(ProceedingJoinPoint jp) throws Throwable {
    try {
        return jp.proceed();
    } catch (Exception e) {
        log.error("Error in {}: {}", jp.getSignature().toShortString(), e.getMessage());
        throw e;  // Exception'ı tekrar fırlat
    }
}

4. ❌ Çok Geniş Pointcut

// ❌ YANLIŞ — TÜM metotları izliyor (toString, hashCode dahil!)
@Around("execution(* *.*(..))")
public Object tooWide(ProceedingJoinPoint jp) throws Throwable { ... }

// ❌ YANLIŞ — Repository metotları dahil (N+1 sorunu tetikleyebilir)
@Around("execution(* com.example..*.*(..))")
public Object stillTooWide(ProceedingJoinPoint jp) throws Throwable { ... }

// ✅ DOĞRU — sadece service katmanı
@Around("execution(* com.example.service.*.*(..))")
public Object focused(ProceedingJoinPoint jp) throws Throwable { ... }

// ✅ EN İYİ — annotation-based, tam kontrol
@Around("@annotation(monitored)")
public Object precise(ProceedingJoinPoint jp, Monitored monitored) throws Throwable { ... }

5. ❌ Thread Safety Unutmak

// ❌ YANLIŞ — Aspect singleton'dır, field paylaşılır
@Aspect
@Component
public class BrokenAspect {
    private long startTime;  // ❌ Thread-unsafe!

    @Around("...")
    public Object measure(ProceedingJoinPoint jp) throws Throwable {
        startTime = System.nanoTime();  // Thread A yazarken Thread B okur!
        Object result = jp.proceed();
        long elapsed = System.nanoTime() - startTime;  // Yanlış Thread'in değeri
        return result;
    }
}

// ✅ DOĞRU — local variable kullan
@Aspect
@Component
public class SafeAspect {

    @Around("...")
    public Object measure(ProceedingJoinPoint jp) throws Throwable {
        long startTime = System.nanoTime();  // ✅ Her thread kendi değişkenine sahip
        Object result = jp.proceed();
        long elapsed = System.nanoTime() - startTime;
        return result;
    }
}

Performance Impact — AOP Ne Kadar Yavaşlatır?

AOP advice'ların bir performance maliyeti vardır. Ancak bu maliyet genellikle ihmal edilebilir düzeydedir:

Advice TürüEk Maliyet (yaklaşık)Açıklama
@Before / @After~0.01msNeredeyse sıfır
@Around (basit)~0.02msproceed() çağrısı
@Around (logging)~0.1msString concatenation + log
@Around (metrics)~0.5msMicrometer kayıt

⚠️ Dikkat: Asıl maliyet AOP'nin kendisinde değil, advice'ın içinde yaptığınız işlemdedir. Advice'ta veritabanı sorgusu, HTTP çağrısı veya dosya okuma yapıyorsanız, bu maliyet çok daha yüksektir.

// ❌ Advice'ta pahalı işlem — YAPMAYIN
@Around("execution(* com.example.service.*.*(..))")
public Object expensiveAdvice(ProceedingJoinPoint jp) throws Throwable {
    // Her metot çağrısında veritabanına yazıyor — ÇOK YAVAŞ!
    auditRepository.save(new AuditLog(jp.getSignature().toString()));
    return jp.proceed();
}

// ✅ Async audit — non-blocking
@Around("execution(* com.example.service.*.*(..))")
public Object asyncAdvice(ProceedingJoinPoint jp) throws Throwable {
    Object result = jp.proceed();
    // Async olarak kaydet — metodu yavaşlatmaz
    CompletableFuture.runAsync(() ->
        auditRepository.save(new AuditLog(jp.getSignature().toString())));
    return result;
}

Gerçek Dünya Örneği: API Request/Response Logger

Tüm API çağrılarını yapılandırılmış şekilde loglayan bütünleşik bir aspect:

@Aspect
@Component
@Slf4j
public class ApiLoggingAspect {

    private final ObjectMapper objectMapper;

    public ApiLoggingAspect(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Around("execution(* com.example.controller.*Controller.*(..))")
    public Object logApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        String controller = joinPoint.getTarget().getClass().getSimpleName();
        String method = joinPoint.getSignature().getName();
        String requestId = UUID.randomUUID().toString().substring(0, 8);

        MDC.put("requestId", requestId);
        MDC.put("controller", controller);
        MDC.put("method", method);

        log.info("→ API Request: {}.{}() args={}",
            controller, method, summarize(joinPoint.getArgs()));

        long start = System.nanoTime();

        try {
            Object result = joinPoint.proceed();
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

            log.info("← API Response: {}.{}() status=OK duration={}ms",
                controller, method, elapsedMs);

            return result;

        } catch (Throwable ex) {
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

            log.error("← API Error: {}.{}() status=ERROR duration={}ms error={}",
                controller, method, elapsedMs, ex.getMessage());

            throw ex;

        } finally {
            MDC.clear();
        }
    }

    private String summarize(Object[] args) {
        if (args == null || args.length == 0) return "[]";
        try {
            return objectMapper.writeValueAsString(args);
        } catch (Exception e) {
            return Arrays.toString(args);
        }
    }
}

Özet

  • `@Around` en güçlü advice türüdür — metodu tamamen sarar: önce, sonra, hata durumu ve hatta metodu çağırmama seçeneği

  • `ProceedingJoinPoint.proceed()` ile hedef metot çalıştırılır — sonucu mutlaka return edin

  • Performance monitoring: System.nanoTime() ile süre ölçümü, eşik seviyelerine göre uyarı

  • Retry mekanizması: Geçici hatalar için exponential backoff ile yeniden deneme

  • Rate limiting: Metot çağrı sıklığını sınırlama

  • Thread safety: Aspect singleton'dır — instance variable kullanmayın, local variable kullanın

  • Principle of least power: Sadece loglama gerekiyorsa @Before yeterli, @Around kullanmayın

  • Advice'ta pahalı işlem yapmayın — loglama kısa tutun, ağır işleri async yapın