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ılabilir1// 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}Ornek2
ortam gerekir1// 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}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 gerekir1// 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}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.