Spring Boot'ta @Scheduled Java Zamanlanmış Görevler ve Cron

Java SpringYayın: Güncellendi: 10 dk okumaHazırlayan: cronjob.com.tr Editörleri

Kurumsal Java ve Spring Boot ekosisteminde periyodik veri senkronizasyonu, önbellek (cache) tazeleme, gece saatlerinde finansal mutabakatların hesaplanması ve süresi dolmuş verilerin temizlenmesi gibi işlemler için yerleşik @Scheduled anotasyonu kullanılır. Harici bir işletim sistemi yapılandırmasına veya Quartz gibi ağır kütüphanelere ihtiyaç duymadan, doğrudan Spring IoC konteyneri içinde cron job tanımlamak bu anotasyon ile son derece pratiktir.

Ancak @Scheduled anotasyonunu üretim ortamında doğru kullanabilmek için Spring'in iç çalışma dinamiklerini bilmek kritik önem taşır. Spring cron ifadeleri klasik Linux crontab yapısından farklı olarak 6 alandan (saniye dahil) oluşur. Ayrıca varsayılan ThreadPoolTaskScheduler tek bir iş parçacığı (single-thread) üzerinde koşar; bu durum uzun süren bir görevin diğer tüm zamanlanmış işleri kilitlemesine neden olur. Çoklu pod mimarisine sahip Kubernetes veya Docker ortamlarında ise aynı görevin her sunucuda mükerrer çalışmasını engellemek için ShedLock gibi dağıtık kilit mekanizmaları gereklidir.

Bu kapsamlı kılavuzda; @EnableScheduling kurulumundan 6 alanlı Spring cron sözdizimine, fixedRate ile fixedDelay arasındaki davranış farklarından ThreadPoolTaskScheduler havuz yönetimine ve ShedLock kümeleme entegrasyonuna kadar tüm aşamaları adım adım inceleyeceğiz. Ayrıca mikroservislerde zamanlama karmaşasını ortadan kaldıran HTTP webhook tabanlı dış tetikleme modelini keşfedeceğiz.

Başka bir ekosistemde aynı gereksinimi çözüyorsanız .NET için Hangfire ve Quartz.NET karşılaştırması, kalıcı depolama ile kümeleme seçeneklerini ölçmek için iyi bir başlangıçtır. Daha küçük bir servis sürecinde zamanlama kuracaksanız Go cron paketleri ve goroutine kullanımı, Spring'in bean ve scheduler modelinden farklı bir yaklaşım sunar.

Spring Framework'te @Scheduled Anotasyonu ve Çalışma Mantığı

Spring Framework, zamanlanmış görevleri yönetmek için TaskScheduler arayüzünü (interface) kullanır. Bir Spring Bean'i içerisindeki metoda @Scheduled anotasyonu eklendiğinde, uygulama ayağa kalkarken Spring bu metodları tespit eder ve arka planda bir zamanlayıcı havuzuna kaydeder.

Spring'in resmi dokümantasyonuna göre zamanlanmış bir metodun karşılaması gereken parametre ve dönüş değeri davranışı şöyledir:

Adım Adım Kurulum: @EnableScheduling ve İlk Görevin Tanımlanması

Spring Boot projenizde zamanlayıcı mekanizmasını devreye almak için ek bir bağımlılık kurmanıza gerek yoktur; spring-boot-starter paketi bu özelliği varsayılan olarak içerir. Kurulum adımları şunlardır:

  1. Zamanlamayı Etkinleştirme: Ana konfigürasyon sınıfınıza veya @SpringBootApplication sınıfınıza @EnableScheduling anotasyonunu ekleyin:
    package com.example.cron;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication
    @EnableScheduling
    public class CronApplication {
        public static void main(String[] args) {
            SpringApplication.run(CronApplication.class, args);
        }
    }
  2. Servis veya Bileşen Tanımlama: Zamanlanmış görevi barındıran sınıfı @Component veya @Service olarak işaretleyerek Spring IoC konteynerine tanıtın.
  3. Görevi Anote Etme: İlgili iş kuralını içeren metodun üzerine @Scheduled anotasyonunu ve zamanlama parametresini yerleştirin.

Spring Cron İfadesi Yapısı (6 Alanlı Sözdizimi ve Linux Cron Farkı)

Spring Framework'ün benimsediği cron sözdizimi, standart Linux crontab sözdiziminden farklıdır. Standart Linux crontab 5 alandan oluşurken, Spring cron ifadeleri zorunlu olarak 6 alandan oluşur. İlk alan her zaman saniye değerini temsil eder.

Alan Sırası Zaman Birimi Geçerli Değerler Özel Karakterler
1Saniye (Second)0 - 59* , - /
2Dakika (Minute)0 - 59* , - /
3Saat (Hour)0 - 23* , - /
4Ayın Günü (Day of Month)1 - 31* , - / ? L W
5Ay (Month)1 - 12 veya JAN - DEC* , - /
6Haftanın Günü (Day of Week)0 - 7 (0 ve 7 Pazar) veya SUN - SAT* , - / ? L #

Spring cron ifadelerinde ? karakteri "belirli bir değer yok" anlamına gelir ve gün çakışmalarını önlemek için kullanılır. İfadelerin genel matematiksel mantığı için crontab sözdizimi sayfamıza başvurabilirsiniz.

// Örnek Spring Cron Desenleri
"0 0 3 * * *"       // Her gece saat 03:00:00'da
"0 */15 * * * *"    // Her 15 dakikada bir (dakika başlarında)
"0 0 9 * * MON-FRI" // Hafta içi her sabah 09:00:00'da
"0 0 0 1 * *"       // Her ayın ilk günü gece yarısı

fixedRate, fixedDelay ve cron Arasındaki Davranışsal Farklar

Spring Boot içerisinde periyodik zamanlama için üç farklı parametre bulunur. Projenizin iş kuralına göre doğru parametreyi seçmek yarış durumlarını (race condition) engeller:

Thread Pool Yapılandırması: Görevlerin Birbirini Kilitlemesini Önleme

Spring Boot'un ThreadPoolTaskScheduler için varsayılan ayarı, tek bir thread (boyutu 1 olan tekil iş parçacığı havuzu) tahsis etmektir; sanal thread yapılandırmasında farklı bir zamanlayıcı kullanılabilir. Eğer sisteminizde her saat başı çalışan ve 20 dakika süren ağır bir raporlama görevi varsa, o süre zarfında her 1 dakikada bir çalışması gereken sağlık kontrolü görevi bloke olur ve 20 dakika boyunca hiç çalışamaz!

Bu sorunu çözmek için uygulamanıza bir ThreadPoolTaskScheduler bean'i ekleyerek havuz boyutunu artırmalısınız:

package com.example.cron.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;

@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "15m")
public class SchedulerConfig {

    @Bean
    public LockProvider lockProvider(DataSource dataSource) {
        return new JdbcTemplateLockProvider(
            JdbcTemplateLockProvider.Configuration.builder()
                .withJdbcTemplate(new JdbcTemplate(dataSource))
                .usingDbTime()
                .build());
    }

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10); // Aynı anda 10 zamanlanmış görev paralel koşabilir
        scheduler.setThreadNamePrefix("spring-cron-thread-");
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(30);
        scheduler.initialize();
        return scheduler;
    }
}

Dağıtık Sistemlerde Tekil Yürütme (ShedLock Entegrasyonu)

Modern bulut mimarilerinde uygulamalar Kubernetes pod'ları veya Docker swarm üzerinde birden fazla kopya (replica) olarak yatayda ölçeklenir. Uygulamanız 4 pod olarak çalışıyorsa ve kodunuzda @Scheduled(cron = "0 0 3 * * *") varsa, gece saat 03:00'te 4 pod aynı anda aynı görevi koşturur! Bu durum müşterilere 4 kere fatura kesilmesine veya aynı e-postanın 4 kez gitmesine yol açar. Konteyner mimarilerinde zamanlama detayları için Docker cron job kılavuzumuza göz atabilirsiniz.

Bu problemi çözmenin sektör standardı ShedLock kütüphanesidir. ShedLock, ortak bir kilit deposunda (JDBC örneğinde veritabanı tablosunda) kilit kaydı açarak, kilit geçerli kaldığı sürece görevin tek bir pod tarafından çalıştırılmasını sağlar.

<!-- pom.xml Bağımlılıkları -->
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
    <version>5.10.0</version>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
    <version>5.10.0</version>
</dependency>

Veritabanınızda ShedLock için gerekli kilit tablosunu oluşturun:

-- PostgreSQL / MySQL Kilit Tablosu
CREATE TABLE shedlock (
    name VARCHAR(64) NOT NULL,
    lock_until TIMESTAMP NOT NULL,
    locked_at TIMESTAMP NOT NULL,
    locked_by VARCHAR(255) NOT NULL,
    PRIMARY KEY (name)
);

Gerçek Kod Örnekleri

Aşağıda ShedLock ile korunan, Türkiye saat diliminde hafta içi her sabah 09:00'da çalışan profesyonel bir Spring Boot servis örneği yer almaktadır:

package com.example.cron.service;

import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
public class FinansalMutabakatServisi {

    private static final Logger log = LoggerFactory.getLogger(FinansalMutabakatServisi.class);

    // Hafta içi her sabah 09:00'da Türkiye saatiyle çalışır
    @Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Europe/Istanbul")
    @SchedulerLock(
        name = "FinansalMutabakatLock",
        lockAtMostFor = "15m",  // Sunucu çökerse kilit en fazla 15 dk sonra düşer
        lockAtLeastFor = "5m"   // Görev 10 saniyede bitse bile en az 5 dk kilitli tutulur
    )
    public void gunlukMutabakatRaporu() {
        log.info("Finansal mutabakat başladı: {}", LocalDateTime.now());
        // Banka API'leri ile bakiye doğrulama mantığı
        log.info("Finansal mutabakat başarıyla tamamlandı.");
    }
}

Sık Karşılaşılan Hatalar ve Çözümleri

Spring Boot projelerinde zamanlanmış görevlerle çalışırken en çok karşılaşılan dört teknik hata ve çözüm yolları:

Only no-arg methods may be annotated with @Scheduled

Anotasyon eklenen metodun herhangi bir parametre kabul etmesi durumunda Spring başlatma anında bu hatayı fırlatır ve uygulama çökertilir. @Scheduled metodları parametre alamaz. İhtiyaç duyulan veriler sınıf alanları (fields) üzerinden veya @Autowired servislerinden temin edilmelidir.

Tek Thread Nedeniyle Görevlerin Birbirini Engellemesi

Uzun süren bir zamanlanmış görev başladığında diğer tüm görevlerin zamanı gelmesine rağmen çalışmaması durumudur. Varsayılan TaskScheduler havuzunun tek iş parçacıklı olmasından kaynaklanır. Çözüm yukarıda gösterilen ThreadPoolTaskScheduler bean'inin tanımlanmasıdır.

Spring Cron Parsing Error (5 Haneli Cron Girilmesi)

Geliştiricilerin Linux crontab alışkanlığıyla @Scheduled(cron = "0 3 * * *") şeklinde 5 alan tanımlaması sonucu ortaya çıkar. Spring cron 6 alan beklediği için hata verir. İfadenin başına saniye alanı eklenerek 0 0 3 * * * şeklinde düzeltilmelidir.

Kubernetes Pod'larında Mükerrer Veri Üretimi

Yatayda ölçeklenen konteynerlerde her pod'un bağımsız tetiklenmesi sonucu mükerrer kayıt oluşmasıdır. Çözüm ShedLock kütüphanesinin kurulması veya görevlerin harici bir web tetikleyicisiyle tekil çağrılmasıdır. Görevlerin hiç çalışmaması durumunda genel hata ayıklama için cron job çalışmıyor rehberimizi inceleyebilirsiniz.

cronjob.com.tr ile HTTP Tabanlı Alternatif Kurulum (Kubernetes Dostu Dış Tetikleme)

Kubernetes kümelerinde ShedLock için veritabanı kilit tabloları yönetmek, kilit sürelerini (lockAtMostFor / lockAtLeastFor) ince hesaplamak ve pod lifecycle problemlerini takip etmek operasyonel yük oluşturur. En temiz alternatif, Spring uygulamanızda güvenli bir REST uç noktası açıp dışarıdan periyodik HTTP POST çağrısı almaktır. Bu sayede uygulamanız kaç pod'a ölçeklenirse ölçeklensin, Ingress/Load Balancer her isteği tek bir pod'a yönlendirir; ayrı veya çakışan isteklerde mükerrer çalışmayı önlemek için yine kilit ve idempotent işlem tasarımı gerekir.

Spring Boot uygulamanızda gizli token korumalı bir controller oluşturun:

package com.example.cron.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/internal/cron")
public class CronWebhookController {

    @Value("${cron.secret.token}")
    private String beklenenToken;

    @PostMapping("/veri-temizligi")
    public ResponseEntity<String> veriTemizligiTetikle(
            @RequestHeader(value = "Authorization", required = false) String authHeader) {

        if (authHeader == null || !authHeader.equals("Bearer " + beklenenToken)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Yetkisiz erisim");
        }

        // Görevi asenkron olarak tetikleyin veya senkron yürütün
        return ResponseEntity.ok("Temizlik gorevi basariyla yurutuldu");
    }
}

cronjob.com.tr yönetim panelinde dakikalar içinde görevinizi yapılandırın:

ShedLock veya uygulama içi scheduler bağımlılıkları olmadan periyodik işleri dışarıdan yönetmenin avantajlarını cron alternatifleri rehberimizden inceleyebilirsiniz.

Sıkça Sorulan Sorular (SSS)

Spring @Scheduled metodları geri dönüş değeri döndürebilir mi?

Evet, parametresiz senkron metodlar bir değer döndürebilir; ancak zamanlayıcı bu dönüş değerini yok sayar. Dönüş tipinin void olması zorunlu değildir.

fixedRate ile fixedDelay farkı nedir?

fixedRate görevin başlangıç anları arasındaki süreyi temel alırken, fixedDelay bir önceki görevin tamamlanma anı ile bir sonraki görevin başlama anı arasındaki bekleme süresini belirtir.

Spring cron ifadesi Linux cron'dan nasıl ayrılır?

Standart Linux cron 5 alandan oluşurken, Spring cron ifadesi en başta zorunlu bir saniye alanı barındırır ve toplam 6 alandan (saniye, dakika, saat, gün, ay, haftanın günü) meydana gelir.

ShedLock ne işe yarar?

ShedLock, Kubernetes pod'ları veya çoklu sunucu kümelerinde ortak kilit deposunu kullanan görevlerin eşzamanlı çalışmasını, kilit süresi aşılmadığı sürece engeller.

Hemen Ücretsiz Cron Job Oluşturun

Spring Boot uygulamanızdaki token korumalı REST uç noktasını cronjob.com.tr ile beş alanlı ifade ve seçtiğiniz saat diliminde çağırın; yanıt kodunu ve yanıt süresini 7 gün boyunca inceleyin.

Ücretsiz Başla →

İlgili Rehberler