Custom Annotation ile AOP
Bir trafik levhası düşünün. "Azami Hız 50 km/h" levhası yolun başına konulur ve o yolda geçen tüm araçlara uygulanır. Levhanın kendisi hiçbir şey yapmaz — sadece bir işarettir. Gerçek kontrolü trafik radarı yapar. Ama radarın neyi kontrol edeceğini levha belirler.
Spring AOP'deki custom annotation'lar tam olarak bu trafik levhasıdır. Annotation kendisi hiçbir mantık içermez — sadece "bu metoda şu davranışı uygula" diye işaret koyar. Gerçek mantığı bir Aspect sınıfı (radar) yürütür.
Bu yaklaşım, AOP'nin en temiz ve declarative kullanımıdır:
execution(* com.example.service.*.*(..))gibi kırılgan pattern'ler yerine@Loggable,@Timed,@Auditablegibi anlamlı annotation'lar kullanırsınız
Neden Custom Annotation?
execution() Pointcut'ın Sorunları
// ❌ Paket yolu değişirse kırılır
@Around("execution(* com.example.service.*.*(..))")
public Object logServiceCalls(ProceedingJoinPoint jp) throws Throwable { ... }
// ❌ Yeni bir service paketi eklenirse güncellenmesi gerekir
@Around("execution(* com.example.service..*.*(..)) || " +
"execution(* com.example.payment.service..*.*(..))")
public Object logAllServices(ProceedingJoinPoint jp) throws Throwable { ... }Sorunlar:
Kırılgan: Paket yapısı değişirse pointcut çalışmaz
Belirsiz: Pointcut'a bakarak hangi metotların etkilendiğini anlamak zor
Ya hep ya hiç: Bir paketteki TÜM metotlara uygulanır — belirli metotları seçemezsiniz
Okunabilirlik: Uzun regex-like ifadeler kodu karmaşıklaştırır
@annotation() Pointcut'ın Avantajları
// ✅ Annotation ile — net, açık, kırılmaz
@Around("@annotation(loggable)")
public Object logAnnotated(ProceedingJoinPoint jp, Loggable loggable) throws Throwable { ... }Avantajlar:
Açık niyet (Explicit intent): Metotun üzerinde annotation'ı gören herkes davranışı anlar
Seçici: Sadece istediğiniz metotlara uygularsınız
Refactoring-safe: Paket yapısı değişse bile annotation yerinde kalır
Parametrik: Annotation'a parametreler ekleyerek davranışı özelleştirebilirsiniz
IDE desteği: "Find Usages" ile annotation'ın nerelerde kullanıldığını görürsünüz
Custom Annotation Oluşturma — Temel Bilgiler
Bir annotation tanımlamak için @interface keyword'ü kullanılır:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}Bu tanımda üç meta-annotation kritik öneme sahiptir:
@Target — Nereye Konulabilir?
@Target, annotation'ın nereye uygulanabileceğini belirler:
@Target(ElementType.METHOD) // Sadece metotlara
@Target(ElementType.TYPE) // Sadece sınıf/interface'e
@Target(ElementType.FIELD) // Sadece alanlara
@Target(ElementType.PARAMETER) // Sadece parametrelere
@Target({ElementType.METHOD, ElementType.TYPE}) // Hem metot hem sınıfAOP için en yaygın kullanım:
METHOD— Belirli metotlara aspect uygulamaTYPE— Sınıftaki tüm metotlara aspect uygulamaMETHOD, TYPE— Her iki kullanım da mümkün
@Retention — Ne Kadar Süre Yaşar?
@Retention, annotation'ın hangi aşamada erişilebilir olduğunu belirler:
@Retention(RetentionPolicy.SOURCE) // Derleme sırasında silinir (Lombok gibi)
@Retention(RetentionPolicy.CLASS) // .class dosyasında var, runtime'da erişilemez
@Retention(RetentionPolicy.RUNTIME) // Runtime'da erişilebilir (AOP İÇİN ZORUNLU)⚠️ Dikkat: AOP annotation'ları için `RUNTIME` zorunludur. Spring, runtime'da reflection ile annotation'ları okur. SOURCE veya CLASS kullanırsanız AOP çalışmaz!
@Documented
Annotation'ın Javadoc'a dahil edilmesini sağlar. Opsiyoneldir ama best practice'tir.
Örnek 1: @Loggable — Metot Loglama
En temel custom annotation: metot giriş-çıkışını loglar.
// ─── Annotation Tanımı ───
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Loggable {
/**
* Log mesajında gösterilecek etiket.
* Boş bırakılırsa metot adı kullanılır.
*/
String value() default "";
/**
* Log seviyesi. Varsayılan INFO.
*/
LogLevel level() default LogLevel.INFO;
/**
* Parametreleri logla?
* Hassas veriler (şifre gibi) için false yapın.
*/
boolean logArgs() default true;
/**
* Dönüş değerini logla?
* Büyük listeler için false yapın.
*/
boolean logResult() default true;
enum LogLevel { TRACE, DEBUG, INFO, WARN }
}
// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class LoggableAspect {
@Around("@annotation(loggable)")
public Object logMethod(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
String label = loggable.value().isEmpty()
? joinPoint.getSignature().toShortString()
: loggable.value();
// Giriş logu
if (loggable.logArgs()) {
logAtLevel(loggable.level(), "→ {} called with args: {}",
label, summarize(joinPoint.getArgs()));
} else {
logAtLevel(loggable.level(), "→ {} called", label);
}
long start = System.nanoTime();
try {
Object result = joinPoint.proceed();
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
// Çıkış logu
if (loggable.logResult()) {
logAtLevel(loggable.level(), "← {} returned: {} ({}ms)",
label, summarize(result), elapsedMs);
} else {
logAtLevel(loggable.level(), "← {} completed ({}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;
}
}
// Sınıf düzeyinde annotation desteği
@Around("@within(loggable)")
public Object logClassMethods(ProceedingJoinPoint joinPoint,
Loggable loggable) throws Throwable {
// Metot düzeyinde annotation varsa onu kullan (öncelik)
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Loggable methodAnnotation = method.getAnnotation(Loggable.class);
if (methodAnnotation != null) {
return joinPoint.proceed(); // Metot-level aspect zaten çalışacak
}
return logMethod(joinPoint, loggable);
}
private void logAtLevel(Loggable.LogLevel level, String format, Object... args) {
switch (level) {
case TRACE -> log.trace(format, args);
case DEBUG -> log.debug(format, args);
case INFO -> log.info(format, args);
case WARN -> log.warn(format, args);
}
}
private String summarize(Object obj) {
if (obj == null) return "null";
if (obj instanceof Collection<?> c) return "[" + c.size() + " items]";
if (obj.getClass().isArray()) return "[" + Array.getLength(obj) + " items]";
String str = obj.toString();
return str.length() > 200 ? str.substring(0, 200) + "..." : str;
}
private String summarize(Object[] args) {
if (args == null || args.length == 0) return "[]";
return Arrays.stream(args)
.map(this::summarize)
.collect(Collectors.joining(", ", "[", "]"));
}
}
// ─── Kullanım ───
@Service
public class OrderService {
@Loggable("Create Order")
public OrderResponse createOrder(CreateOrderRequest request) {
// İş mantığı...
}
@Loggable(value = "Process Payment", logArgs = false) // Hassas veri
public PaymentResult processPayment(PaymentDetails details) {
// Kart bilgilerini loglamayız
}
@Loggable(level = Loggable.LogLevel.DEBUG, logResult = false) // Büyük liste
public List<OrderResponse> searchOrders(OrderSearchCriteria criteria) {
// Binlerce kayıt dönebilir, loglamayız
}
}
// Sınıf düzeyinde — tüm metotları logla
@Service
@Loggable(level = Loggable.LogLevel.DEBUG)
public class AuditService {
// Tüm metotlar DEBUG seviyesinde loglanır
}Örnek 2: @Timed — Performance Monitoring
// ─── Annotation ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Timed {
String metricName() default "";
long warnThresholdMs() default 500;
long errorThresholdMs() default 3000;
String[] tags() default {};
}
// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class TimedAspect {
@Around("@annotation(timed)")
public Object measureTime(ProceedingJoinPoint joinPoint, Timed timed) throws Throwable {
String metric = timed.metricName().isEmpty()
? joinPoint.getSignature().getName()
: timed.metricName();
long start = System.nanoTime();
try {
Object result = joinPoint.proceed();
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
if (elapsedMs >= timed.errorThresholdMs()) {
log.error("🔴 CRITICAL: {} took {}ms (threshold: {}ms)",
metric, elapsedMs, timed.errorThresholdMs());
} else if (elapsedMs >= timed.warnThresholdMs()) {
log.warn("🟡 SLOW: {} took {}ms (threshold: {}ms)",
metric, elapsedMs, timed.warnThresholdMs());
} else {
log.debug("🟢 {} completed in {}ms", metric, elapsedMs);
}
return result;
} catch (Throwable ex) {
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
log.error("💥 {} failed after {}ms: {}", metric, elapsedMs, ex.getMessage());
throw ex;
}
}
}
// ─── Kullanım ───
@Service
public class ReportService {
@Timed(metricName = "generate_monthly_report", warnThresholdMs = 2000, errorThresholdMs = 10000)
public Report generateMonthlyReport(int year, int month) {
// Uzun süren işlem...
}
}Örnek 3: @Auditable — Denetim Kaydı
Kritik iş operasyonlarını denetim amaçlı kaydetmek:
// ─── Annotation ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auditable {
String action();
String entity() default "";
}
// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class AuditAspect {
private final AuditLogRepository auditLogRepository;
private final SecurityContextHolder securityContext;
public AuditAspect(AuditLogRepository auditLogRepository) {
this.auditLogRepository = auditLogRepository;
}
@AfterReturning(pointcut = "@annotation(auditable)", returning = "result")
public void audit(JoinPoint joinPoint, Auditable auditable, Object result) {
String username = getCurrentUsername();
String action = auditable.action();
String entity = auditable.entity().isEmpty()
? inferEntityFromMethod(joinPoint)
: auditable.entity();
AuditLog auditLog = AuditLog.builder()
.username(username)
.action(action)
.entity(entity)
.details(buildDetails(joinPoint, result))
.timestamp(Instant.now())
.ipAddress(getClientIp())
.build();
// Async kaydet — iş mantığını yavaşlatma
CompletableFuture.runAsync(() -> auditLogRepository.save(auditLog));
log.info("AUDIT: {} performed '{}' on {}", username, action, entity);
}
private String buildDetails(JoinPoint joinPoint, Object result) {
Map<String, Object> details = new LinkedHashMap<>();
details.put("method", joinPoint.getSignature().toShortString());
details.put("args", Arrays.toString(joinPoint.getArgs()));
if (result != null) {
details.put("resultType", result.getClass().getSimpleName());
}
return details.toString();
}
private String getCurrentUsername() {
var auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : "anonymous";
}
private String getClientIp() {
var requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes sra) {
return sra.getRequest().getRemoteAddr();
}
return "unknown";
}
private String inferEntityFromMethod(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getSimpleName();
return className.replace("Service", "").replace("Controller", "");
}
}
// ─── Kullanım ───
@Service
public class UserService {
@Auditable(action = "CREATE", entity = "User")
public UserResponse createUser(CreateUserRequest request) { ... }
@Auditable(action = "UPDATE", entity = "User")
public UserResponse updateUser(Long id, UpdateUserRequest request) { ... }
@Auditable(action = "DELETE", entity = "User")
public void deleteUser(Long id) { ... }
@Auditable(action = "CHANGE_PASSWORD", entity = "User")
public void changePassword(Long id, ChangePasswordRequest request) { ... }
}Audit log çıktısı:
AUDIT: admin performed 'DELETE' on User
AUDIT: john.doe performed 'CHANGE_PASSWORD' on UserÖrnek 4: @RequireRole — Yetkilendirme
// ─── Annotation ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value(); // Gerekli roller
boolean all() default false; // true = hepsi gerekli, false = herhangi biri yeterli
}
// ─── Aspect ───
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(requireRole)")
public void checkRole(JoinPoint joinPoint, RequireRole requireRole) {
var auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
throw new UnauthorizedException("Kimlik doğrulama gerekli");
}
Set<String> userRoles = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
String[] requiredRoles = requireRole.value();
boolean hasAccess;
if (requireRole.all()) {
// Tüm roller gerekli
hasAccess = userRoles.containsAll(Arrays.asList(requiredRoles));
} else {
// Herhangi bir rol yeterli
hasAccess = Arrays.stream(requiredRoles).anyMatch(userRoles::contains);
}
if (!hasAccess) {
throw new ForbiddenException(
String.format("Gerekli roller: %s. Mevcut: %s",
Arrays.toString(requiredRoles), userRoles));
}
}
}
// ─── Kullanım ───
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/users")
@RequireRole("ROLE_ADMIN")
public List<UserResponse> getAllUsers() { ... }
@DeleteMapping("/users/{id}")
@RequireRole(value = {"ROLE_ADMIN", "ROLE_SUPER_ADMIN"}, all = false)
public void deleteUser(@PathVariable Long id) { ... }
@PostMapping("/system/reset")
@RequireRole(value = {"ROLE_ADMIN", "ROLE_SUPER_ADMIN"}, all = true)
public void resetSystem() { ... }
}Örnek 5: @Cacheable (Basitleştirilmiş)
// ─── Annotation ───
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleCache {
String name();
long ttlSeconds() default 300; // 5 dakika
}
// ─── Aspect ───
@Aspect
@Component
@Slf4j
public class SimpleCacheAspect {
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
@Around("@annotation(simpleCache)")
public Object checkCache(ProceedingJoinPoint joinPoint,
SimpleCache simpleCache) throws Throwable {
String key = simpleCache.name() + ":" + Arrays.toString(joinPoint.getArgs());
CacheEntry entry = cache.get(key);
if (entry != null && entry.isValid()) {
log.debug("Cache HIT: {}", key);
return entry.value();
}
log.debug("Cache MISS: {}", key);
Object result = joinPoint.proceed();
cache.put(key, new CacheEntry(result,
Instant.now().plusSeconds(simpleCache.ttlSeconds())));
return result;
}
private record CacheEntry(Object value, Instant expiresAt) {
boolean isValid() { return Instant.now().isBefore(expiresAt); }
}
}
// ─── Kullanım ───
@Service
public class ProductService {
@SimpleCache(name = "products", ttlSeconds = 600)
public ProductResponse findById(Long id) {
return productRepository.findById(id)
.map(productMapper::toResponse)
.orElseThrow(() -> new ResourceNotFoundException("Product", "id", id));
}
}Annotation Parametrelerine Erişim
Aspect metotlarında annotation parametrelerine erişmenin iki yolu vardır:
Yol 1: Parametre Binding (Önerilen)
// Annotation parametresini doğrudan metot parametresi olarak al
@Around("@annotation(loggable)") // küçük harf = parametre adı
public Object log(ProceedingJoinPoint jp, Loggable loggable) throws Throwable {
String label = loggable.value();
// ...
}Yol 2: Reflection ile Okuma
@Around("@annotation(com.example.annotation.Loggable)") // full qualified name
public Object log(ProceedingJoinPoint jp) throws Throwable {
MethodSignature signature = (MethodSignature) jp.getSignature();
Loggable loggable = signature.getMethod().getAnnotation(Loggable.class);
String label = loggable.value();
// ...
}💡 İpucu: Parametre binding (Yol 1) daha temiz ve type-safe'tir. Reflection (Yol 2) sadece annotation'a dinamik erişim gerektiğinde kullanın.
Birden Fazla Custom Annotation Birleştirme
Bir metoda birden fazla annotation ekleyebilirsiniz:
@Service
public class PaymentService {
@Loggable("Process Payment")
@Timed(metricName = "payment_processing", warnThresholdMs = 1000)
@Auditable(action = "PAYMENT", entity = "Transaction")
@Retryable(maxAttempts = 3, retryOn = TransientException.class)
public PaymentResult processPayment(PaymentRequest request) {
return paymentGateway.charge(request);
}
}Çalışma sırası @Order ile belirlenir:
@Aspect @Component @Order(1) // İlk çalışır — en dışta
public class RetryAspect { }
@Aspect @Component @Order(2)
public class AuditAspect { }
@Aspect @Component @Order(3) // Son çalışır — en içte
public class LoggableAspect { }RetryAspect.before
→ AuditAspect.before
→ LoggableAspect.before
→ paymentService.processPayment() ← Hedef metot
← LoggableAspect.after
← AuditAspect.after
← RetryAspect.afterYaygın Hatalar
1. ❌ @Retention(RUNTIME) Unutmak
// ❌ YANLIŞ — varsayılan CLASS, AOP çalışmaz!
@Target(ElementType.METHOD)
public @interface Loggable { }
// ✅ DOĞRU
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) // ZORUNLU
public @interface Loggable { }2. ❌ Annotation İsmi ile Parametre İsmi Uyuşmaması
// ❌ YANLIŞ — "loggable" parametresi yok
@Around("@annotation(loggable)")
public Object log(ProceedingJoinPoint jp, Loggable myAnnotation) { }
// ↑ parametre adı "myAnnotation"
// ✅ DOĞRU — isimler eşleşmeli
@Around("@annotation(loggable)")
public Object log(ProceedingJoinPoint jp, Loggable loggable) { }
// ↑ "loggable" ile eşleşir3. ❌ Private Metotlara Annotation Koymak
@Service
public class UserService {
@Loggable // ❌ AOP ÇALIŞMAZ — Spring AOP proxy-based, private metotlara ulaşamaz
private User findUserInternal(Long id) { ... }
@Loggable // ✅ ÇALIŞIR — public metot
public UserResponse findById(Long id) { ... }
}4. ❌ Aynı Sınıf İçinden Çağrı (Self-Invocation)
@Service
public class UserService {
@Loggable
public UserResponse findById(Long id) { ... }
public UserResponse getActiveUser(Long id) {
return this.findById(id); // ❌ @Loggable ÇALIŞMAZ!
// this → gerçek nesne, proxy değil
}
}Annotation mi, execution() mı? Karar Rehberi
| Kriter | execution() | @annotation |
|---|---|---|
| Granülarite | Paket/sınıf düzeyinde | Metot düzeyinde |
| Açıklık | Gizli — kodu okuyan anlamayabilir | Açık — metotta görünür |
| Bakım | Paket değişince kırılır | Refactoring-safe |
| Tercih edin | Altyapı (tüm repository, tüm controller) | İş mantığı (belirli metotlar) |
Önerilen yaklaşım:
Altyapı concern'leri (tüm controller'ları loglama):
execution()veyawithin()İş mantığı concern'leri (belirli metotları audit etme):
@annotation()Hibrit: Named pointcut'larla ikisini birleştirin
Özet
Custom annotation + AOP, en temiz ve declarative cross-cutting concern yönetimidir
@Target(METHOD)+@Retention(RUNTIME)+@interfaceile annotation tanımlanır@annotation(paramName)pointcut'ı ile annotation'a sahip metotlar hedeflenirAnnotation parametreleri, advice metotlarında doğrudan erişilebilir (parametre binding)
@Loggable,@Timed,@Auditable,@Retryable— yaygın custom annotation örnekleri`@Retention(RUNTIME)` zorunludur — aksi halde AOP çalışmaz
Private metotlara ve self-invocation'da annotation-based AOP çalışmaz (proxy kısıtlaması)
Birden fazla annotation birleştirilebilir, sıralama
@Orderile belirlenir
AI Asistan
Sorularını yanıtlamaya hazır