Spring Framework ile Dependency Injection

Genel Bakış

Bu yazıda IoC (Inversion of Control) prensibi, Dependency Inversion prensibi, Dependency Injection deseni ve Spring Framework ile bu işlemlerin ne şekilde gerçekleştirileceği hakkında bilgiler edineceğiz.

Inversion of Control Prensibi Nedir?

Inversion of Control, Türkçe’ye kontrolün/bağımlılığın tersine çevrilmesi olarak da geçebilmektedir. Bu kavramı genellikle nesne yönelimli programlamada sıklıkla görmekteyiz. Sınıfların ya da servislerin; bağımlılıklarının, yaşam döngüsünün (oluşturulmasının ve yok edilmesinin) kontrollerinin framework ya da bir container içerisine verilmesi işlemidir. Bu gibi sorumlulukların artık geliştiricilerin değil de başka bir yapının ele alması anlamına gelmektedir. Bu geliştiriciler açısından oldukça önemli bir işlemdir; bir sınıftan nesneler yaratmak, yaratılan nesnenin oluşturulması anındaki detayları bilmek, yaşam döngüsünü efektif bir biçimde yönetmek ve organizasyonunu ele almak gibi birçok kavramdan kurtulması demektir. Bu durumda bu detayların bir yapıya aktarılması geliştiriciye sadece bu yapının kullanmasını bırakmak demektir. Bir yapıyı kullanmak, oluşturup kullanmaktan daha basit olmakla birlikte; geliştirme anında geliştiricileri yüksek efor gerektiren süreçten kurtarır, zaman kazanımı gibi birçok konuda da yardımcı olmaktadır.

Dependency Inversion Prensibi Nedir? (Bağımlılıkların Tersine Çevrilmesi Prensibi)

Nesne yönelimli programlama metodolojisi ile yazılımın geliştirildiği ortamlarda ilerleyen süreçlerde nesneler arası bağ kurmak zor olabiliyor. Bir nesnede yapılan değişiklikler veya yerine başka nesneyi koymak, başka yerlerde problemlere yol açabiliyor. Bu problemleri en aza indirmek için de Dependency Inversion gibi prensiplere ihtiyaç duyuyoruz. “Dependency Inversion” prensibi, sınıfları arası geçişlerin “loosely coupled” yani “gevşek bağlı” olması anlamına geliyor. Yüksek seviye sınıfların, düşük seviye sınıflara direkt bağlı olmaması, her ikisinin de soyutlamalara bağlı olması beklenilen davranıştır. Buradaki soyutlamalar, detaylara bağlı olmamalıdır. Detaylar, soyutlamalara bağlı olmalıdır.

class ExceptionReporter {
    private OracleDatabase oracleDatabase;

    public ExceptionReporter() {
        oracleDatabase = new OracleDatabase();
    }

    public void reportException(Exception exception) {
        oracleDatabase.add(exception);
    }
}

class OracleDatabase {
    public void add(Object object) {
        System.out.println("Exception was added to data source.");
    }
}

dependency-inv

Yukarıdaki diyagram ve kod incelendiğinde ExceptionReporter sınıfının (yüksek seviyeli sınıf), OracleDatabase sınıfına (düşük seviyeli sınıf) direkt olarak bağımlı olduğu görülmektedir. İleride veritabanı olarak Oracle değil de MySQL kullanmak istenirse maalesef bu sınıfa müdahale etmek zorunda kalınacaktır. Bu istenmeyen bir davranıştır. Bunun çözümünü ise buradaki bağımlılıkları soyutlayarak sağlanmasıdır.

Yukarıdaki UML diyagramını düzenlenirse aşağıdaki gibi bir yapı elde edilir.

dependency-inv-2
class ExceptionReporter {
    private IDatabase database;

    public ExceptionReporter(IDatabase database) {
        this.database = database;
    }

    public void reportException(Exception exception) {
        database.add(exception);
    }
}

class MySQLDatabase implements IDatabase {
    @Override
    public void add(Object object) {
        ...
    }
}

class OracleDatabase implements IDatabase {
    @Override

    public void add(Object object) {
        ...
    }
}
interface IDatabase {
    void add(Object object);
}

Yukarıdaki kod ve diyagramda da görüldüğü gibi yüksek seviyeli sınıf alt sınıfa direkt değil de soyutlamalar üzerinden bir bağ kurmaktadır. Buradaki yeni bir implementasyon üst sınıfı direkt olarak etkilememektedir.

Dependency Injection Deseni Nedir? (Bağımlılıkların Enjekte Edilmesi Deseni)

Üst kısımlarda nesnelerin oluşturulması işleminden IoC prensibinin sorumlu olduğundan bahsettik, buradaki enjekte edilme işlemi de IoC Container tarafından gerçekleştirilmektedir. Buradaki enjekte edilme işleminden bahsedilen; bağımlılıkların (nesnelerin, sınıf içerisinde kullanılacak olan yapıların) sınıflara geçilmesi işlemidir.

Spring Framework Nedir?

Spring Framework, resmi dökümanında yazılan yazıya göre kendisini; “Spring, Java programlamayı herkes için; daha hızlı, daha kolay ve daha güvenli hale getirir. Spring’in hız, basitlik ve üretkenliğe odaklanması onu dünyanın en popüler Java framework’ü haline getirdi.” gibi açıklamalar ile tanıtmaktadır. Bu tanımlardan yola çıkarak Spring’in bizlere geliştirme anlamında basitlik sağlayarak yapacağımız işe odaklanmamızı sağlar diyebiliriz.

Spring IoC Container Genel Bir Bakış

Spring IoC Container yukarıda bahsedilen IoC Container görevini üstlenen ve bizler için Dependency Injection desenini uygulayan bir yapıdır. Uygulamalarımızı geliştirirken bu sürecin Spring IoC Container tarafından yürütülmesini bekleriz, bu sayede geliştirme sürecindeki asıl işlere odaklanırız.

Aşağıda Spring IoC Container için temel paketler ve bunlara ait bilgiler yer almaktadir.

org.springframework.beans ve org.springframework.context paketleri Spring IoC için en temel ve en sık kullanılan paketlerdir. Yazının devam eden kısımlarında bu paketler altındaki sınıflar ve bu sınıfların üstlendiği işlevler hakkında bilgiler edineceğiz.

org.springframework.context.ApplicationContext arayüzü, configuration metadata bilgilerini kullanarak istenilen nesnelerin; taranması, enjekte edilmesi, oluşturulması gibi işlemleri gerçekleştirmektedir.

Buradaki configuration metadata olarak adlandırılan tanımlamalar 3 farklı şekilde tanımlanabilmektedir. Bunlar; XMLAnnotation ve Java Code olarak sağlanır. Bu konfigürasyon içerisinde oluşturulmasını istediğimiz nesnelerin; bilgilerini, bağımlılıklarını, yaşam döngülerini ve onlara ait diğer detayları bildirmekteyiz.

Yukarıda belirtilen ApplicationContext arayüzünü uygulayan birkaç sınıf yer almaktadır, bunlardan bazıları; ClassPathXmlApplicationContext ve FileSystemXmlApplicationContext sınıflarıdır. Bu sınıflar içlerine aldığı path’e göre XML dosyasındaki metadata bilgilerine göre ilgili bağımlılıkların oluşturulmasını sağlamaktadır.

* Spring IoC Container sadece XML bilgilerine göre bu işlemi gerçekleştirmemektedir, bu kısımlardan kendini soyutlamaktadır. Son zamanlarda çoğu Spring geliştiricisi Java tabanlı konfigürasyon ile devam etmeyi seçmektedir. (spring.io documentation)

Yukarıda bahsedilen konfigürasyonlara ait metadata ifadelerini kayıt etmek için aşağıda dökümantasyon üzerindeki temel bir XML dosyası yer almaktadır.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="..."> 
        <!-- collaborators and configuration for this bean go here -->
    

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>

Yukarıdaki XML örneğine bakıldığı zaman bean ve beans gibi kavramlar ve bu kavramlar içerisinde de id ve class gibi ifadeler yer almaktadır.

  • Bean: Spring içerisindeki bean kavramı uygulamanızdaki bağımlılık (sınıf) olarak da düşünülebilir. Herbir bean bir bağımlılığı ifade etmektedir, beans ise bunların listesidir. 
  • Id: Bean değeri içerisinde bulunan id ifadesi o bean’in unique (tekil) olarak işaretlenmesini sağlayan özel bir değerdir. O bağımlılığı çağırmak istediğimiz zaman kullanmaktayız.
  • Class: Bean’in bulunduğu class isminin yer aldığı yerdir.

Süreci örnek üzerinden devam ettirelim, temel olarak bir bildirim servisi olduğunu bu servislerin de sms ve email olarak 2’ye ayrıldığını düşünelim. Sınıf, arayüz yapısı ve package düzeni temel olarak aşağıdaki gibi olacaktır:

  • java
    • services
      • notification
        • sms
          • SmsNotificationServiceImpl.java
        • email
          • EmailNotificationServiceImpl.java
        • NotificationService.java
    • App.java
package services.notification;

/**
* Notification service interface.
*/
public interface NotificationService {
   /**
    * Notify something.
    */
   void sendNotification();
}
package services.notification.email;

import services.notification.NotificationService;

public class EmailNotificationServiceImpl implements NotificationService {
   @Override
   public void sendNotification() {
       System.out.println("Notification was sent by email provider.");
   }
}
package services.notification.sms;

import services.notification.NotificationService;

public class SmsNotificationServiceImpl implements NotificationService {
   @Override
   public void sendNotification() {
       System.out.println("Notification was sent by sms provider.");
   }
}

Yukarıdaki servislerin XML karşılığı resources altındaki services.xml ekteki gibi olacaktır.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

   <bean id="smsNotificationService" class="services.notification.sms.SmsNotificationServiceImpl" scope="singleton"/>

   <bean id="emailNotificationService" class="services.notification.email.EmailNotificationServiceImpl" scope="prototype"/>

</beans>

Tanımlamalara bakıldığı zaman:

  • smsNotificationService id değerine sahip bir bean var ve bu da services.notification.sms.SmsNotificationServiceImpl package altındaki sınıfı temsil etmektedir scope değeri olarak da singleton olarak işaretlenmiştir.
  • emailNotificationService id değerine sahip bir bean var ve bu da services.notification.email.EmailNotificationServiceImpl package altındaki sınıfı temsil etmektedir scope değeri olarak da prototype olarak işaretlenmiştir.

** scope olarak tanımlanan ifade, o bean’in oluşturulma sırasındaki davranışını temsil etmektedir. 

  • Singleton: Nesne her çağrıldığında aynı instance (sınıf örneği) vermesi anlamına gelmektedir.
  • Prototype: Nesne her çağrıldığında her seferinde yeniden oluşturulması anlamına gelmektedir.

Yukarıda temel olarak 2 servis tanımı ve scope değeri atanmış halde durmaktadır. Bunların çağrımı için bu metadata ifadelerinin okunması ve daha sonrasında ise kullanılması gerekmektedir, aşağıda bu işlemleri gerçekleştiren kod parçası yer almaktadır.

public class App {
   public static void main(String[] args) {
       ApplicationContext applicationContext = new ClassPathXmlApplicationContext("services.xml");
       NotificationService notificationService = null;

       notificationService = applicationContext.getBean("smsNotificationService", SmsNotificationServiceImpl.class);
       notificationService.sendNotification();

       notificationService = applicationContext.getBean(EmailNotificationServiceImpl.class);
       notificationService.sendNotification();
   }
}

Yazının başında da belirtildiği gibi Spring Framework içerisindeki IoC Container görevini üstlenen ApplicationContext arayüzünü uygulayan temel birkaç sınıf yer almaktadır, burada kullanılan sınıf ise ClassPathXmlApplicationContext sınıfıdır. İçerisine aldığı XML içindeki metadata bilgilerini okuyarak ilgili tanımlamaları kendi içerisinde yapmaktadır.

2 servisi de çağırırken ApplicationContext içerisindeki 2 farklı metot kullanılmıştır, bunlardan biri id değerine göre bir çağrım diğeri ise Class değerine göre. Id değerine göre yapılan çağrımda 2. parametre olarak gelen nesnenin neye çevrileceği bilgisi de parametre olarak geçebilmektedir.

Yukarıdaki kodun çıktısı da şu şekilde olacaktır:

Notification was sent by sms provider.

Notification was sent by email provider.

Yukarıda tanımlı bean’ler için temel işlemler gerçekleştirildi, bunları biraz daha detaylandırıp örneklendirmekte fayda var. Sms gönderme işlemlerde konfigürasyon bilgilerini constructor içerisinde bir nesneden aldığını, Email gönderme işlemlerinde ise konfigürasyon bilgilerinin constructor içerisinde property olarak aldığını düşünelim.

Sms konfigürasyon bilgilerini tutacak temsili sınıf ve diğer sınıfların güncel hali aşağıdaki gibi olacaktır.

package services.notification.sms.configuration;

public class SmsConfiguration {
   public String smsProviderHost = "XYZ-Sms-Provider-Host";
}
public class SmsNotificationServiceImpl implements NotificationService {

   private final SmsConfiguration smsConfiguration;

   public SmsNotificationServiceImpl(SmsConfiguration smsConfiguration) {
       this.smsConfiguration = smsConfiguration;
   }

   @Override
   public void sendNotification() {
       System.out.printf("Notification was sent by %s%n", smsConfiguration.smsProviderHost);
   }
}
public class EmailNotificationServiceImpl implements NotificationService {

   private final String emailProviderHost;
   private final Integer emailProviderPort;

   public EmailNotificationServiceImpl(String emailProviderHost, Integer emailProviderPort) {
       this.emailProviderHost = emailProviderHost;
       this.emailProviderPort = emailProviderPort;
   }

   @Override
   public void sendNotification() {
       System.out.printf("Notification was sent by  %s : %d%n", emailProviderHost, emailProviderPort);
   }
}

Düzenlenen sınıflara bakıldığı zaman, hepsinin de constructor içerisinde bir nesne ya da temel veri tiplerinden değerler aldığı görülmektedir. Constructor ile gelen verileri metadata içerisinde ifade etmek için <constructor-arg/> ifadesi kullanılmaktadır.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

   <bean id="smsConfiguration" class="services.notification.sms.configuration.SmsConfiguration"
         scope="singleton"
         lazy-init="true"/>

   <bean id="smsNotificationService" class="services.notification.sms.SmsNotificationServiceImpl"
         scope="singleton"
         lazy-init="false">
       <constructor-arg name="smsConfiguration" ref="smsConfiguration"/>
   </bean>

   <bean id="emailNotificationService" class="services.notification.email.EmailNotificationServiceImpl"
         scope="prototype"
         lazy-init="false">
       <constructor-arg index="0" value="XYZ-Email-Provider-Host"/>
       <constructor-arg index="1" value="571"/>
   </bean>

</beans>

  • SmsNotificationServiceImpl sınıfına parametre olarak gelecek veri, bir sınıf örneği olduğu için bu sınıf örneğinin hangi sınıfa ait olduğunu XML içerisinde ref ile belirtmek gerekiyor. Buradaki name ifadesi property name ifade etmektedir ve ref, bean içerisindeki id değerine denk gelen yapıyı ifade ediyor. SmsConfiguration sınıfına ait id değeri ​​smsConfiguration olduğu için ref ile bu id değerine atıfta bulunuyoruz ve bizlere bu sınıfa ait bir nesne gönderiyor.
  • EmailNotificationServiceImpl sınıfına bakıldığı zaman, constructor ile alınan ifadeler birer nesne değil de temel veri tipleri olduğu için bunlara direkt value değeri atanabilmektedir. Sms örneğinde olduğu gibi property name değerine de değer atanabilir, burada farklılık olması için parametrelerin index değerine göre atama işlemi gerçekleştirildi.

Bunların yanı sıra bean tanımı içerisinde lazy-init gibi bir ifade yer almaktadır. Bu lazy-init ifadesi ilgili bean’in ne zaman oluşturulacağını temsil etmektedir. Eğer lazy-init değeri true ise çağrıldığı zaman oluşturulacaktır, false ise de uygulama ayağa kalkarken Spring IoC Container direkt olarak bir örneğini oluşturacaktır ve kullanımını istediğimiz zaman bizlere bu oluşturduğu örneği verecektir.

Yukarıdaki kodun çalıştırılması sonucu oluşacak çıktı aşağıdaki gibi olacaktır:

Notification was sent by XYZ-Sms-Provider-Host

Notification was sent by  XYZ-Email-Provider-Host : 571

Proje ilerledikçe sınıflar ve sınıflara parametre olarak geçen yapıların karmaşıklığı artmaktadır. Bu durumda parametrelerin tek tek XML içerisinde takibi zorlaşmaktadır. Bizlere yardımcı olabilecek bir diğer kavram olan autowire kavramı karşımıza çıkmaktadır. Autowire kavramı ile sınıf içerisindeki ilgili bağımlılıkların ne şekilde otomatik olarak enjekte edileceğine karar verebilmekteyiz.

  • no: default olarak gelen davnıştır, XML içerisinde ref  değerlerine bakılacak ilgili bağımlılıklar çözülmektedir.
  • byName: property adına göre otomatik olarak bağımlılıkları enjekte etmektedir. Eğer bir property autowire olarak işaretlenmişse; property adı jsonService olsun, Spring ilk olarak setJsonService(..) adında bir metot arayacak ve oradan ilgili enjekte işlemlini gerçekleştirecektir.
  • constructor: constructor içerisindeki elemanları tiplerine göre otomatik olarak enjekte edecektir.
  • byType: tipe göre ilgili enjekte işlemini gerçekleştirmektedir.

Aşağıda SmsNotificationServiceImpl sınıfına ait bağımlılıkların autowire ile constructor içerisinden otomatik alınması görülmektedir buna ek olarak da EmailNotificationServiceImpl sınıfına ait bağımlılıklar constructor ile değil de direkt property value olarak alınmıştır. Property değerine de direkt olarak atama yapma işlemlerini de gerçekleştirebilmekteyiz. Spring’in buna erişmesi için de o property ifadeleri için setter bir metot tanımı yapılması gerekmektedir.

public class SmsNotificationServiceImpl implements NotificationService {

   private final SmsConfiguration smsConfiguration;

   public SmsNotificationServiceImpl(SmsConfiguration smsConfiguration) {
       this.smsConfiguration = smsConfiguration;
   }

   @Override
   public void sendNotification() {
       System.out.printf("Notification was sent by %s%n", smsConfiguration.smsProviderHost);
   }
}
public class EmailNotificationServiceImpl implements NotificationService {

   public String emailProviderHost;
   public Integer emailProviderPort;

   @Override
   public void sendNotification() {
       System.out.printf("Notification was sent by  %s : %d%n", emailProviderHost, emailProviderPort);
   }

   public void setEmailProviderHost(String emailProviderHost) {
       this.emailProviderHost = emailProviderHost;
   }

   public void setEmailProviderPort(Integer emailProviderPort) {
       this.emailProviderPort = emailProviderPort;
   }
}
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="smsConfiguration" class="services.notification.sms.configuration.SmsConfiguration"
          scope="singleton"
          lazy-init="true"/>

    <bean id="smsNotificationService" class="services.notification.sms.SmsNotificationServiceImpl"
          scope="singleton"
          autowire="constructor"
          lazy-init="false">
    </bean>

    <bean id="emailNotificationService" class="services.notification.email.EmailNotificationServiceImpl"
          scope="prototype"
          lazy-init="false">
        <property name="emailProviderHost" value="XYZ-Email-Provider-Host"/>
        <property name="emailProviderPort" value="571"/>
    </bean>
</beans>

Yukarıdaki XML içerisine bakıldığı zaman smsNotificationService id değerine sahip bean için autowire="constructor" ifadesi yer almaktadır diğer emailNotificationService id değerine sahip bean için de property olarak bir tag açılmış ve oradan ilgili değerler atanmıştır.

Annotation-based Configuration

Yazının başından bu yana kadar olan bütün kısımlarda uygulama içerisindeki; bütün bağımlılıkları, oluşturulma anındaki davranışları, parametre olarak aldıkları değerleri gibi işlemlerde  XML kullandık ve gerekli tanımlamaları da içlerinde gerçekleştirdik. Projede büyüdükçe bu XML içerisindeki değerlerin okunması ve bakımı zorlaşmaktadır bunların yanı sıra XML içerisindeki alınan hatalar da run-time esnasında fark edilecektir. Bu ve bunun gibi olumsuz özellikleri mevcuttur. Bağımlılıkların tanımlanması işlemlerini daha güvenli bir şekilde gerçekleştirmek için Annotation-based (anotasyon tabanlı) tanımlamalar da mevcuttur. Bu kısımda ise yukarıda XML içerisindeki gerçekleştirilen kısımların uygulama kodu içerisinde nasıl gerçekleştirildiği hakkında bilgiler edineceğiz.

Spring içerisindeki işlemleri anotasyon ile gerçekleştirmek için öncelikle XML içerisinde bu işlemi belirtmemiz gerekmektedir, Spring default olarak bu özelliği açmamaktadır. Bunun için XML dosyasında <context:annotation-config/> bulunmalıdır. Aşağıda temel XML dosyası yer almaktadır.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans

https://www.springframework.org/schema/beans/spring-beans.xsd


http://www.springframework.org/schema/context

       https://www.springframework.org/schema/context/spring-context.xsd">
    <context:annotation-config/>

</beans>

Aşağıda yukarıda gerçekleştirilen XML işlemlerindeki ifadeleri temsil etmesi amacıyla temel birkaç annotation yer almaktadır:

  • @Component
  • @Scope
  • @Lazy
  • @Required
  • @Autowired
  • @Qualifier

@Component

XML içerisinde tanımladığımız <bean /> etiketine sahip bağımlılıkları uygulama kodu içerisinde ifade etmek istediğimiz zaman kullandığımız bir anotasyontur.

<bean id="emailNotificationService" class="services.notification.email.EmailNotificationServiceImpl"
        scope="singleton"
        lazy-init="false">

Yukarıdaki XML içerisinde tanımlanan bean’in anotasyon olarak ifade edilmesi aşağıda yer almaktadır.

@Component
@Scope("singleton")
@Lazy(false)
public class EmailNotificationServiceImpl implements NotificationService {
    @Override
    public void sendNotification() {
        ...
    }
}

Yukarıdaki kod örneğine bakıldığı zaman @Scope ve @Lazy gibi anotasyonlar da görülmektedir. XML içerisinde de bahsedildiği gibi bunlar oluşturulma ve çağrılma anındaki davranışı değiştirmektedir.
@Required

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Required
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

Bu annotation, tanımlandığı yerin uygulama ayağa kalktığı anda değer atanmasını zorunlu hale getirir. Atanmadığı ya da çalışmadığı durumlarda run-time sırasında exception fırlatır.

@Autowired

Bean üzerindeki bağımlılıkların enjekte edilmesini istediğimizde kullanmaktayız. Bu bağımlılıklar genellikle uygulama içerisinde @Component olarak tanımladığımız yapılardır.

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

Yukarıda constructor tabanlı bir DI işlemi gerçekleştirilmiştir. Bunların yanı sıra geleneksel setter metotlar ile de kullanılabilmektedir.

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

Yukarıdaki 2 tanımda farklı olarak bağımlılıkların enjekte edilme yöntemleri gösterildi, bunların yanı sıra iki işlem de birlikte kullanılabilmektedir.

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

Bunlara ek olarak Collection, Map ve Array gibi yapılar ve daha fazlası için de kullanımı mevcuttur.

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    private Set movieCatalogs;

    private Map movieCatalogs;
    
    @Autowired
    public void setMovieCatalogs(Set movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    @Autowired
    public void setMovieCatalogs(Map movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }
    // ...
}

@Qualifier

Uygulama içerisinde bir arayüzü uygulayan (implementation) birden fazla sınıf olabilir, bu arayüzün çağrımı sırasında bunu uygulayan hangi sınıfın gelmesi gerektiğini belirlemek karmaşıklığa neden olabilmektedir. Aşağıda temel bir arayüz ve bu arayüzü uygulayan birkaç sınıf yer almaktadır.

/**
 * Notification service interface.
 */
public interface NotificationService {
    /**
     * Notify something.
     */
    void sendNotification();
}
public class EmailNotificationServiceImpl implements NotificationService {

    @Override
    public void sendNotification() {
        System.out.println("Notification was sent by email provider.");
    }
}
public class SmsNotificationServiceImpl implements NotificationService {

    @Override
    public void sendNotification() {
        System.out.println("Notification was sent by sms provider.");
    }
}

Yukarıda belirtilen durumda NotificationService arayüzü kullanılmak istenildiğinde buna karşılık hangi sınıfın geleceği belli değildir. Bu; EmailNotificationServiceImpl olabilir, SmsNotificationServiceImpl de. Bu tür durumlarda bizlerin bunları niteleyeceği bir şeyin olması gerekmektedir. Bunu da @Qualifier olarak adlandırmaktayız.

Sınıfların üst kısmına @Qualifier anotasyon eklenerek o sınıfa onu belirtecek bir isim verilir, o sınıfın uyguladığı arayüzü çağırırken de anotasyon içerisinde belirttiğimiz ismi kullanırız bu sayede ortadaki belirsizlik kalkmış olur.

Aşağıda, yukarıda tanımlanan sınıf ve arayüzlere ait Qualifier tanımı yer almaktadır.

/**
 * Notification service interface.
 */
public interface NotificationService {
    /**
     * Notify something.
     */
    void sendNotification();
}
@Qualifier("email")
public class EmailNotificationServiceImpl implements NotificationService {

    @Override
    public void sendNotification() {
        System.out.println("Notification was sent by email provider.");
    }
}
@Qualifier("sms")
public class SmsNotificationServiceImpl implements NotificationService {

    @Override
    public void sendNotification() {
        System.out.println("Notification was sent by sms provider.");
    }
}

Aşağıdaki sınıf içerisinde NotificationService arayüzü çağrılırken @Qualifier ile hangi sınıfın geleceği belirtiliyor.

public class ExampleServiceImpl {
    private NotificationService notificationService;

    public ExampleServiceImpl(@Qualifier("sms") NotificationService notificationService) {
        this.notificationService = notificationService;
    }
}

Yukarıda temel olarak XML ve Annotation-based şekilde bu işlemlerin nasıl gerçekleştirileceği konu anlatımı ve bolca kod örnekleri ile anlatıldı. XML ile yapılan uygulamanın aşağıda uygulama kodu ile gerçekleştirilmesi yer almaktadır.

/**
 * Notification service interface.
 */
public interface NotificationService {
    /**
     * Notify something.
     */
    void sendNotification();
}
@Component
@Qualifier("email")
@Scope("singleton")
@Lazy(false)
public class EmailNotificationServiceImpl implements NotificationService {

    @Override
    public void sendNotification() {
        System.out.println("Notification was sent by email provider.");
    }
}
@Component
@Scope("singleton")
@Lazy(false)
public class SmsConfiguration {
    public String smsProviderHost = "XYZ-Sms-Provider-Host";
}
@Component
@Qualifier("sms")
@Scope("singleton")
@Lazy(false)
public class SmsNotificationServiceImpl implements NotificationService {

    private final SmsConfiguration smsConfiguration;

    @Autowired
    public SmsNotificationServiceImpl(SmsConfiguration smsConfiguration) {
        this.smsConfiguration = smsConfiguration;
    }

    @Override
    public void sendNotification() {
        System.out.println("Notification was sent by " + this.smsConfiguration.smsProviderHost);
    }
}
@Component
public class App {

    public final NotificationService notificationService;

    public App(@Qualifier("email") NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.scan("services");
        applicationContext.register(App.class);

        applicationContext.refresh();

        App appBean = applicationContext.getBean(App.class);
        appBean.notificationService.sendNotification();
    }
}

Yukarıda tanımlanan App sınıfı içerisindeki ifadelere bakıldığında farklı bir sınıf ve bu sınıfa bağlı davranış söz konusudur. Uygulama içerisindeki XML bağımlılığını kaldırdığımızdan dolayı XML işlemleri sırasında yer alan ClassPathXmlApplicationContext sınıfına olan bağımlılık da ortadan kalktı. Bunun yerine AnnotationConfigApplicationContext sınıfını kullanmaktayız.

AnnotationConfigApplicationContext sınıfının, dökümantasyon üzerindeki API’sine bakıldığı zaman; BeanFactoryBeanDefinitionRegistryAnnotationConfigRegistryApplicationContext gibi arayüzleri uyguladığı görülmektedir. Buradan da anlaşılacağı gibi bu sınıf bizlere bean’ler ile ilgili işlemleri zaten sağlamaktadır buna ek olarak package içerisini tarama, ilgili bağımlılıkları kod içerisinden kayıt etme (register) gibi işlemleri gerçekleştirmemize olanak sağlamaktadır.

Yukarıdaki koda bakıldığı zaman scan ile hangi paket içerisi taranacağının belirtildiği, buna ek olarak da register ile hangi sınıfların kod içerisinde manuel olarak enjekte edileceği belirtilmiştir. Bu gibi kayıt işlemlerinden sonra ilgili container’in yeniden başlatılması için refresh metotu kullanılmıştır daha sonrasında ise normal servis çağırma işlemi gerçekleştirilmektedir.

Yukarıdaki uygulamaya ait çıktı;

Notification was sent by email provider.

App sınıfı içerisindeki NotificationService, constructor içerisinde @Qualifier("email") olarak işaretlendiğinden dolayı EmailNotificationServiceImpl çalışacaktır.


Yazı içerisinde;

  • Inversion of Control Prensibi Nedir?
  • Dependency Inversion Prensibi Nedir?
  • Dependency Injection Deseni Nedir?
  • Spring Framework Nedir?
  • Spring Framework ile Dependency Injection Nasıl Yapılır?
  • XML ve Annotation-based Dependency Injection Nasıl Yapılır?

gibi soruların, diğer konuların anlatımı ve bolca kod örnekleri ile bilgiler aktarıldı.

Kaynak: https://devnot.com/2021/spring-framework-ile-dependency-injection/