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ış
| Advice | Ne Zaman Çalışır | Dönüş Değerine Erişim | Exception'a Erişim | Metodu Engelleyebilir mi? |
|---|---|---|---|---|
@Before | Metot öncesinde | ❌ | ❌ | Sadece exception fırlatarak |
@After | Metot sonrasında (her durumda) | ❌ | ❌ | ❌ |
@AfterReturning | Metot başarıyla döndükten sonra | ✅ | ❌ | ❌ |
@AfterThrowing | Metot exception fırlattığında | ❌ | ✅ | ❌ |
@Around | Metot ö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ı:
| Metot | Dönüş | Açıklama |
|---|---|---|
getSignature() | Signature | Metot imzası (isim, dönüş tipi, parametre tipleri) |
getArgs() | Object[] | Metot parametreleri |
getTarget() | Object | Gerçek nesne (proxy değil) |
getThis() | Object | Proxy nesnesi |
getSignature().getName() | String | Metot adı |
getSignature().toShortString() | String | Kısa imza: UserService.findById(..) |
getSignature().toLongString() | String | Tam 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)
finallybloğ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önerBirden 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önerBu, 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 (
finallygibi)`@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
@Orderile 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+@Componentikisi birlikte gereklidir
AI Asistan
Sorularını yanıtlamaya hazır