Eğitim Portalı/Spring/Spring AOP (Aspect-Oriented Programming)
Spring02-spring/04-aop

Spring AOP (Aspect-Oriented Programming)

Her ciddi uygulamada, asıl iş mantığının yanında tekrar eden "yan görevler" vardır: loglama, güvenlik kontrolü, transaction yönetimi, performans ölçümü, önbellekleme. Bunlara kesişen ilgiler (cross-cutting concerns) d…

Spring AOP (Aspect-Oriented Programming)

Her ciddi uygulamada, asıl iş mantığının yanında tekrar eden "yan görevler" vardır: loglama, güvenlik kontrolü, transaction yönetimi, performans ölçümü, önbellekleme. Bunlara kesişen ilgiler (cross-cutting concerns) denir; çünkü tek bir yere ait değildirler, uygulamanın her köşesine yayılırlar. AOP (Aspect-Oriented Programming), bu yan görevleri iş mantığından ayırıp tek bir yerde toplamanın yoludur. Spring'in en güçlü ve en çok kullanılan özelliklerinden biridir — @Transactional ve method security bile altta AOP'tur.

Önce sorunu görelim

Örnek 1 (./Ornek1.java) bir SiparisServisi'nin her metoduna loglama ve süre ölçümü kodunun elle serpiştirildiği hâlini gösterir:

void siparisOlustur(String urun) {
    long t0 = System.currentTimeMillis();
    System.out.println("[LOG] başladı");
    // ... asıl iş (tek satır) ...
    System.out.println("[LOG] bitti " + (System.currentTimeMillis() - t0));
}

Bu yaklaşımın dertleri açıktır: aynı kod her metoda kopyalanır (DRY ihlali), asıl iş mantığı log satırlarının arasında kaybolur ve log formatını değiştirmek istediğinde onlarca metoda dokunman gerekir. Loglama, iş mantığının sorumluluğu değildir — ayrılmalıdır.

AOP terimleri

AOP'u konuşmak için ortak bir sözlük gerekir:

  • Aspect: Kesişen ilgiyi içeren modül (ör. LoglamaAspect).
  • Advice: Aspect'in ne zaman çalışacağı. Türleri: @Before, @After, @AfterReturning, @AfterThrowing, @Around.
  • Pointcut: Aspect'in nerede (hangi metotlarda) çalışacağını tanımlayan ifade.
  • JoinPoint: Advice'ın uygulandığı an/nokta (çalışan metot, argümanları vb.).

Pointcut ifadeleri

Pointcut'lar çeşitli biçimlerde yazılır:

@Before("execution(* com.ornek.servis.*.*(..))")  // paket/metot imzasına göre
@Before("bean(siparisServisi)")                    // bean adına göre (Spring'e özgü, pratik)
@Before("@annotation(com.ornek.Loglanabilir)")     // belirli bir anotasyona göre

Çözüm: @Before / @After

Örnek 2 (./Ornek2.java) loglamayı LoglamaAspect adlı bir aspect'e taşır. İş mantığı artık tertemizdir; loglama tek bir yerdedir ve @Before/@After ile ilgili tüm metotlara otomatik uygulanır:

@Aspect
class LoglamaAspect {
    @Before("bean(siparisServisi)")
    void once(JoinPoint jp) { System.out.println("[LOG] başlıyor: " + jp.getSignature().getName()); }
    @After("bean(siparisServisi)")
    void sonra(JoinPoint jp) { System.out.println("[LOG] bitti"); }
}

Spring AOP nasıl çalışır? (Proxy) Spring, aspect uygulanan bean'in etrafına bir proxy (vekil) nesne sarar. Bean'i container'dan aldığında aslında proxy'yi alırsın; bir metot çağırdığında önce proxy araya girer, advice'ı çalıştırır, sonra gerçek metoda geçer. Bu yüzden AOP yalnızca bean üzerinden, dışarıdan yapılan çağrılarda devreye girer; aynı sınıfın kendi içinden (self-invocation) yaptığı çağrılarda devreye girmez — sık karşılaşılan bir tuzaktır.

En güçlüsü: @Around

@Around, metodu tamamen sarar: proceed() çağrısının öncesinde ve sonrasında kod çalıştırabilir, hatta proceed()'i hiç çağırmayarak metodu engelleyebilir. Örnek 3 (./Ornek3.java) tek bir aspect'le hem süre ölçümü yapar hem yetki kontrolü uygular:

@Around("bean(bankaServisi)")
Object denetle(ProceedingJoinPoint pjp) throws Throwable {
    if (!yetkili()) return null;          // proceed() yok -> metot ENGELLENDİ
    long t0 = System.nanoTime();
    Object sonuc = pjp.proceed();         // asıl metodu çalıştır
    log(System.nanoTime() - t0);
    return sonuc;
}

Yetkili kullanıcı transferi yapabilir ve süresi ölçülür; yetkisiz kullanıcının çağrısı ise hiç çalıştırılmadan reddedilir. İş mantığı (BankaServisi) bunların hiçbirini bilmez.

AOP'un gerçek dünyadaki yeri

Spring'in günlük olarak kullandığın birçok özelliği aslında AOP üzerine kuruludur:

  • @Transactional → metodu bir transaction'la sarar (commit/rollback).
  • @Cacheable → metot sonucunu önbelleğe alır, ikinci çağrıda metoda hiç girmez.
  • Method security (@PreAuthorize) → çağrıdan önce yetki kontrolü.
  • @Async → metodu ayrı bir thread'de çalıştırır.

Hepsi @Around mantığıyla, proxy üzerinden çalışır. Bu yüzden AOP'u anlamak, bu özelliklerin neden bazen "beklenmedik" davrandığını (ör. self-invocation tuzağı) anlamanı da sağlar.

Özet

Kesişen ilgilerin yarattığı kod kirliliğini (Örnek 1), bunları @Before/@After ile bir aspect'e taşımayı (Örnek 2) ve @Around ile metodu sarıp ölçme/engelleme gücünü (Örnek 3) gördük; Spring AOP'un proxy temelli çalıştığını ve self-invocation tuzağını öğrendik. Bu, hem kendi kesişen ilgilerini temizlemeni hem de Spring'in @Transactional/@Cacheable gibi özelliklerini bilinçli kullanmanı sağlar. Sırada, Spring çekirdeğinin son durağı: SpEL, olay mekanizması (events) ve profiller.

Kod Örnekleri(3)

Ornek1

çalıştırılabilir
Ornek1.java
1// Ornek1: PROBLEM — kesişen ilgiler (cross-cutting concerns) her metoda elle serpiştirilmiş.
2// Loglama ve süre ölçümü, asıl iş mantığının arasına karışmış; her metotta TEKRARLANIYOR.
3// Çalıştırma: java Ornek1.java
4public class Ornek1 {
5
6    public static void main(String[] args) {
7        var servis = new SiparisServisi();
8        servis.siparisOlustur("Klavye");
9        servis.siparisIptal(42);
10
11        System.out.println("""
12
13                --- Sorun ---
14                Loglama ve süre ölçümü kodu HER metoda elle yazıldı:
15                  - aynı kod tekrar tekrar kopyalandı (DRY ihlali),
16                  - asıl iş mantığı, log satırlarının arasında kayboldu,
17                  - bir gün log formatını değiştirmek istesen, HER metoda dokunman gerekir.
18                Loglama, güvenlik, transaction gibi 'kesişen ilgiler' iş mantığından AYRILMALI -> AOP (Örnek 2).""");
19    }
20}
21
22class SiparisServisi {
23
24    void siparisOlustur(String urun) {
25        long t0 = System.currentTimeMillis();           // <-- ölçüm (kesişen ilgi)
26        System.out.println("[LOG] siparisOlustur basladi: " + urun); // <-- loglama (kesişen ilgi)
27
28        // ----- ASIL İŞ MANTIĞI (tek satır) -----
29        System.out.println("   -> sipariş kaydedildi: " + urun);
30        // ---------------------------------------
31
32        System.out.println("[LOG] siparisOlustur bitti (" + (System.currentTimeMillis() - t0) + " ms)");
33    }
34
35    void siparisIptal(int id) {
36        long t0 = System.currentTimeMillis();           // <-- yine ölçüm
37        System.out.println("[LOG] siparisIptal basladi: #" + id); // <-- yine loglama
38
39        // ----- ASIL İŞ MANTIĞI -----
40        System.out.println("   -> sipariş iptal edildi: #" + id);
41        // ---------------------------
42
43        System.out.println("[LOG] siparisIptal bitti (" + (System.currentTimeMillis() - t0) + " ms)");
44    }
45}
Çıktı yerel JDK 21 ile yakalandı — tarayıcıda JVM çalışmaz.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: ÇÖZÜM — AOP ile loglamayı bir Aspect'e taşımak (@Before / @After).
2// İş mantığı artık TERTEMİZ; loglama tek bir yerde, otomatik uygulanıyor.
3// Çalıştırma: portal bu dosyayı derleyip Spring classpath'iyle çalıştırır.
4package com.egitim.spring.aop;
5
6import org.aspectj.lang.JoinPoint;
7import org.aspectj.lang.annotation.After;
8import org.aspectj.lang.annotation.Aspect;
9import org.aspectj.lang.annotation.Before;
10import org.springframework.context.annotation.AnnotationConfigApplicationContext;
11import org.springframework.context.annotation.Bean;
12import org.springframework.context.annotation.Configuration;
13import org.springframework.context.annotation.EnableAspectJAutoProxy;
14
15public class Ornek2 {
16
17    // İş mantığı TERTEMİZ — hiçbir log/ölçüm kodu yok.
18    // public static: Spring AOP'un proxy (CGLIB) üretebilmesi için sınıf erişilebilir olmalı.
19    public static class SiparisServisi {
20        public void siparisOlustur(String urun) { System.out.println("   -> sipariş kaydedildi: " + urun); }
21        public void siparisIptal(int id) { System.out.println("   -> sipariş iptal edildi: #" + id); }
22    }
23
24    // Kesişen ilgi (loglama) tek bir Aspect'te toplandı.
25    @Aspect
26    public static class LoglamaAspect {
27        // bean(...) pointcut'ı: "siparisServisi" bean'inin tüm metotları (paket adından bağımsız).
28        @Before("bean(siparisServisi)")
29        void once(JoinPoint jp) {
30            System.out.println("[LOG] başlıyor: " + jp.getSignature().getName()
31                    + " argümanlar=" + java.util.Arrays.toString(jp.getArgs()));
32        }
33
34        @After("bean(siparisServisi)")
35        void sonra(JoinPoint jp) {
36            System.out.println("[LOG] bitti  : " + jp.getSignature().getName());
37        }
38    }
39
40    @Configuration
41    @EnableAspectJAutoProxy
42    public static class AopConfig {
43        @Bean SiparisServisi siparisServisi() { return new SiparisServisi(); }
44        @Bean LoglamaAspect loglamaAspect() { return new LoglamaAspect(); }
45    }
46
47    public static void main(String[] args) {
48        var ctx = new AnnotationConfigApplicationContext(AopConfig.class);
49
50        // Çağrı proxy üzerinden geçtiği için aspect otomatik devreye girer.
51        SiparisServisi servis = ctx.getBean(SiparisServisi.class);
52        servis.siparisOlustur("Klavye");
53        servis.siparisIptal(42);
54
55        ctx.close();
56
57        System.out.println("""
58
59                --- AOP terimleri ---
60                Aspect    : kesişen ilgiyi içeren modül (LoglamaAspect).
61                Advice    : ne zaman çalışacağı (@Before, @After, @Around...).
62                Pointcut  : nerede çalışacağı (hangi metotlar).
63                JoinPoint : advice'ın uygulandığı an/nokta (çağrılan metot).
64                Sonuç: iş mantığı loglama kodundan tamamen arındı; loglama TEK yerde.""");
65    }
66}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek2.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.

Ornek3

ortam gerekir
Ornek3.java
1// Ornek3: @Around advice — metodu SARAR; öncesini/sonrasını kontrol eder, hatta engelleyebilir.
2// Gerçek senaryo: süre ölçümü + yetki (authorization) kontrolü, tek bir aspect'te.
3// Çalıştırma: portal bu dosyayı derleyip Spring classpath'iyle çalıştırır.
4package com.egitim.spring.aop;
5
6import org.aspectj.lang.ProceedingJoinPoint;
7import org.aspectj.lang.annotation.Around;
8import org.aspectj.lang.annotation.Aspect;
9import org.springframework.context.annotation.AnnotationConfigApplicationContext;
10import org.springframework.context.annotation.Bean;
11import org.springframework.context.annotation.Configuration;
12import org.springframework.context.annotation.EnableAspectJAutoProxy;
13
14public class Ornek3 {
15
16    public static void main(String[] args) {
17        var ctx = new AnnotationConfigApplicationContext(Config.class);
18        BankaServisi banka = ctx.getBean(BankaServisi.class);
19
20        System.out.println("--- Yetkili kullanıcı ---");
21        Guvenlik.giris("admin");
22        banka.paraTransfer(500);
23
24        System.out.println("\n--- Yetkisiz kullanıcı ---");
25        Guvenlik.giris("misafir");
26        banka.paraTransfer(1000); // @Around bunu ENGELLER
27
28        ctx.close();
29
30        System.out.println("""
31
32                --- @Around neden güçlü? ---
33                @Around, metodu SARAR: proceed() ÇAĞIRMADAN önce/sonra kod çalıştırır,
34                hatta proceed()'i hiç çağırmayarak metodu ENGELLEYEBİLİR.
35                Tek bir aspect'le hem süre ölçümü hem yetki denetimi yaptık; iş mantığı tertemiz kaldı.
36                Spring'de @Transactional, @Cacheable, method security — hepsi @Around mantığıyla çalışır.""");
37    }
38
39    @Configuration
40    @EnableAspectJAutoProxy
41    public static class Config {
42        @Bean BankaServisi bankaServisi() { return new BankaServisi(); }
43        @Bean DenetimAspect denetimAspect() { return new DenetimAspect(); }
44    }
45
46    // İş mantığı tertemiz: ne süre ölçümü ne yetki kontrolü içerir.
47    public static class BankaServisi {
48        public void paraTransfer(int tutar) {
49            System.out.println("   -> " + tutar + " TL transfer edildi.");
50        }
51    }
52
53    // Basit bir "oturum" — gerçekte SecurityContext olurdu.
54    public static class Guvenlik {
55        static String kullanici = "anonim";
56        static void giris(String k) { kullanici = k; System.out.println("(giriş: " + k + ")"); }
57    }
58
59    @Aspect
60    public static class DenetimAspect {
61        @Around("bean(bankaServisi)")
62        Object denetle(ProceedingJoinPoint pjp) throws Throwable {
63            // 1) ÖNCE: yetki kontrolü — yetkisizse metodu hiç çalıştırma.
64            if (!"admin".equals(Guvenlik.kullanici)) {
65                System.out.println("[GÜVENLİK] erişim reddedildi (kullanıcı: " + Guvenlik.kullanici + ")");
66                return null; // proceed() çağrılmaz -> metot ENGELLENDİ
67            }
68            // 2) Süre ölç ve metodu çalıştır.
69            long t0 = System.nanoTime();
70            Object sonuc = pjp.proceed(); // asıl metodu çağır
71            long ms = (System.nanoTime() - t0) / 1_000_000;
72            System.out.println("[ÖLÇÜM] " + pjp.getSignature().getName() + " " + ms + " ms sürdü.");
73            return sonuc;
74        }
75    }
76}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek3.java ile çalışmaz. Orijinal portal bunu gömülü Tomcat / Spring context ile koşuyordu. Beklenen davranış yukarıdaki anlatım ve kodda açıklanmıştır.