Eğitim Portalı/Spring/SpEL, Olaylar (Events) ve Profiller
Spring02-spring/05-spel-events-profiles

SpEL, Olaylar (Events) ve Profiller

Spring çekirdeğinin son durağında, container'ı daha esnek ve güçlü kılan üç yardımcı yeteneği işliyoruz: ifadeleri çalışma anında değerlendiren SpEL, bileşenleri gevşek bağlayan olay mekanizması ve uygulamayı ortama g…

SpEL, Olaylar (Events) ve Profiller

Spring çekirdeğinin son durağında, container'ı daha esnek ve güçlü kılan üç yardımcı yeteneği işliyoruz: ifadeleri çalışma anında değerlendiren SpEL, bileşenleri gevşek bağlayan olay mekanizması ve uygulamayı ortama göre yapılandıran profiller. Bunların üçü de, Spring'le yazdığın gerçek uygulamalarda sık sık karşına çıkacak.

SpEL (Spring Expression Language)

SpEL, metin olarak yazılmış ifadeleri çalışma anında değerlendiren güçlü bir dildir. Onu zaten gizliden gizliye kullandın: @Value("#{...}") bir SpEL ifadesidir. Doğrudan da kullanılabilir:

ExpressionParser parser = new SpelExpressionParser();
parser.parseExpression("2 + 3 * 4").getValue();              // 14
parser.parseExpression("tutar > 1000 ? tutar * 0.1 : 0")     // bir nesne üzerinde
       .getValue(new StandardEvaluationContext(sepet), Double.class);

SpEL; aritmetik, mantıksal, ternary, metot çağrısı, koleksiyon projeksiyonu/filtreleme (.![...], .?[...]) ve nesne özelliklerine erişimi destekler. Örnek 1 (./Ornek1.java) bunu gerçek bir senaryoda kullanır: indirim kurallarını koda gömmek yerine ifade olarak tutar ve bir sepet üzerinde değerlendirir. Böylece kuralları yeniden derlemeden (örneğin bir yapılandırmadan okuyarak) değiştirebilirsin.

SpEL Spring'in birçok yerinde karşına çıkar: @Value, Spring Security ifadeleri (@PreAuthorize("hasRole('ADMIN')")), @Cacheable koşulları, Spring Integration yönlendirme.

Olaylar (Application Events)

Bazen bir şey olduğunda (sipariş oluştu, kullanıcı kaydoldu) birden çok bileşenin tepki vermesi gerekir: e-posta gönder, analitiğe yaz, fatura kes... Bunları doğrudan servise gömmek, servisi o bileşenlere sıkıca bağlar. Spring'in olay mekanizması bu bağı koparır.

Yayıncı (publisher) bir olay nesnesi yayınlar; dinleyiciler (@EventListener) bağımsızca tepki verir. Yayıncı, kimin dinlediğini bilmez:

class SiparisServisi {
    private final ApplicationEventPublisher yayinci;
    void siparisOlustur(...) {
        yayinci.publishEvent(new SiparisOlusturuldu(urun, tutar)); // kime gittiğini bilmez
    }
}
@Component class EpostaDinleyici {
    @EventListener void on(SiparisOlusturuldu e) { /* e-posta gönder */ }
}

Örnek 2 (./Ornek2.java) bir sipariş olayına e-posta ve analitik bileşenlerinin nasıl bağımsızca tepki verdiğini gösterir. Yeni bir tepki eklemek için servisi değil, yalnızca yeni bir dinleyici eklersin. Bu, daha önce gördüğümüz Observer deseninin Spring'e gömülü, hazır hâlidir.

İhtiyaç hâlinde @EventListener + @Async ile olay dinleyicileri asenkron (ayrı thread'de) de çalıştırılabilir; ayrıca @TransactionalEventListener ile olay, transaction commit'ine bağlanabilir.

Profiller (@Profile)

Aynı uygulama farklı ortamlarda (geliştirme, test, üretim) farklı davranmalıdır: geliştirmede sahte/bellek-içi bir ödeme servisi yeterken, üretimde gerçek banka entegrasyonu gerekir. Profiller bunu yönetir:

@Component @Profile("dev")  class SahteOdeme  implements OdemeServisi { ... }
@Component @Profile("prod") class GercekOdeme implements OdemeServisi { ... }

Aktif profil belirlendiğinde Spring yalnızca ona ait bean'leri oluşturur; geri kalanlar hiç yüklenmez. Aktif profil genelde spring.profiles.active=prod (properties), ortam değişkeni veya JVM parametresiyle verilir. Örnek 3 (./Ornek3.java) dev ve prod profillerinde aynı kodun nasıl farklı bir OdemeServisi aldığını gösterir.

Profiller; ortama özel veritabanı yapılandırması, sahte/gerçek entegrasyonlar ve test izolasyonu için günlük olarak kullanılır. Spring Boot'ta application-dev.yml / application-prod.yml gibi profil-bazlı yapılandırma dosyaları da vardır (Boot bölümünde).

Diğer çekirdek yetenekler

Bu bölümdeki üçü dışında, Spring çekirdeğinin günlük kullanımda işine yarayacak başka araçları da vardır: bean tanımlarını veya bean'leri özelleştirmek için BeanFactoryPostProcessor / BeanPostProcessor, kaynak (resource) yükleme soyutlaması (Resource, ResourceLoader) ve property kaynakları (@PropertySource). Bunları ihtiyaç doğdukça keşfedeceksin.

Özet

SpEL ile ifadeleri çalışma anında değerlendirmeyi (ve kuralları koddan ayırmayı), olay mekanizmasıyla bileşenleri gevşek bağlamayı ve profillerle ortama göre yapılandırmayı öğrendik. Bununla Spring çekirdeğini — IoC/DI, bean yaşam döngüsü ve scope, yapılandırma ve stereotype'lar, AOP ve bu yardımcı yetenekleri — tamamlamış olduk. Bu sağlam temelin üzerine, sıradaki adımlarda veri erişimi, transaction, validation ve güvenlik gibi Spring'in uygulama katmanlarını ekleyeceğiz; ardından Spring Boot ile hepsini üretim hızında bir araya getireceğiz.

Kod Örnekleri(3)

Ornek1

ortam gerekir
Ornek1.java
1// Ornek1: SpEL (Spring Expression Language) — metin olarak ifadeleri çalışma anında değerlendir.
2// Gerçek senaryo: indirim kurallarını koda gömmek yerine, ifade olarak tanımlayıp uygulamak.
3// Çalıştırma: portal Spring classpath'iyle çalıştırır.
4import org.springframework.expression.ExpressionParser;
5import org.springframework.expression.spel.standard.SpelExpressionParser;
6import org.springframework.expression.spel.support.StandardEvaluationContext;
7
8public class Ornek1 {
9
10    record Sepet(double tutar, int urunAdedi, boolean uyeMi) {}
11
12    public static void main(String[] args) {
13        ExpressionParser parser = new SpelExpressionParser();
14
15        // --- Basit ifadeler ---
16        System.out.println("2 + 3 * 4      = " + parser.parseExpression("2 + 3 * 4").getValue());
17        System.out.println("'merhaba'.length() = " + parser.parseExpression("'merhaba'.length()").getValue());
18        System.out.println("{1,2,3,4}.![#this*#this] = "
19                + parser.parseExpression("{1,2,3,4}.![#this * #this]").getValue()); // koleksiyon projeksiyonu
20
21        // --- Bir nesne üzerinde ifade değerlendirme (gerçek senaryo) ---
22        // İndirim kuralları ARTIK koda gömülü değil; metin/ifade olarak tutuluyor.
23        // (Bunlar bir yapılandırma dosyasından/veritabanından da gelebilirdi.)
24        String[] kurallar = {
25                "tutar > 1000 ? tutar * 0.10 : 0",          // 1000 üstüne %10
26                "uyeMi ? tutar * 0.05 : 0",                  // üyeye ek %5
27                "urunAdedi >= 5 ? 50 : 0"                    // 5+ ürüne 50 TL
28        };
29
30        Sepet sepet = new Sepet(1500, 6, true);
31        var ctx = new StandardEvaluationContext(sepet); // kök nesne = sepet
32        System.out.println("\nSepet: " + sepet);
33
34        double toplamIndirim = 0;
35        for (String kural : kurallar) {
36            double indirim = parser.parseExpression(kural).getValue(ctx, Double.class);
37            System.out.printf("  kural [%s] -> %.2f TL indirim%n", kural, indirim);
38            toplamIndirim += indirim;
39        }
40        System.out.printf("%nToplam indirim: %.2f TL -> ödenecek: %.2f TL%n",
41                toplamIndirim, sepet.tutar() - toplamIndirim);
42
43        System.out.println("""
44
45                --- SpEL nerede kullanılır? ---
46                @Value("#{...}"), @PreAuthorize("hasRole('ADMIN')"), Spring Security ifadeleri,
47                @Cacheable koşulları, Spring Integration yönlendirmeleri... hepsi SpEL'dir.
48                Kuralları koddan ayırıp metin olarak tutmak, onları yeniden derlemeden değiştirmeyi sağlar.""");
49    }
50}
canlı çalıştırma için ortam gerekir

Bu örnek Spring / Spring Boot (Gradle) ortamı gerektirir; tek dosya olarak java Ornek1.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.

Ornek2

ortam gerekir
Ornek2.java
1// Ornek2: Olay (event) mekanizması — bileşenleri gevşek bağlamak.
2// Gerçek senaryo: sipariş oluşunca e-posta VE analitik, birbirinden habersiz tepki versin.
3// Çalıştırma: portal Spring classpath'iyle çalıştırır.
4import org.springframework.context.ApplicationEventPublisher;
5import org.springframework.context.annotation.AnnotationConfigApplicationContext;
6import org.springframework.context.event.EventListener;
7import org.springframework.stereotype.Component;
8import org.springframework.stereotype.Service;
9
10public class Ornek2 {
11
12    // Olay nesnesi: "ne oldu" bilgisini taşır (basit bir record yeterli).
13    record SiparisOlusturuldu(String urun, double tutar) {}
14
15    public static void main(String[] args) {
16        var ctx = new AnnotationConfigApplicationContext();
17        ctx.register(SiparisServisi.class, EpostaDinleyici.class, AnalitikDinleyici.class);
18        ctx.refresh();
19
20        // Servis siparişi oluşturur ve bir OLAY yayınlar; kime gittiğini BİLMEZ.
21        ctx.getBean(SiparisServisi.class).siparisOlustur("Klavye", 450);
22
23        ctx.close();
24
25        System.out.println("""
26
27                --- Olay mekanizması ne kazandırır? ---
28                SiparisServisi, e-posta veya analitik bileşenlerini HİÇ tanımıyor; sadece olay yayınlıyor.
29                Yeni bir tepki (ör. SMS, fatura) eklemek için SERVİSİ değiştirmene gerek yok;
30                sadece yeni bir @EventListener eklersin. Bu, Observer deseninin Spring'e gömülü hâlidir.
31                (@EventListener ayrıca @Async ile asenkron da çalıştırılabilir.)""");
32    }
33}
34
35@Service
36class SiparisServisi {
37    private final ApplicationEventPublisher yayinci;
38    // Spring, olay yayınlamak için ApplicationEventPublisher'ı enjekte eder.
39    SiparisServisi(ApplicationEventPublisher yayinci) { this.yayinci = yayinci; }
40
41    void siparisOlustur(String urun, double tutar) {
42        System.out.println("[Servis] sipariş kaydedildi: " + urun);
43        // Olayı yayınla — dinleyenler kendileri tepki verir.
44        yayinci.publishEvent(new Ornek2.SiparisOlusturuldu(urun, tutar));
45    }
46}
47
48@Component
49class EpostaDinleyici {
50    // Bu tipte bir olay yayınlanınca Spring bu metodu otomatik çağırır.
51    @EventListener
52    void siparisOlunca(Ornek2.SiparisOlusturuldu olay) {
53        System.out.println("[E-posta] '" + olay.urun() + "' siparişi için onay e-postası gönderildi.");
54    }
55}
56
57@Component
58class AnalitikDinleyici {
59    @EventListener
60    void siparisOlunca(Ornek2.SiparisOlusturuldu olay) {
61        System.out.println("[Analitik] ciroya " + olay.tutar() + " TL eklendi.");
62    }
63}
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: Profiller (@Profile) — ortama göre farklı bean'ler.
2// Gerçek senaryo: dev ortamında sahte/bellek-içi servis, prod ortamında gerçek servis.
3// Çalıştırma: portal Spring classpath'iyle çalıştırır.
4import org.springframework.context.annotation.AnnotationConfigApplicationContext;
5import org.springframework.context.annotation.Profile;
6import org.springframework.stereotype.Component;
7
8public class Ornek3 {
9
10    public static void main(String[] args) {
11        // Hangi ortamdayız? Aktif profili genelde application.properties / ortam değişkeni belirler.
12        System.out.println("=== Profil: dev ===");
13        calistir("dev");
14
15        System.out.println("\n=== Profil: prod ===");
16        calistir("prod");
17
18        System.out.println("""
19
20                --- Profiller ne işe yarar? ---
21                @Profile("dev"/"prod"/"test") ile bean'ler ortama göre seçilir.
22                Aktif profil: spring.profiles.active=prod (properties) veya ortam değişkeni ile belirlenir.
23                Tipik kullanım: dev'de sahte/bellek-içi bağımlılık, prod'da gerçek servis;
24                test profilinde ise test'e özel yapılandırma.""");
25    }
26
27    static void calistir(String profil) {
28        var ctx = new AnnotationConfigApplicationContext();
29        ctx.getEnvironment().setActiveProfiles(profil); // aktif profili seç (refresh'ten ÖNCE)
30        ctx.register(SahteOdeme.class, GercekOdeme.class, OdemeKullanici.class);
31        ctx.refresh();
32
33        // Aktif profile göre Spring DOĞRU OdemeServisi bean'ini enjekte eder.
34        ctx.getBean(OdemeKullanici.class).ode(199.90);
35        ctx.close();
36    }
37}
38
39interface OdemeServisi { void ode(double tutar); }
40
41@Component
42@Profile("dev") // yalnızca "dev" profili aktifken bu bean oluşturulur
43class SahteOdeme implements OdemeServisi {
44    public void ode(double tutar) { System.out.println("  [SAHTE] " + tutar + " TL ödendi (gerçek tahsilat yok)."); }
45}
46
47@Component
48@Profile("prod") // yalnızca "prod" profilinde
49class GercekOdeme implements OdemeServisi {
50    public void ode(double tutar) { System.out.println("  [GERÇEK] " + tutar + " TL banka üzerinden tahsil edildi."); }
51}
52
53@Component
54class OdemeKullanici {
55    private final OdemeServisi odeme; // aktif profile göre doğru uygulama enjekte edilir
56    OdemeKullanici(OdemeServisi odeme) { this.odeme = odeme; }
57    void ode(double tutar) { odeme.ode(tutar); }
58}
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.