Spring Framework Fundamentals

  • Spring en basit bir tabir ile Dependency Injection Container olarak tanımlanabilir. Bu içeriği okumayı bitirince ne olduğunu daha iyi anlayacaksınız.

Dependency Injection Nedir ???#

  • Diyelim ki yazacağın class database access yapacak. Yani bir DAO. Diyelim ki parametrede alınan bir id ile size bir User döndürüyor.
public class UserDao {

    public User findById(Integer id) {
        // execute a sql query to find the user
    }
}
  • Şimdi bu işlemi gerçekleştirebilmek için en basit şekilde bir Connection Pool‘a ihtiyacımız var.
import javax.sql.DataSource;

public class UserDao {

    public User findById(Integer id) throws SQLException {
        try (Connection connection = dataSource.getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // use the connection etc.
        }
    }

}
  • Görüldüğü gibi DB query isini işletebilmek için bir DataSource objesi kullanarak DB’ye bağlandık ve bir query çalıştırdık.

  • Burada bir sıkıntı var. Bu UserDao class’ı DataSource nesnesi olmadan çalışmayacak. Basitçe bu sorunu her seferinde method çağırıldığında new ile yeni bir DataSource nesnesi oluşturarak çözebilirdik.

import com.mysql.cj.jdbc.MysqlDataSource;

public class UserDao {

    public User findById(Integer id) {
        MysqlDataSource dataSource = new MysqlDataSource(); // (1)
        dataSource.setURL("jdbc:mysql://localhost:3306/testDB");
        dataSource.setUser("root");
        dataSource.setPassword("password");

        try (Connection connection = dataSource.getConnection()) { // (2)
             PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
             // execute the statement..convert the raw jdbc resultset to a user
             return user;
        }
    }
}
  • Bu işlem sıkıntıyı çözdü ama ya bir tane daha methoda ihtiyacım olursa ne olacak ? Onun içinde de mi her zaman DataSource newleyeceğim ?

  • Bu sorunu bir tane utility fonksiyonu gibi sadece bir DataSource almak için kullanabileceğimiz bir fonksiyona ayırabiliriz.

import com.mysql.cj.jdbc.MysqlDataSource;

public class UserDao {

    public User findById(Integer id) {
        try (Connection connection = newDataSource().getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select , handle exceptions, return the user
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = newDataSource().getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select ,  handle exceptions, return the user
        }
    }

    public DataSource newDataSource() {
        MysqlDataSource dataSource = new MysqlDataSource(); // (3)
        dataSource.setUser("root");
        dataSource.setPassword("password");
        dataSource.setURL("jdbc:mysql://localhost:3306/testDB");
        return dataSource;
    }
}
  • Bu işlem şimdilik bizi kurtardı. Ama ya ilerleyen bir zamanda başka bir DAO ProductDao oluşturulacak olsa. Ne olacak o zaman ?

  • Ayrıyeten buradaki her method için yepyeni birer Connection açılıyor. Bu gerçekten gerekli mi ? Gereksiz kaynak kullanmıyor muyuz ?

Dependencies Controllerini Global Bir yapıya çıkartmayı deneyelim#

import com.mysql.cj.jdbc.MysqlDataSource;

public enum Application {

    INSTANCE;

    private DataSource dataSource;

    public DataSource dataSource() {
        if (dataSource == null) {
            MysqlDataSource dataSource = new MysqlDataSource();
            dataSource.setUser("root");
            dataSource.setPassword("password");
            dataSource.setURL("jdbc:mysql://localhost:3306/testDB");
            this.dataSource = dataSource;
        }
        return dataSource;
    }
}
  • Buradaki gibi bir Singleton yapı kurarak, ne zaman bir DataSource a yani Connection‘a ihtiyacım olursa buradan alabileceğiz.

Şimdi UserDao‘muzu bu yeni Singleton yapı ile güncelleyelim.

import com.yourpackage.Application;

public class UserDao {
    public User findById(Integer id) {
        try (Connection connection = Application.INSTANCE.dataSource().getConnection()) { // (1)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select etc.
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = Application.INSTANCE.dataSource().getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select etc.
        }
    }
}
  • Artık her bir fonksiyon için yeni bir Connection açma durumunu engellemiş olduk.
  • Ayrıca artık UserDao kendi DataSource nesnesini oluşturmak zorunda değil. Ne zaman ihtiyacı olursa, alacağı yeri biliyor. Ancak methodları işletebilmek için hala DataSource nesnesine ihtiyacı var.
  • Ve program büyümeye devam ettikçe bir sürü Depencency’ler oluştuktan sonra bir yerden sonra bunların ucunu kaçırabiliriz.

Inversion of Control (IoC)#

Şimdi olayı bir boyut öteye taşıyalım.

  • UserDao nun Connection‘ı alma gibi bir derdi olmasa, veritabanı işlemlerini kullanmak isteyen her class UserDao‘ya DataSource‘u gönderse nasıl olur ?

  • Bu olayın adı Inversion of Control‘dür. Yani eğer benim methodlarımı kullanmak istiyorsan bana bir DataSource versen iyi olur xd

import javax.sql.DataSource;

public class UserDao {

    private DataSource dataSource;

    private UserDao(DataSource dataSource) { // (1)
        this.dataSource = dataSource;
    }

    public User findById(Integer id) {
        try (Connection connection = dataSource.getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where id =  ?");
               // TODO execute the select etc.
        }
    }

    public User findByFirstName(String firstName) {
        try (Connection connection = dataSource.getConnection()) { // (2)
               PreparedStatement selectStatement = connection.prepareStatement("select * from users where first_name =  ?");
               // TODO execute the select etc.
        }
    }
}
  • Yani UserDao kullanmak isteyen bir class aşağıdaki kullanacak.
public class MyApplication {

    public static void main(String[] args) {
        UserDao userDao = new UserDao(Application.INSTANCE.dataSource());
        User user1 = userDao.findById(1);
        User user2 = userDao.findById(2);
        // etc ...
    }
}
  • Çok daha okunaklı oldu değil mi ?

Dependency Injection Containerları#

Bu senaryo boyunca bizim yerimize DataSource ve UserDao classımızı bizim yerimize oluşturuna hatta ve hatta UserDao’nun DataSource nesnesine ihtiyacı olduğunu önceden bilip, bize hazıt bir DataSource nesnesi veren bir yardımcı olsa ne güzel olurdu dimi ?

  • İşte Spring tam olarak bu işi yapıyor. Yazının en başında demiştik. Dependency Injection ve Inversion of Control

Ama Spring bu verileri neyi nasıl yapacağını nerden bilecek ?

  • Bunlar içinde Application Context adında bir container’ı vvar Springin.

Yani springin sizin için sağlaması gereken nesneleri siz bu Application Context içinde bulunduruyorsunuz ve Spring bu tüm Dependency Injection ve Inversion of Control olaylarını sizin için hallediyor. Geriye size kod yazmak kalıyor.

Aşağıda Application Context örneği mevcut

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import javax.sql.DataSource;

public class MyApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(someConfigClass); // (1)

        UserDao userDao = ctx.getBean(UserDao.class); // (2)
        User user1 = userDao.findById(1);
        User user2 = userDao.findById(2);

        DataSource dataSource = ctx.getBean(DataSource.class); // (3)
        // etc ...
    }
}
  1. ApplicationContext bizim ihtiyacımız olan hazır (ayarlanmış) UserDao nesnesini bize veriyor. Unutmayalım UserDaonun içinde de bir DataSource nesnesine ihtiyaç vardı. Spring bu nesneyi de oluşturdu ve bu UserDao nesnesine koydu.

  2. Spring ayrıca DataSource nesnesini de bize verebiliyor. Bu DataSource nesnesi UserDao nun içinde tanımlana nesnenin bire bir aynısı.

Springin yaptıkları çok güzel değil mi ? Peki bunları nasıl yapıyor ?

  • Yukarda someConfigClass olarak tanımladığımız nesnenin içeriğine bir bakalım
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyApplicationContextConfiguration {  // (1)

    @Bean
    public DataSource dataSource() {  // (2)
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("password");
        dataSource.setURL("jdbc:mysql://localhost:3306/testDB");
        return dataSource;
    }

    @Bean
    public UserDao userDao() { // (3)
        return new UserDao(dataSource());
    }

}
  1. @Configuration anotasyonu

  2. İstenilen DataSource nesnesini dönen ve @Bean anotasyonu ile biçimlendirilmiş bir method.

  3. Tanımlı başka bir @Bean anotasyonlu methodu kullanan başka bir method.

Başka şekillerde ApplicationContext tanımlayabilir miyiz ?#

  • Evet kesinlikle başka şekillerde de (xml) context tanımları yapabilirsiniz.
  • Ancak benim tavsiyem @Configuration ile kullanmanız. Çünkü xml karışık olabilri.
	<bean id="country" class="com.test.Country">
		<constructor-arg index="0" value="India"></constructor-arg>
		<constructor-arg index="1" value="20000"></constructor-arg>
	</bean>
  • Eğer yukarıdaki gibi bir bean tanımlaması yapacak olsaydınız, main clasınızda AnnotationConfigApplicationContext yerine ClassPathXmlApplicationContext kullanmanız ve bu xml dosyasını göstermeniz gerkecekti.

@Bean nedir ne işe yarar ? Spring Bean tam olarak nedir ?#

Springe tanıttığımız UserDao ve DataSource nesneleri artık birer Bean ve bu Bean lerin kontrolü tamamen springe ait.

Ama şimdi şöyle bir durum söz konusu. Bu bean ler hep aynı olarak mı kalacak bir lifecycle ları var mı ?

Spring Bean Scope’ları#

  • UserDao nesnemizden Spring kaç tane oluşturumalı ? Bu gibi soruları cevaplayabilmek için Bean Scope’ları bilmemiz gerekir.

  • Spring bir tane mi bean yaratmalı (Singleton) ? Tüm DAO sınıfları aynı bean imi kullanacak ? -> Yani oluşturulan bean den bir instance olarak ApplicationContextte saklanacak.

  • Yoksa spring her bir DAO için ayrı bean’ler mi oluşturmalı (Prototype) ? -> Her bir bean request için yeni bir bean Spring tarafından oluşturulup bize sağlanacaktır.

  • Ya da daha komplex durumlar mı söz konusu, örneğin: Her bir HTTP Request için bir DataSource bean gelsin gibi mi (Session) ? -> Her bir HTTP request için ayrı bir bean oluşturulacak.

Default bean scope Singleton dur.

@ComponentScan Annotation#

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyApplicationContextConfiguration {

    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("password");
        dataSource.setURL("jdbc:mysql://localhost:3306/testDB");
        return dataSource;
    }

    @Bean
    public UserDao userDao() { // (1)
        return new UserDao(dataSource());
    }

}

Yukarıdaki örnekte neden userDao içine direk bir dataSource() methodunu çağırıyoruz ? Bunu da Spring kullanarak yapamaz mıyız ?

  • İşte bu noktada Spring’in @ComponentScan anotasyonu devreye giriyor.

Kodumuzu aşağıdaki şekilde güncelleyelim.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class MyApplicationContextConfiguration {

    @Bean
    public DataSource dataSource() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("password");
        dataSource.setURL("jdbc:mysql://localhost:3306/testDB");
        return dataSource;
    }

    // no more UserDao @Bean method!
}
  1. Görüldüğü üzere nesnemize @CompnentScan anotasyonu eklendi.

  2. Daha önce @Bean ile tanımlamış olduğumuz userDao beanimiz silindi.

Peki bu ne anlama geldi ?#

  • @ComponentScan anotasyonu springe der ki : MyApplicationContextConfiguration configürasyon classımızın bulunduğu package’ın içindeki tüm spring bean’ine benzeyen bean’leri tara.

  • Yani eğer sizin MyApplicationContextConfiguration class’ınız com.atacankullabci package’ının içerisindeyse bu paketin içeriği spring tarafından olası spring bean’leri bulmak üzere taranacak.

Ama burada bir sorun var hala : Spring bir classın spring bean olarak alınması gerektiğini nereden bilecek ?

@Component ve @Autowired işlevleri ?#

Konfigürasyon classımızdan sildiğimiz userDao’yu aşağıdaki gibi güncelleyelim,

import javax.sql.DataSource;
import org.springframework.stereotype.Component;

@Component
public class UserDao {

    private DataSource dataSource;

    private UserDao(DataSource dataSource) { // (1)
        this.dataSource = dataSource;
    }
}
  1. Dikkat edeceğiniz gibi UserDao class’ımız @Component anotasyonu ile düzenlendi.

Bu şu demek : Hey, Spring eğer ComponentScan ile yönlerdirildiğin bir tarama olursa beni de gör. Ben senin tarafından bean olarak kullanılmak istiyorum !

  • (Daha önce Spring ile uğraşmış kişiler olacaktır. @Controller, @Service ve @Repository anotasyonları neden kullanmadık diyeceklerdir. Eğer @Component içeriğine bakarsanız bu üç anotasyonun da bu Component anotasyonundan implement edildiklerini göreceklerdir.)

Eveeet, şimdi puzzle’ın son parçası kaldı. private DataSource dataSource; ile tanımladığımız bu nesnenin, bizim konfigürasyon classımızdaki @Bean anotasyonlu DataSource’u olarak kullanmasını Spring’e bir şekilde anlatmamız gerekmekte.

  • Çok kolay, @Autowired anotasyonu da tam da bu işi yapacak. Son kodumuz aşağıdaki gibi olmalı.
import javax.sql.DataSource;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;

@Component
public class UserDao {

    private DataSource dataSource;

    private UserDao(@Autowired DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

Şimdi bu kadar kısa görünen bu kod içerisinde neler olup bittiğine bir bakalım;

  1. UserDAO @Component anotasyonuna sahip → Spring bizim için bunu oluşturacak.

  2. UserDAO contructorda @Autowired anotasyonuna sahip → Spring atomatik olarak bizim @Bean anotasyonu ile konfigüre ettiğimiz DataSource nesnesine inject edecek.

Son olarak, eski spring versiyonları için (4.2 ve altı) @Autowired anotasyonunu sprsifik bir şekilde belirtmeni gerekmektedir. Ancak yeni spring versiyonları için bunu ille de yapmanız gerek yok. Spring gerekli bean’i bulup inject edebilmektedir. Aşağıdaki iki örnek te aynı işlevi görecektir.

@Component
public class UserDao {

    private DataSource dataSource; // Spring @Autowired olmasa da inject edebiliyor.

    private UserDao(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
@Component
public class UserDao {

    @Autowired
    private DataSource dataSource;

}
  • Bir alternatif olarak Spring setter methodlarının üzerinde de inject edebilme olanağı sağlar.
import javax.sql.DataSource;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Autowired;

@Component
public class UserDao {

    private DataSource dataSource;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

}

Field vs Setter Injection#

  • İki yaklaşımın birbiri arasında hiçbir farkı yoktur. İkiside aynı sonucu elde etmenizi sağlayacaktır.

  • Ama programlarınız okunabilirliği için birini seçip onunla devam etmeniz önerilmektedir. Yani programınızın %40 ını field, %60 ınız setter injection olarak kullanmayınız.

  • Resmi Spring’s dökümantasyonu: Use constructor injection for mandatory dependencies and setter/field injection for optional dependencies. Be warned: Be really consistent with that.

Atacan KULLABCI © 2024