Spring Context Bean'lerine Erişim ve Injection - (Wiring Beans)
22 min read

Spring Context Bean'lerine Erişim ve Injection - (Wiring Beans)

Spring Context içindeki bean'lere erişebilmek ve bean'leri birbirine enjekte ederek (injection) nasıl bağımlılıkları yönetebileceğimizi anlatıyoruz.
Spring Context Bean'lerine Erişim ve Injection - (Wiring Beans)
Photo by Simona Sroková / Unsplash

Spring Context'e eklediğimiz bean'lere erişebilmek ve onları diğer bean'lere enjecte etmek (injection) için kullanabileceğimiz beş farklı metod bulunuyor. Tüm bu metodları örnekleriyle beraber detaylıca inceliyoruz.

Bir önceki "Spring Context Nedir? Bean Nasıl Oluşturulur ve Kullanılır?" başlıklı yazımızda Spring Context'in ve bean'lerin nasıl oluşturulacağını ve context'e bu bean'leri nasıl ekleyebileceğimizi öğrenmiştik. Ayrıca Context içindeki bean'lere de getBean() metodu ile direkt erişmiştik. Ancak uygulamalarımızda, ihtiyaç duyduğumuz yerde Spring'e context'tinden bir instance vermesini basitçe söyleyebiliyor olmamız gerekir. Çünkü bir bean'in içinden başka bir bean'e erişip onun bir referansını isteyeceğimiz birçok durumla karşılacağız. Bunu yapabilmek için de bu yazımızda birkaç metod öğreneceğiz.

Spring context'e bean eklemek için @Bean anotasyonunu kullanmayı öğrenmiştik. Artık @Bean anotasyonu kullanarak konfigürasyon sınıfında tanımlayacağınız iki bean arasında bir ilişki kurmaya başlıyoruz. Burada bean'ler arasındaki ilişkileri kurmanın ilk iki yolunu tartışacağız:

  • Bean'leri, onları oluşturan yöntemleri doğrudan çağırarak bağlamak (biz buna wiring diyeceğiz)
  • Bir metod parametresi kullanarak Spring'in bize bir değer sağlamasını isteyeceğiz. (buna ise auto-wiring diyeceğiz)

1. Konfigürasyon Sınıfında Tanımlanan Bean'ler Arasında Bağlantı Kurma

Bu bölümde, konfigürasyon sınıfında @Bean anotasyonu ile oluşturulan bean'ler arasında yine anotasyon kullanarak bağlantı kurmayı öğreneceksiniz. Bu yaklaşımla sıklıkla da karşılaşacaksınız.

İlk olarak Spring Context içinde iki instance olduğunu varsayalım: bir papağan (parrot) ve bir kişi (person). Bu instance'ları oluşturup context'e ekleyeceğiz. İstediğimiz şey ise kişiyi bir papağan sahibi yapmak olacak. Başka bir deyişle, iki instance'ı birbirine bağlamamız gerekiyor. Bu basit örnek, gereksiz karmaşıklık yaratmadan bean'leri Spring context'inde birbirine bağlamak için iki yaklaşımı tartışmamıza yardımcı olacak ve yalnızca Spring konfigürasyonlarına odaklanmanızı sağlayacak.

Dolayısıyla, iki yaklaşımın (wiring ve auto-wiring) her biri için iki adımımız var (şekil 1):

  1. Kişiyi ve papağan bean'ini Spring context'ine ekleyin (Bir önceki yazımızda öğrendiğiniz gibi).
  2. Kişi ve papağan arasında bir ilişki kurun.
Şekil 1: Yapmak istediğimiz şey context'e bean'leri eklemek ve aralarında ilişki kurmak

Spring context'inde iki bean'e sahip olmak, aralarında bir ilişki kurmak istiyoruz. Bunu, wiring veya auto-wiring sayesinde yapabiliriz.

Şekil 2, kişi ve papağan nesnesi arasındaki “has-A” ilişkisini Şekil 1'den daha teknik bir şekilde gösterir.

Şekil 2: Person ve Parrot sınıflarının UML ile gösterimi

Örneklerimizi gerçekleştirebilmek için sınıflarımızı hızlıca oluşturalım.

Parrot sınıfımız:

public class Parrot {
 
  private String name;
 
  @Override
  public String toString() {
    return "Parrot : " + name;
  }
}

Person sınıfımız:

public class Person {
 
  private String name;
  private Parrot parrot;
}

Son olarak ise konfigurasyon sınıfında @Bean anotasyonu kullanarak iki bean'i nasıl tanımlayacağımıza bakalım.

@Configuration
public class ProjectConfig {
 
  @Bean
  public Parrot parrot() {
    Parrot p = new Parrot();
    p.setName("Koko");
    return p;
  }
 
  @Bean
  public Person person() {
    Person p = new Person();
    p.setName("Ella");
    return p;
  }
}

Şimdi aşağıdaki kodda gösterildiği gibi bir Main sınıf yazabilir ve iki instance'ın henüz birbirine bağlı olmadığını kontrol edebilirsiniz.

public class Main {
 
  public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext
      (ProjectConfig.class);                      ❶
 
    Person person = 
      context.getBean(Person.class);              ❷
 
    Parrot parrot = 
      context.getBean(Parrot.class);              ❸
 
    System.out.println(
      "Person's name: " + person.getName());      ❹
 
    System.out.println(
      "Parrot's name: " + parrot.getName());      ❺
 
    System.out.println(
      "Person's parrot: " + person.getParrot());  ❻
  }}

❶ Yapılandırma sınıfına dayalı olarak Spring context'in bir instance'ını oluşturur

❷ Spring context'inden Person sınıfının bir referansını alır

❸ Spring context'inden Parrot sınıfının bir referansını alır

❹ Person bean'inin context içinde olduğunu kanıtlamak için kişinin adını yazdırır

❺ Parrot bean'inin context içinde olduğunu kanıtlamak için papağanın adını yazdırır

❻ Instance'lar arasında henüz bir ilişki olmadığını kanıtlamak için kişinin papağanını yazdırır

Bu uygulamayı çalıştırırken, sonraki kod parçacığında sunulana benzer bir konsol çıktısı göreceksiniz:

Person's name: Ella     ❶
Parrot's name: Koko     ❷
Person's parrot: null   ❸

❶ Person bean'i Spring context içinde var

❷ Parrot bean'i Spring context içinde var

❸ Person ve parrot arasındaki ilişki kurulmamıştır.

Burada dikkat edilmesi gereken en önemli şey, kişinin papağanının (üçüncü output) null olmasıdır. Bununla birlikte, hem kişi hem de papağan instance'ları context içindedir. Bu null çıktısı, instance'lar arasında henüz bir ilişki olmadığını gösterir (şekil 3).

Şekil 3: Context içinde bean'ler mevcut fakat aralarında bir ilişki bulunmuyor.

Şimdi ne yapmak istediğimizi anladıysak iki yaklaşım ile de nasıl yapabileceğimizi görelim.

1.1 @Bean Metodları Arasında Doğrudan Metod Çağrımı Yaparak Bean'leri Bağlama

Bu bölümde, Person ve Parrot instance'ları arasındaki ilişkiyi kuracağız. Bunu başarmanın ilk yolu (wiring), konfigürasyon sınıfında bir metodu diğerinden çağırmaktır. Bu yöntem sık sık kullanılır, çünkü bu basit bir yaklaşımdır. Bir sonraki kod blokunda, Person ve Parrot arasında bir bağlantı kurmak için yapılandırma sınıfımda yapmam gereken küçük değişikliği bulabilirsiniz (Şekil 4).

Şekil 4: Person metodu içinde Parrot metodunu çağırıyoruz.

BEan'ler arasındaki ilişkiyi doğrudan wiring kullanarak kuruyoruz. Bu yaklaşım, doğrudan ayarlamak istediğiniz bean'i döndüren metodu çağırmayı ifade eder. Bu metodu, bağımlılığı ayarladığınız bean'i tanımlayan metoddan çağırmanız gerekir.

@Configuration
public class ProjectConfig {
 
  @Bean
  public Parrot parrot() {
    Parrot p = new Parrot();
    p.setName("Koko");
    return p;
  }

  @Bean
  public Person person() {
    Person p = new Person();
    p.setName("Ella");
    p.setParrot(parrot());     ❶
    return p;
  }
}

❶ Papağan bean'inin kişinin papağan field'ına referansını ayarlama

Aynı uygulamayı çalıştırdığınızda, konsolda değiştirilen çıktıyı gözlemleyeceksiniz. Şimdi ikinci satırın Ella'nın (context içindeki Person objesi) Koko'nun (context içindeki Parrot objesi) sahibi olduğunu gösterdiğini göreceksiniz:

Person's name: Ella
Person's parrot: Parrot : Koko    ❶

❶ Artık kişi ile papağan arasındaki ilişkinin kurulduğunu gözlemliyoruz.

Bu yaklaşımın ardından bazılarının aklına şu soru gelmiş olabilir: Bu, iki Parrot instance'ı (Şekil 5) yarattığımız anlamına gelmiyor mu? Bir instance'ı context ilgili Bean anotasyonunu görünce, diğerini instance'ı da parrot() metodunu çağırdığımızda oluşturmuş olmuyor muyuz? Cevabımız hayır, bu uygulamada genel olarak sadece bir papağan instance'ımız var.

Şekil 5: parrot() metodu çağırdığımızda ikinci Parrot instance'ı mı oluşacak?

İlk başta garip görünebilir, ancak Spring parrot() metodunu çağırdığınızda, context'indeki parrot bean'ini istediğinizi anlayacak kadar akıllıdır. Bean'leri Spring Context'e eklemek için @Bean anotasyonu kullandığımızda, Spring metodların nasıl çağrıldığını kontrol eder ve metod çağrısının üzerine mantık uygulayabilir (sonraki yazılarda detaylarını göreceğiz). Şu an için, person() metodu parrot() metodunu çağırdığında, Spring'in bir sonraki açıklandığı gibi mantık uygulayacağını unutmayın.

Spring, eğer Parrot bean'i zaten context'te varsa, parrot() metodunu çağırmak yerine instance'ı doğrudan context'ten alacaktır. Parrot bean'i henüz context'te mevcut değilse, Spring parrot() metodunu çağırır ve bean'i döndürür (Şekil 6).

Şekil 6: çağrılan metodun bean'inin context içinde olup olmamasına göre Spring'in davranışı değişir.

@Bean anotasyonlu iki metod birbirini çağırdığında, Spring bean'ler arasında bir bağlantı oluşturmak istediğinizi bilir. Bean zaten (3A) context içinde varsa, Spring çağrımı @Bean metoduna iletmeden mevcut bean'i döndürür. Bean yoksa (3B), Spring bean'i oluşturup context'e ekler ve referansını geri verir.

Aslında bu davranışı test etmek oldukça kolaydır. Parrot sınıfına bir no-args constructor eklemeniz ve konsola ondan bir mesaj yazdırmanız yeterlidir. İletinin konsolda kaç kez yazdırıldığı kontrolü ile işleyişi test edebiliriz. Davranış doğruysa, iletiyi yalnızca bir kez görürsünüz. Hadi şu deneyi yapalım.

public class Parrot {
 
  private String name;
 
  public Parrot() {
    System.out.println("Parrot created");
  }
 
  @Override
  public String toString() {
    return "Parrot : " + name;
  }
}

Uygulamayı yeniden çalıştırın. Çıktı değişti ve şimdi "Parrot created" mesajı da görünüyor. Sadece bir kez göründüğünü gözlemleyeceksiniz, bu da Spring'in bean'i yaratmayı yönettiğini ve parrot() metodunu sadece bir kez çağırdığınızı kanıtlıyor:

Parrot created
Person's name: Ella
Person's parrot: Parrot : Koko

1.2 @Bean Anotasyonlu Metodun Parametreleri Yardımıyla Bean'leri Bağlama

Bu bölümde, size doğrudan @Bean metodunu çağırmak için alternatif bir yaklaşım göstereceğim. Başvurmak istediğimiz bean'i tanımlayan metodu doğrudan çağırmak yerine, ilgili nesne türünün metoduna bir parametre ekleriz ve bu parametre aracılığıyla bize bir değer sağlamak için Spring'e güveniriz (Şekil 7). Bu yaklaşım, bölüm 1.1'de tartıştığımız yaklaşımdan biraz daha esnektir. Bu yaklaşımda, elde etmek istediğimiz bean'in @Bean anotasyonlu bir metodla tanımlanması veya @Component gibi stereotype anotasyonuyla tanımlanmış olması önemli değildir.

Deneyimlerime göre, geliştiricilerin bu yaklaşımı kullanmasını sağlayanın mutlaka bu esneklik olmadığını gözlemledim; çoğunlukla bean ile çalışırken hangi yaklaşımı kullanacaklarını belirleyen her geliştiricinin kendi zevkidir. Birinin diğerinden daha iyi olduğunu söyleyemem, ancak gerçek dünyadaki senaryolarda her iki yaklaşımla da karşılaşacaksınız ve bu yüzden bunları anlayıp kullanabilmeniz gerekir.

@Bean yöntemini doğrudan çağırmak yerine bir parametre kullandığımız bu yaklaşımı göstermek için kullanacağımız koda bakalım.

Şekil 7: Parametre ile Spring'e istediğimiz bean'inin ne olduğunu iletiyoruz.

Metoda bir parametre tanımlayarak, Spring'e context'ten bu parametrenin tipinde bir bean sağlamasını söyleriz. Daha sonra ikincisini (person) oluştururken sağlanan bean'i (parrot) kullanabiliriz. Bu şekilde iki bean arasındaki has-A ilişkisini kurarız.

Sonraki kod blokunda, konfigürasyon sınıfının tanımını bulabilirsiniz. Person() yöntemine göz attığınızda Parrot türünde bir parametre aldığını ve dönen referansı ilgili field'a atadığımızı görebilirsiniz. Metodu çağırırken, Spring context'inde bir Parrot bean'i bulması ve değerini person() yönteminin parametresine enjekte etmesi gerektiğini bilir.

@Configuration
public class ProjectConfig {
 
  @Bean
  public Parrot parrot() {
    Parrot p = new Parrot();
    p.setName("Koko");
    return p;
  }
 
  @Bean
  public Person person(Parrot parrot) {     ❶
    Person p = new Person();
    p.setName("Ella");
    p.setParrot(parrot);
    return p;
  }
}

❶ Spring Parrot bean'inin bu parametreye enjekte eder.

Önceki paragrafta "enjekte etmek" fiilini kullandım. Burada, bundan sonra Dependency Injection (DI) kullandığımızda ne yapacağımıza atıfta bulunuyorum. Adından da anlaşılacağı gibi, DI, belirli bir field'a veya parametreye değer ayarlayan bir tekniktir. Bizim durumumuzda Spring, çağırdığımız person() metodunun parametresine belirli bir değer ayarlar ve bu metodun bağımlılığını çözer. DI, IoC ilkesinin bir uygulamasıdır ve IoC, framework'ün yürütme sırasında uygulamayı denetlediğini ifade eder. Bu konuyu "Spring Framework Nedir? - Spring Ekosistemi" başlıklı yazımızda detaylıca incelemiştik.

Oluşturulan nesne instance'larını yönetmenin ve uygulamalarımızı geliştirirken yazdığımız kodu en aza indirmemize yardımcı etmenin çok rahat bir yolu olduğundan, genellikle DI kullanmamız gereken en önemli pattern'lerden birisidir.

Uygulamayı tekrar çalıştırdığımızda, konsolunuzdaki çıktı bir sonraki kod snippet'ine benzer olacaktır. Papağan Koko'nun gerçekten Ella kişisi ile bağlantılı olduğunu gözlemleyeceksiniz:

Parrot created
Person's name: Ella
Person's parrot: Parrot : Koko

2. Bean Enjekte Etmek İçin @Autowired Anotasyonunu Kullanmak

Bu bölümde, Spring Context içinde yer alan bean'ler arasında bağlantı (dependency) oluşturmak için kullanılan başka bir yaklaşımı ele alacağız. Bean'i tanımladığınız sınıfı değiştirebildiğinizde (bu sınıf bir bağımlılığın parçası olmadığında) @Autowired adlı bir anotasyon kullandığımız bu teknikle sık sık karşılaşacaksınız. @Autowired anotasyonunu kullanarak, Spring'in context'inden bir değer eklemesini istediğimiz nesnenin field'ını işaretleriz ve bu amacı doğrudan bağımlılığa ihtiyaç duyan nesneyi tanımlayan sınıfta işaretleriz. Bu yaklaşım, bölüm 1'de tartıştığımız alternatiflerden iki nesne arasındaki ilişkiyi görmeyi kolaylaştırır. Göreceğiniz gibi, @Autowired aotasyonunu kullanmanın üç yolu vardır:

  • Sınıfın field'ına değer enjekte etmek. Bu yöntem genellikle kod örneklerinde ve proof of concept işlerde kullanılır.
  • Değeri constructor parametreleriyle enjekte etmek. Bu yöntem gerçek dünyadaki senaryolarda en sık kullanacağınız yaklaşımdır.
  • Değeri, setter ile enjekte etmek. Bu yöntem çok sık kullanılmaz.

Bunları daha ayrıntılı tartışalım ve her biri için bir örnek yazalım.

2.1 Sınıf Field'larında @Autowired Kullanarak Injection İşlemi

Bu bölümde, geliştiricilerin kodlarında sıklıkla kullandığı @Autowired anotasyonunun kullanıldığı üç yaklaşımdan en basitini tartışarak başlıyoruz. Bu yaklaşım sınıf field'larının üstünde anotasyon kullanmaktır (Şekil 8). Öğreneceğiniz gibi, bu yaklaşım çok basit olsa bile dezavantajlara sahiptir ve bu yüzden production kodunu yazarken kullanmaktan kaçınırız. Ancak, b kod örneklerinde, proof of concept işlerde ve test yazmada sıklıkla kullanıldığını göreceksiniz, bu nedenle bu yaklaşımı nasıl kullanacağınızı bilmeniz gerekir.

Şekil 8: Sınıf field'ı üzerinde @Autowired anotasyonunu kullanarak enjekte işlemi

Field üzerindeki @Autowired anotasyonunu kullanarak, Spring'e bu field için context'inden bir değer sağlaması talimatını veriyoruz. Spring, person ve parrot olmak üzere iki bean oluşturur ve parrot nesnesini Person türündeki bean'in field'ına enjekte eder.

Spring'e context içinden bir değer enjekte etmek istediğimizi söylemek için Person sınıfının parrot field'ına @Autowired anotasyonu ekleyebileceğimiz bir proje geliştirelim. İki nesnemizi tanımlayan sınıflarla başlayalım: Person ve Parrot. Parrot sınıfının tanımını bir sonraki kod snippet'inde bulabilirsiniz:

@Component
public class Parrot {
 
  private String name = "Koko";
 
  @Override
  public String toString() {
    return "Parrot : " + name;
  }
}

Bir önceki yazımızda öğrendiğiniz stereotype anotasyonu olan @Component i burada kullanıyoruz. Stereotype anotasyonunu, konfigürasyon sınıfını kullanarak bean oluşturmaya alternatif olarak kullanırız. Bir sınıfa @Component anotasyon eklerken Spring, bu sınıfın bir instance'ını oluşturması ve context içine eklemesi gerektiğini bilir. Sonraki kod snippet'i Person sınıfının tanımını gösterir:

@Component
public class Person {
 
  private String name = "Ella";
 
  @Autowired                   ❶
  private Parrot parrot;
}

❶ Field'ı @Autowired ile işaretleyerek, Spring'e context içinden uygun bir değer enjekte etmesi talimatını veriyoruz.

NOT: Bu örnekte Spring Context'e bean eklemek için stereotype anotasyonu kullandım. Bean'leri @Bean kullanarak tanımlayabilirdim, ama çoğu zaman, gerçek dünyadaki senaryolarda, stereotype anotasyonlarıyla birlikte kullanılan @Autowired karşılaşacaksınız, bu yüzden sizin için en yararlı yaklaşıma odaklanalım.

Örneğimize devam etmek için bir konfigürasyon sınıfı tanımlıyoruz. Konfigürasyon sınıfına ProjectConfig adını vereceğim. Bu sınıfta, önceki yazımızda öğrendiğiniz gibi Spring'e @Component anotasyonu verdiğim sınıfları nerede bulacağını söylemek için @ComponentScan anotasyonunu kullanacağım. Sonraki kod snippet'i konfigürasyon sınıfının tanımını gösterir:

@Configuration
@ComponentScan(basePackages = "beans")
public class ProjectConfig {
 
}

Daha sonra, Spring'in parrot bean'inin referansını doğru bir şekilde enjekte ettiğini kanıtlamak için bu bölümün önceki örneklerinde kullandığım gibi Main sınıfı kullanacağım:

public class Main {
 
  public static void main(String[] args) {
    var context = new AnnotationConfigApplicationContext
                        (ProjectConfig.class);
 
    Person p = context.getBean(Person.class);
 
    System.out.println("Person's name: " + p.getName());
    System.out.println("Person's parrot: " + p.getParrot());
  }
}

Bu, uygulamanın konsoluna aşağıdaki çıktıya benzer bir şey yazdırır. Çıktının ikinci satırı, papağanın (benim durumumda Koko adlı) kişi bean'ine (Ella adlı) ait olduğunu kanıtlamaktadır:

Person's name: Ella
Person's parrot: Parrot : Koko

Peki production kodunda neden bu yaklaşım istenmez? Kullanmak tamamen yanlış değildir, ancak uygulamanızı production kodunda sürdürülebilir ve test edilebilir hale getirmek istersiniz kullanmamanız daha doğru olacaktır.

Değeri doğrudan field'a enjekte ederseniz;

  • field'ı final yapma seçeneğiniz olmaz (sonraki kod snippet'ine bakın) ve bu şekilde, kod initialize edildikten sonra kimsenin bu değişkenin değerini değiştiremeyeceğinden emin olamazsınız:
@Component
public class Person {
 
  private String name = "Ella";
 
  @Autowired   
  private final Parrot parrot;     ❶
 
}

❶ Bu kod derlenmez ve hata verir. Başlangıç değeri olmayan bir final field tanımlayamazsınız.

  • initialize aşamasında değeri kendiniz yönetmeniz daha zordur.

2.2 Constructor İle @Autowired Kullanarak Injection İşlemi

Nesnenin attribute'lerine değer enjekte etmek için sahip olduğunuz ikinci seçenek, instance'ı tanımlayan sınıfın constructor'ını kullanmaktır (Şekil 9). Bu yaklaşım, production kodunda en sık kullanılan ve önerdiğim yaklaşımdır. Bu yaklaşımla field'ları final olarakta  tanımlayabilirsiniz. Constructor'ı çağırırken değerleri ayarlama olanağı, Spring'in sizin için field enjeksiyonu yapmasına güvenmek istemediğiniz belirli birim testleri yazarken de size yardımcı olur.

Şekil 9: Construction injection yöntemiyle bean'leri enjekte edebilirsiniz.

Constructor'ın bir parametresini tanımladığınızda, Spring constructor'ı çağırırken bu parametreye değer olarak context'inden geçerli bir bean sağlar.

Projemizi field injection yerine constructor injection kullanacak şekilde hızlıca değiştirebiliriz. Aşağıdaki kodda sunulduğu gibi yalnızca Person sınıfını değiştirmeniz yeterlidir. Sınıf için bir constructor tanımlamanız ve @Autowired anotasyonunu üzerine eklemeniz gerekir. Şimdi parrot field'ını da final yapabiliriz. Konfigürasyon sınıfınızda herhangi bir değişiklik yapmanız gerekmez.

@Component
public class Person {
 
  private String name = "Ella";
 
  private final Parrot parrot;    ❶
 
  @Autowired                      ❷
  public Person(Parrot parrot) {
    this.parrot = parrot;
  } 
}

❶ Artık bu field final ile initialize edildikten sonra değerinin değiştirilemeyeceği hale getirildi.

❷ Constructor üzerinde @Autowired anotasyonunu kullanırız.

Uygulamayı tekrar başlattığınızda aynı sonucu gözlemleyebilirsiniz. Bir sonraki kod snippet'inde görebileceğiniz gibi, kişi papağanın sahibidir, bu nedenle Spring iki instance arasındaki bağlantıyı doğru bir şekilde kurmuş oldu:

Person's name: Ella
Person's parrot: Parrot : Koko

NOT: Spring versiyon 4.3'ten sonra, sınıfta yalnızca bir constructor olduğunda, @Autowired anotasyonunu yazmadan da constructor injection yapabilirsiniz.

2.3 Setter İle @Autowired Kullanarak Injection İşlemi

Dependency injection için setter kullanma yaklaşımını uygulayan geliştiricileri sık sık bulamazsınız. Bu yaklaşımın avantajlardan daha fazla dezavantajı vardır: Okumak daha zordur, field'ları final yapmanıza izin vermez ve test yazmayı zorlaştırır. Yine de bu yaklaşımından da bahsetmek istedim.

Setter injection uygulamak için yalnızca Person sınıfını değiştirmemiz yeterlidir. Bir sonraki kod snippet'inde, setter'da @Autowired anotasyonunu kullandım:

@Component
public class Person {
 
  private String name = "Ella";
 
  private Parrot parrot;
  
  @Autowired
  public void setParrot(Parrot parrot) {
    this.parrot = parrot;
  }
}

Uygulamayı çalıştırdığnızda, bu bölümün daha önce gösterilen örnekleriyle aynı çıktıyı alırsınız.

3. Döngüsel Bağımlılıklar İle Başa Çıkmak

Spring'in uygulamanızın nesnelerine bağımlılıkları (dependency) oluşturmasına ve ayarlamasına izin vermek rahatlatıcı bir işlemdir. Spring'in bu işi sizin için yapmasına izin vermek sizi bir dizi kod satırı yazmaktan kurtarır ve uygulamanın okunmasını ve anlaşılmasını kolaylaştırır. Ancak Spring'in bazı durumlarda kafası karışabilir. Uygulamada sıklıkla karşılaşılan bir senaryo, yanlışlıkla döngüsel bağımlılık (circular dependency) oluşturmaktır.

Döngüsel bağımlılık (Şekil 10), bir bean oluşturmak için (adını Bean A verelim), Spring'in henüz var olmayan başka bir bean enjekte etmesi gereken bir durumdur (Bean B). Ancak Bean B aynı zamanda Bean A'ya bağımlıdır. Yani, Bean B'yi yaratmak için, Spring'in önce Bean A'ya ihtiyacı var. Spring şimdi çıkmaza girer. Bean B'ye ihtiyacı olduğu için Bean A oluşturamaz, Bean A'ya ihtiyacı olduğu için Bean B'yi yaratamaz.

Şekil 10: Circular Dependency durumu

Döngüsel bağımlılık örneğine bakarsak, Spring'in Parrot tipinde bir bean oluşturması gerekiyor. Ancak Parrot'un bir Person bağımlılığı olduğundan, Spring'in önce bir Person yaratması gerekir. Ancak, bir Person oluşturmak için, Spring'in zaten bir Parrot üretmiş olması gerekir. Spring şimdi çıkmaza girdi. Bir Person'a ihtiyaç duyduğu için Parrot oluşturamaz ve Parrot'a ihtiyacı olduğu için bir Person yaratamaz.

Döngüsel bağımlılıktan kaçınmak kolaydır. Sadece oluşturulması diğerine bağlı olan nesneleri tanımlamadığınızdan emin olmanız gerekir. Bunun gibi bir nesneden diğerine bağımlılıklara sahip olmak, sınıfların kötü bir tasarımıdır. Böyle bir durumda, kodunuzu yeniden yazmanız gerekir.

Bir uygulamada en az bir kez döngüsel bağımlılık oluşturmayan herhangi bir Spring geliştiricisi tanıdığımı sanmıyorum. Bu senaryonun farkında olmanız gerekir, böylece karşılaştığınızda nedenini bilirsiniz ve hızlı bir şekilde çözersiniz.

Aşağıdaki kod örneğinde döngüsel bağımlılık örneği bulacaksınız. Bir sonraki kod parçacıklarında sunulduğu gibi, Parrot bean'inin instance'ını Person bean'ine bağımlı hale getirdim ve bunun tersi de oldu.

@Component
public class Person {
 
  private final Parrot parrot;
 
  @Autowired
  public Person(Parrot parrot) {    ❶
    this.parrot = parrot;
  }
}

❶ Person instance'ı oluşturmak için Spring'in bir Parrot bean'ine sahip olması gerekir.

public class Parrot {
 
  private String name = "Koko";
 
  private final Person person;
 
  @Autowired
  public Parrot(Person person) {    ❶
    this.person = person;
  }
}

❶ Parrot instance'ını oluşturmak için, Spring'in bir Person bean'ine sahip olması gerekir.

Uygulamayı böyle bir yapılandırmayla çalıştırmak, bir sonraki snippet'te sunulan gibi bir exception'a yol açacaktır:

Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'parrot': Requested bean is currently in creation: Is there an unresolvable circular reference?
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:347)

Bu exception mesajında, Spring karşılaştığı sorunu size anlatmaya çalışır. Exception iletisi oldukça açıktır: Spring döngüsel bir bağımlılık ve duruma neden olan sınıflarla ilgilenir. Böyle bir exception bulduğunda, exception tarafından belirtilen sınıflara gitmeniz ve döngüsel bağımlılığı ortadan kaldırmanız gerekir.

4. Spring Contex'te Birden Fazla Bean Arasından Seçim Yapma

Bu bölümde, Spring'in bir parametreye veya sınıf field'ına bir değer eklemesi gereken, ancak aralarından seçim yapabileceğiniz aynı tipte birden çok bean'e sahip olduğu senaryoyu ele alıyoruz. Spring context'inde üç Parrot bean'iniz olduğunu varsayın ve Spring'i, Parrot tipinde bir değeri bir parametreye ek olarak yapılandırıyorsunuz. Bu durumda Spring nasıl davranacak? Aynı tip bean'lerden hangisi böyle bir senaryoda enjekte edilecek?

Uygulamanıza bağlı olarak, aşağıdaki durumlara sahipsiniz:

  1. Parametrenin tanımlayıcısı, içerikteki bean'lerden birinin adıyla eşleşir (unutmayın, değerini döndüren @Bean anotasyonlu metodun adı). Bu durumda, Spring, adın parametreyle aynı olduğu bean'i seçecektir.
  2. Parametrenin tanımlayıcısı içerikteki bean adlarının hiçbiriyle eşleşmiyor. Bu durumda aşağıdaki seçeneklere sahipsiniz:
  • Bean'lerden birini @Primary anotasyonu ile primary olarak işaretlediyseniz Spring ilk olarak bu bean'i tercih edecektir.
  • Birazdan göreceğimiz @Qualifier anotasyonunu kullanarak belirli bir bean'i açıkça seçebilirsiniz.
  • Fasulyelerin hiçbiri primary değilse ve @Qualifier kullanmıyorsanız, uygulama bir exception fırlatır ve context'in aynı türde birden fazla bean içerdiğinden ve Spring'in hangisini seçeceğini bilmediğinden şikayet eder.

Spring context'inde birden fazla aynı tip instance'a sahip olduğumuz bir senaryo deneyelim. Sonraki snippet, iki Parrot instance'ı tanımlayan ve metod parametreleri aracılığıyla injection kullanan bir konfigürasyon sınıfını gösterir.

@Configuration
public class ProjectConfig {
 
  @Bean
  public Parrot parrot1() {
    Parrot p = new Parrot();
    p.setName("Koko");
    return p;
  }
 
  @Bean
  public Parrot parrot2() {
    Parrot p = new Parrot();
    p.setName("Miki");
    return p;
  }
 
  @Bean
  public Person person(Parrot parrot2) {     ❶
    Person p = new Person();
    p.setName("Ella");
    p.setParrot(parrot2);
    return p;
  }
}

❶ Parametrenin adı papağan Miki'yi temsil eden bean'in adıyla eşleşir.

Uygulamayı bu yapılandırmayla çalıştırarak, bir sonraki kod snippet'ine benzer bir konsol çıkışı gözlemlersiniz. Spring'in person bean'ini Miki adlı papağana bağladığını gözlemleyin, çünkü bu papağanı temsil eden bean parrot2 adına sahiptir (Şekil 3.11):

Parrot created
Person's name: Ella
Person's parrot: Parrot : Miki
Şekil 11: Bean adı tam eşleştiği için Spring hangi instance'ı seçeceğini bilir.

Context aynı türün birden fazla instance'ını içerdiğinde, Spring'e context'inden belirli bir instance sağlamasını sağlamanın bir yolu, bu instance'ın adına güvenmektir. Parametreyi Spring'in size sağlamasını istediğiniz instance ile aynı şekilde adlandırmanız yeterlidir.

Gerçek bir senaryoda, başka bir geliştirici tarafından kolayca yeniden değiştirilebilen ve yanlışlıkla değiştirilebilen parametrenin adına güvenmekten kaçınmayı tercih ederim. Daha rahat hissetmek için, genellikle belirli bir bean enjekte etme niyetimi ifade etmek için daha görünür bir yaklaşım olan @Qualifier anotasyonundan yana kullanırım. Yine, deneyimlerime göre, geliştiricilerin @Qualifier anotasyonunu kullanmak için tartıştığını ve karşı olduğunu gördüm. Bu durumda kullanmanın daha iyi olduğunu hissediyorum çünkü niyetinizi açıkça tanımlıyor. Diğer geliştiriciler, bu anotasyonu eklemenin gerekli olmayan (ortak) kod oluşturduğuna inanıyor.

Aşağıdaki snippet, @Qualifier anotasyonu kullanarak bir instance sağlar. Parametrenin belirli bir tanımlayıcısına sahip olmak yerine, şimdi @Qualifier anotasyonu ile eklemek istediğim bean'i belirttiğimi gözlemleyin.

@Configuration
public class ProjectConfig {
 
  @Bean
  public Parrot parrot1() {
    Parrot p = new Parrot();
    p.setName("Koko");
    return p;
  }
 
  @Bean
  public Parrot parrot2() {
    Parrot p = new Parrot();
    p.setName("Miki");
    return p;
  }
 
  @Bean
  public Person person(
    @Qualifier("parrot2") Parrot parrot) {    ❶
 
    Person p = new Person();
    p.setName("Ella");
    p.setParrot(parrot);
    return p;
  }
}

❶ @Qualifier anotasyonunu kullanarak, context'ten belirli bir bean'i enjekte etme niyetinizi açıkça işaretlersiniz.

Uygulamayı yeniden çalıştırarak, uygulama konsola aynı sonucu yazdırdığını doğrulayabilirsiniz:

Parrot created
Person's name: Ella
Person's parrot: Parrot: Miki

Benzer bir durum, @Autowired ek açıklamayı kullanırken de olabilir. Bu örnekte, Parrot türünde iki bean (@Bean anotasyonu kullanarak) ve bir Person instance'ı (stereotype anotasyonları kullanarak) tanımlıyoruz. Spring'i, person tipindeki bean'e iki papağan bean'inden birini enjekte edecek şekilde yapılandıracağım.

Bir sonraki kod snippet'inde sunulduğu gibi, parrot sınıfına @Component ek açıklamayı eklemedim, çünkü konfigürasyon sınıfındaki @Bean anotasyonunu kullanarak Parrot tipindeki iki bean'i tanımlamayı planlıyorum:

public class Parrot {
 
  private String name;
}

@Component stereotype ek anotasyonunu kullanarak Person türünde bir bean tanımlıyoruz. Bir sonraki kod snippet'inde constructor'ın parametresine verdiğim tanımlayıcıyı gözlemleyin. "Parrot2" tanımlayıcısını vermemin nedeni, Spring'in bu parametreye enjekte etmesini istediğim context'teki bean için de yapılandıracağım addır:

@Component
public class Person {
 
  private String name = "Ella";
 
  private final Parrot parrot;
 
  public Person(Parrot parrot2) {
    this.parrot = parrot2;
  }
}

Yapılandırma sınıfındaki @Bean ek açıklamayı kullanarak Parrot tipinde iki bean tanımlıyorum. Spring'e stereotype anotasyonları eklenmiş sınıfları nerede bulacağını söylemek için yine de @ComponentScan eklememiz gerektiğini unutmayın. Bizim durumumuzda, Person sınıfını @Component anotasyonu ile işaretledik. Sonraki kod yapılandırma sınıfının tanımını gösterir.

@Configuration
@ComponentScan(basePackages = "beans")
public class ProjectConfig {
 
  @Bean
  public Parrot parrot1() {
    Parrot p = new Parrot();
    p.setName("Koko");
    return p;
  }
 
  @Bean    
  public Parrot parrot2() {      ❶
    Parrot p = new Parrot();
    p.setName("Miki");
    return p;
  }
}

❶ Mevcut kurulumda, parrot2 adlı bean, Spring'in Person bean'ine enjekte ettiği bean'dir.

Bir sonraki kod snippet'inde sunulan Main metodu çalıştırırsanız ne olur? Bizimki hangi papağanın sahibi olacaktır? Constructor'ın parametresinin adı Spring context'inde bean'ın adlarından biriyle eşleştiğinden (parrot2), Spring bu bean'i enjekte eder (Şekil 12). Bu nedenle uygulamanın konsola yazdırdığı papağanın adı Miki'dir:

public class Main {
 
  public static void main(String[] args) {
    var context = new 
        AnnotationConfigApplicationContext(ProjectConfig.class);
 
    Person p = context.getBean(Person.class);
 
    System.out.println("Person's name: " + p.getName());
    System.out.println("Person's parrot: " + p.getParrot());
  }
}
Şekil 12: Birden fazla aynı tip bean içinden adı eşleşen bean enjekte edilir.

Spring context aynı tipte birden çok bean içerdiğinde, Spring, adı parametrenin adıyla eşleşen bean'i seçer.

Bu uygulamayı çalıştıran konsol aşağıdaki çıktıyı gösterir:

Person's name: Ella
Person's parrot: Parrot: Miki

@Bean anotasyonlu metod parametresi için tartıştığımız gibi, değişkenin adına güvenmemenizi öneririm. Bunun yerine, niyetimi açıkça ifade etmek için @Qualifier anotasyonunu kullanmayı tercih ederim ve Context'ten belirli bir bean enjekte ederim. Bu şekilde, birinin değişkenin adını yeniden düzenleme ve böylece uygulamanın çalışma şeklini etkileme olasılığını en aza indiririz. Bir sonraki kod snippet'inde Person sınıfında yaptığım değişikliğe bakın. @Qualifier ek açıklamayı kullanarak, Spring'in context'ten eklemesini istediğim bean'in adını belirtiyorum ve constructor'ın parametresinin tanımlayıcısına güvenmiyorum:

@Component
public class Person {
 
  private String name = "Ella";
 
  private final Parrot parrot;
 
  public Person(@Qualifier("parrot2") Parrot parrot) {
    this.parrot = parrot;
  }
}

Uygulamanın davranışı değişmez ve çıktı aynı kalır. Bu yaklaşım, kodunuzu hatalara daha az maruz hale getirir.

5. ÖZET

  • Spring Context, framework'ün yönettiği nesneleri toplamak için uygulamanın belleğinde bir yerdir. Framework'ün nesnelerinizi yönetmesini istiyorsanız context'e bu nesneleri eklemeniz gerekir.
  • Bir uygulamayı implemente ederken, bir nesneden diğerine başvurmanız gerekebilir. Böylece, bir nesne sorumluluklarını yürütürken eylemlerini diğer nesnelere devredebilir. Bu davranışı uygulamak için Spring Context'te bean'ler arasında ilişkiler kurmanız gerekir.
  • Üç yaklaşımdan birini kullanarak iki bean arasında bir ilişki kurabilirsiniz:
  1. Bir bean içerisinden, diğerini oluşturan @Bean anotasyonlu metodu çağırarak. Spring, context'te bean'e atıfta bulunduğunuzdan haberdardır ve bean zaten varsa, başka bir instance oluşturmak için aynı metodu tekrar çağırmaz. Bunun yerine, context'teki varolan bean'in döndürür.
  2. @Bean anotasyonlu metoda parametre tanımlama. Spring, @Bean anotasyonlu bir metodun parametresi olduğunu gözlemlediğinde, bu parametrenin tipini context içinde arar ve bu bean'i parametreye bir değer olarak atar.
  3. @Autowired anotasyonunu üç şekilde kullanarak:

3.1. Sınıfın field'larını @Autowired anotasyonuyla işaretleyerek. Bu yaklaşım genellikle örneklerde ve proof of concept kodlarda kullanılır.

3.2. Sınıfın constructor'ını @Autowired anotasyonuyla işaretleyerek ve istediğimiz bean'leri paremetre olarak ekleyerek. Bu yaklaşım gerçek dünyada en çok kullanılan yöntemdir.

3.3. Sınıfın setter metodlarını @Autowired anotasyonuyla işaretleyerek. Bu yaklaşım production kodda sıklıkla kullanılmaz.

  • Yukarıdaki beş seçenekten herhangi birini kullanarak, Spring'in IoC ilkesi tarafından desteklenen bir teknik olan DI yöntemini kullanmış olursunuz.
  • Birbirine bağımlı iki bean'in oluşturulması döngüsel bir bağımlılık oluşturur. Spring, döngüsel bağımlılıkla bean oluşturamaz ve exception fırlatır. Bean'leri yapılandırırken döngüsel bağımlılıklardan kaçındığınızdan emin olun.
  • Spring context, içinde aynı tipten birden fazla bean olduğunda, bu bean'lerden hangisinin enjekte edilmesi gerektiğine karar veremez. Spring'e hangi instance'ın enjekte edilmesi gerektiğini üç yöntemle söyleyebilirsiniz.
  1. @Primary anotasyonu kullanarak, bean'lerden birini bağımlılık ekleme için varsayılan olarak işaretlemek
  2. @Qualifier anotasyonu kullanarak bean'leri adlandırmak ve isme göre enjekte etmek.
  3. Parametre adını Bean'i oluşturan metod adıyla aynı tutmak. Fakat bu yöntemi tercih etmeyin!