Spring Framework / Boot Core Annotations

Screenshot

Merhaba. Bu yazıda Spring Framework/Boot ‘un annotationlarını anlatmaya çalışacağım. Bu bölüm belki iki veya üç yazıya bölünebilir.

Spring Framework Java için geliştirilmiş açık kaynak kodlu uygulama geliştirme için kullanılan frameworktür. Kotlin dili ile de kullanmak mümkündür. Sanılanın aksine Spring Framework sadece web geliştirme için kullanılmaz ve Spring programcının kod kalitesine karışmaz. Spring programcının üzerinden bazı yükleri alır.

Bir Spring projesini açtığımızda karşımıza “@SpringBootApplication” annotation’ı ile işaretlenmiş bir main metodu çıkar. Bu annotation main’de veya run metodunun çağırıldığı sınıfta olur. SpringApplication.run metodunun parametresi ile aldığı Class<?> türünden nesne ile Reflection’dan yararlanarak başlangıçta bizim için konfigürasyonlar yapar.

@SpringBootApplication
public class DemoApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication üç annotation’ın birleşimidir. Bunlar @SpringBootConfiguration, @ComponentScan, @EnableAutoConfigurationannotationlarıdır. Spring’i başlatan sınıfın konumu önemlidir, çünkü default olarak Reflection ile arama yaparken kendi içinde bulunduğu paket ve kapsanan paketleri arar. Üst dizinde kalan paketlere bakmaz.

@Component, @Lazy ve @PostConstruct annotation:

Component annotation’ı bizim için nesne yaratır. Bir nesnenin ne zaman yaratılacağına o bean’in scope’u karar verir. Eğer scope’u belirtmez isek deafult olarak Singleton scope ile yaratılır ve buna ek olarak Lazy annotation’ı ile belirtilmez ise initilization noktasında yaratılır. @Lazy annotation’ı gerektiğinde, istendiğinde yaratmak anlamına gelir.

@Component
public class DateTimePrinter
{
    public DateTimePrinter()
    {
        System.out.println("DateTimePrinter");
    }
}

Screenshot

@Component annotation

Yukarıdaki örnekte görüldüğü gibi sadece Component annotation’ı kullanarak nesnenin initilization noktasında yaratılmış olduğunu gördük.

Şimdi bir de @Lazy annotation’ı kullanarak yapalım.

@Component
@Lazy
public class DateTimePrinter
{
    public DateTimePrinter()
    {
        System.out.println("DateTimePrinter");
    }
}

Screenshot

@Component with @Lazy

@Lazy annotation ile belirttiğimiz için nesnemiz initilization noktasıda yaratılmadı. Nesne yaratıldıktan hemen sonra yapılmasını istediğimiz şeyler için@PostConstruct annotation’ı kullanabiliriz. Örneğin aşağıdaki örneğimizde DateTimePrinter türünden nesnemiz yaratıldıktan hemen sonra tarih ve saat bilgisini yazdıralım.

@Component
public class DateTimePrinter
{
    public DateTimePrinter()
    {
        System.out.println("\nDateTimePrinter");
    }
    @PostConstruct
    private void printDateTime()
    {
        var formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy kk:mm:ss");
        System.out.println(formatter.format(LocalDateTime.now()) + "\n");
    }
}

Screenshot

Burada dikkat etmemiz gereken, PostConstruct annotation Spring’in kendi annotation’ı değildir. Bu annotation “javax.annotation” paketinden gelmektedir.

Şimdi de DateTimePrinter sınıfı gibi 2 tane sınıf daha yazalım ve bunların her birinin görevi ayrı olsun. DatePrinter, TimePrinter ve konsola ikisini birden bastıran DateTimePrinter sınıfları.

@Component
public class DatePrinter
{
    public DatePrinter()
    {
        System.out.println("\nDatePrinter");
    }
    @PostConstruct
    private void printDate()
    {
        var formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        System.out.println(formatter.format(LocalDate.now()) + "\n");
    }
}
@Component
public class TimePrinter
{
    public TimePrinter()
    {
        System.out.println("\nTime Printer");
    }
    @PostConstruct
    private void printTime()
    {
        var formatter = DateTimeFormatter.ofPattern("kk:mm:ss");
        System.out.println(formatter.format(LocalTime.now()) + "\n");
    }
}

Şimdi çalıştıralım ve hepsini birlikte görelim.

Screenshot

Hepsi Component annotation ile işaretlendi ve başlangıçta yaratıldı. Fakat yaratılma sırası neye göre? Component isim sırası ile çağırılıyor. Eğer component’ın ismi değiştirilmediyse default olarak sınıfın isminin küçük harfle başlanan halidir. (DateTimePrinter ise dateTimePrinter gibi).

@Scope annotation

Scope annotation’ına baktığımızda iki tane @AliasFor annotation’ı görürüz. Bunlar “scopeName”ve default olarak gelen “value”dur. Bunların ikisi de genelde aynı işlevi görür. Böyle yapılmasının sebeplerinden biri de okunabilirliği arttırmaktır. Yani biz @Scope(“prototype”) demek ile @Scope(scopeName = “prototype”) demek arasında bir fark yoktur. Scope annotationı default olarak Singletonşeklinde gelir. Yani @Scope ile @Scope(“singleton”) aynı anlama gelmektedir. Diğer bir scope türü de “prototype” tır. Bu ise başlangıçta bir kere yaratılıp arka plana atılmak yerine her istendiğinde yeni bir nesne yaratır.

@Component
@Scope("prototype")
public class DatePrinter
{
    public DatePrinter()
    {
        System.out.println("\nDatePrinter");
    }
    @PostConstruct
    private void printDate()
    {
        var formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        System.out.println(formatter.format(LocalDate.now()) + "\n");
    }
}

Yukarıdaki örnekte DatePrinter sınıfının Scope’unu prototype olarak işaretledik. Şimdi çalıştırdığımızda yaratılmadığını görüyoruz.

Screenshot

@Component annotation ile işaretlenen sınıfı ayrıca @Scope(“singleton”) olarak işaretlememize gerek yoktur. Çünkü başta yazdığımız gibi @Component annotation da deafult olarak singleton olarak çalışır.

@Bean ve @Configuration Annotation

Yukarıdaki açıklamalarımızda prototype için istendiğinde çağırılır dedik peki bu istendiğinde ne anlama geliyor? Nasıl bir sınıfın yaratılmasını isteriz?

Burada karşımıza @Bean annotation’ı çıkıyor. @Bean annotation’ı bir nesneyi yaratmak, konfigüre etmek ve Spring IoC tarafından kullanılabilmesini sağlar. @Bean annotation’lı metodları herhangi bir @Component annotation ile kullanabilirsiniz. @Bean annotation’ı genellikle @Configuration annotation’ı ile birlikte kullanılır. Bir sınıfa @Configuration annotation’ı eklemek o sınıfın bean kaynağı olduğunu gösterir. Ayrıca @Configuration annotation ile işaretlenen sınıflar aynı sınıftaki diğer @Bean metodlarını çağırarak bean’ler arası bağımlılıklarının tanımlanmasına izin verir. @Configuration annotation’ı @Component annotation ile işaretlendiğinden dolayı @Configuration ile işaretlediğimiz sınıfları ayrıca @Component olarak işaretlememize gerek yoktur.

@Bean annotationları genellikle dependency injection kavramı ile birlikte kullanılır.

Spring’te 3 farklı şekilde dependency injection yapabiliriz.

1 - Constructor injection

2 - Field Injection

2 — Setter Injection

Constructor Injection

Örneğin yukarıdaki sınıflarımızı tekrardan yazalım. Bu sefer Printer sınıfların Configuration sınıfları olsun. Örnek olarak DatePrinter ve DateConfiguration sınıflarına bakalım.

@Component
public class DatePrinter
{
    private final DateTimeFormatter m_dateTimeFormatter;
    private final LocalDate m_locaDate;
    public DatePrinter(DateTimeFormatter m_dateTimeFormatter, LocalDate m_locaDate)
    {
        this.m_dateTimeFormatter = m_dateTimeFormatter;
        this.m_locaDate = m_locaDate;
    }
    @PostConstruct
    private void printDate()
    {
        System.out.println("Today date: " + m_dateTimeFormatter.format(m_locaDate));
    }
}
@Configuration
public class DateConfiguration
{
    @Bean
    public DateTimeFormatter getDateTimeFormatter()
    {
        return DateTimeFormatter.ofPattern("dd/MM/yyyy");
    }
    @Bean
    public LocalDate getDate()
    {
        return LocalDate.now();
    }
}

Çalıştırdığımızda çıktımız şöyle olacaktır.

Screenshot

Burada @Bean annotation ile DatePrinter türünden yaratılacak nesnemiz için DateTimeFormatter ve LocalDate türünden nesneleri initilization noktasında yaratıyoruz ve Spring bizim için DatePrinter türünden nesne yaratılırken dependency injection yapıyor. (Scope’u prototype yapsaydık istendiği zaman yaratılıp çağırılacaktı) Burada Constructor’da @Autowired annotation kullanmadık, çünkü Spring’in belirli bir sürümünden sonra tek bir constructor için @Autowired zorunluluğu kalktı.

Şimdi DatePrinter’ın yanına DateTimePrinter ve TimePrinter ekleyelim. Fakat burada şöyle bir nokta var. Bunların üçü de farklı formatlarda yazdırılacak.

Date formatı: dd/MM/yyyy

DateTime formatı: dd/MM/yyyy kk:mm:ss

Time formatı: kk:mm:ss

Biz bu üç sınıf için de configuration sınıflarında DateTimeFormatter nesnesine dönen metodu yazdık diyelim. Spring bunlardan hangisine hangi metodu vereceğini bilemez (Multiple Bean), çünkü türleri aynı. Bunun için @Bean annotation’ın value değerine o bean için bir isim girebiliriz. Bu bean’i verdiğimiz isimle çağırmak için de @Qualifier annotation’ı kullanabiliriz. Bu annotation aldığı value değeri ile (verdiğimiz bean ismi) o bean’i bulur ve dependency injection için onu kullanır.

@Component
public class DatePrinter
{
    private final DateTimeFormatter m_dateTimeFormatter;
    private final LocalDate m_locaDate;
    public DatePrinter(@Qualifier("date.formatter") DateTimeFormatter m_dateTimeFormatter, LocalDate m_locaDate)
    {
        this.m_dateTimeFormatter = m_dateTimeFormatter;
        this.m_locaDate = m_locaDate;
    }
    @PostConstruct
    private void printDate()
    {
        System.out.println("Date: " + m_dateTimeFormatter.format(m_locaDate));
    }
}
@Configuration
public class DateConfiguration
{
    @Bean("date.formatter")
    @Scope("prototype")
    public DateTimeFormatter getDateTimeFormatter()
    {
        return DateTimeFormatter.ofPattern("dd/MM/yyyy");
    }
    @Bean
    @Scope("prototype")
    public LocalDate getDate()
    {
        return LocalDate.now();
    }
}
@Component
public class DateTimePrinter
{
    private final DateTimeFormatter m_dateTimeFormatter;
    private final LocalDateTime m_localDateTime;
    public DateTimePrinter(LocalDateTime m_localDateTime, @Qualifier("datetime.formatter") DateTimeFormatter dateTimeFormatter)
    {
        this.m_dateTimeFormatter = dateTimeFormatter;
        this.m_localDateTime = m_localDateTime;
    }
    @PostConstruct
    private void printDateTime()
    {
        System.out.println("DateTime: " + m_dateTimeFormatter.format(m_localDateTime));
    }
}
@Configuration
public class DateTimeConfiguration
{
    @Bean("datetime.formatter")
    @Scope("prototype")
    public DateTimeFormatter getDateTimeFormatter()
    {
        return DateTimeFormatter.ofPattern("dd/MM/yyyy kk:mm:ss");
    }
    @Bean
    @Scope("prototype")
    public LocalDateTime getToday()
    {
        return LocalDateTime.now();
    }
}
@Component
public class TimePrinter
{
    private final DateTimeFormatter m_dateTimeFormatter;
    private final LocalTime m_localTime;
    public TimePrinter(@Qualifier("time.formatter") DateTimeFormatter m_dateTimeFormatter, LocalTime m_localTime)
    {
        this.m_dateTimeFormatter = m_dateTimeFormatter;
        this.m_localTime = m_localTime;
    }
    @PostConstruct
    private void printDate()
    {
        System.out.println("Time: " + m_dateTimeFormatter.format(m_localTime));
    }
}
@Configuration
public class TimeConfiguration
{
    @Bean("time.formatter")
    @Scope("prototype")
    public DateTimeFormatter getDateTimeFormatter()
    {
        return DateTimeFormatter.ofPattern("kk:mm:ss");
    }
    @Bean
    @Scope("prototype")
    public LocalTime getTime()
    {
        return LocalTime.now();
    }
}

Screenshot

Field Injection

Field injection yapmak için @Autowired annotation kullanırız. Bu sefer DatePrinter sınıfını Field Injection yapacak şekilde değiştirelim.

@Component
public class DatePrinter
{
    @Autowired
    @Qualifier("date.formatter")
    private DateTimeFormatter m_dateTimeFormatter;
    @Autowired
    private LocalDate m_locaDate;
    @PostConstruct
    private void printDate()
    {
        System.out.println("Date: " + m_dateTimeFormatter.format(m_locaDate));
    }
}

DateConfiguration sınıfında herhangi bir değişiklik yapmadık. Field injection’ı gerek olmadıkça kullanmamalıyız yada çok basit işlemler için kullanmalıyız. Zaten statik kod analizi aracı da önermiyor.

Screenshot

Bunun nedenleri olarak final veri tipi kullanamamak ve çok fazla eleman olduğunda karmaşıklığa yol açması verilebilir.

Setter Injection

Şimdi de TimePrinter sınıfını setter injection olacak şekilde düzenleyelim.

@Component
public class TimePrinter
{
    private DateTimeFormatter m_dateTimeFormatter;
    private LocalTime m_localTime;
    @Autowired
    public void setDateTimeFormatter(@Qualifier("time.formatter") DateTimeFormatter formatter)
    {
        m_dateTimeFormatter = formatter;
    }
    @Autowired
    public void setLocalTime(LocalTime localTime)
    {
        m_localTime = localTime;
    }
    @PostConstruct
    private void printDate()
    {
        System.out.println("Time: " + m_dateTimeFormatter.format(m_localTime));
    }
}

Field injection’da final yapamıyoruz diye önermedik. Peki burada niye bir şey demedik? Çünkü Field injection’da final yapabilecekken final yapamıyoruz ama setter injection’da senaryo gereği yapmak gerekebilir.

Şimdi DateTime’ı saniyede bir güncelleyen schedulerTask yazalım.

@Component
public class PeriodicDateTimePrinter
{
    private final LocalDateTime m_localDateTime;
    private final DateTimeFormatter m_dateTimeFormatter;
    public PeriodicDateTimePrinter(LocalDateTime m_localDateTime,
        @Qualifier("datetime.formatter") DateTimeFormatter dateTimeFormatter)
    {
        m_dateTimeFormatter = dateTimeFormatter;
        m_localDateTime = m_localDateTime;
    }
    private TimerTask timerTask()
    {
        return new TimerTask()
        {
            @Override
            public void run()
            {
                System.out.printf("Scheduler: %s\r", m_dateTimeFormatter.format(m_localDateTime));
            }
        };
    }
    @PostConstruct
    private void startScheduler()
    {
        var timer = new Timer();
        timer.schedule(timerTask(), 0, 1000);
    }
}

Bu kodu çalıştırdığımızda zamanın güncellenmediğini görürüz, çünkü PeriodicDateTimePrinter nesnesi bir defa yaratılıyor ve yaratılırken DateTimeConfiguration içerisinde @Scope(“prototype”) olarak işaretli LocalDateTime nesnesine geri dönen getToday isimli metot var ama bu metod ta PeriodicDateTimePrinter nesnesi init edilirken çağırılıp yaratılıyor ve biz sürekli aynı nesneyi kullanıyoruz dolayısıyla güncellenmiyor. Bunu daha kolay anlamak için DateTimeConfiguration içindeki getToday metodune bir print yazabilirsiniz. Bunun çözümü için otomatik olarak enjekte edilen ApplicationContext sınıfını kullanırız ve getBean isimli metoduyla istediğim bean’i istediğim zaman alabilirim.

@Component
public class PeriodicDateTimePrinter
{
    private final ApplicationContext m_apApplicationContext;
    private final DateTimeFormatter m_dateTimeFormatter;
    public PeriodicDateTimePrinter(ApplicationContext applicationContext,
        @Qualifier("datetime.formatter") DateTimeFormatter dateTimeFormatter)
    {
        m_dateTimeFormatter = dateTimeFormatter;
        m_apApplicationContext = applicationContext;
    }
    private TimerTask timerTask()
    {
        return new TimerTask()
        {
            @Override
            public void run()
            {
                var dateTime = m_apApplicationContext.getBean(LocalDateTime.class);
                System.out.printf("Scheduler: %s\r", m_dateTimeFormatter.format(dateTime));
            }
        };
    }
    @PostConstruct
    private void startScheduler()
    {
        var timer = new Timer();
        timer.schedule(timerTask(), 0, 1000);
    }
}

Şimdi çalıştırdığımızda ise her saniye zaman güncellenir.

Kaynak:

  • C ve Sistem Programcıları Derneği, Oğuz Karan’ın ders videosu.
  • Spring Framework Dökümantasyon

© 2026 Nuri Can Öztürk