← Kursa Dön
📄 Text · 12 min

Bean Lifecycle (Yaşam Döngüsü)

Giriş

Spring container'daki her bean, tıpkı bir insan gibi, doğar (oluşturulur), büyür (konfigüre edilir), çalışır (kullanılır) ve sonunda ölür (yok edilir). Bu yaşam döngüsünü anlamak, kaynakları doğru yönetmek (veritabanı bağlantısı, dosya handle'ı, thread pool, cache) ve initialization mantığı yazmak için kritik öneme sahiptir.

Gerçek Dünya Analojisi

Bir restoranın açılış sürecini düşünün:

  1. İnşaat — Bina yapılır (bean instantiation)

  2. Donanım kurulumu — Mutfak ekipmanları, masalar yerleştirilir (dependency injection)

  3. Personel tanışması — Şef, garsonlar birbirini tanır (Aware callbacks)

  4. Son kontroller — Sağlık müfettişi gelir, her şeyi kontrol eder (BeanPostProcessor)

  5. Açılış hazırlıkları — Menü asılır, ilk yemekler hazırlanır (@PostConstruct)

  6. Resmi açılış — Müşteri kabul edilir (Bean kullanıma hazır)

  7. Kapanış prosedürü — Mutfak temizlenir, kapılar kilitlenir (@PreDestroy)

Neden Önemli?

  • Veritabanı connection pool'unu başlatma ve kapatma

  • Cache'i önceden doldurma (warm-up)

  • Dosya handle'larını ve network bağlantılarını temiz kapatma

  • Scheduled task'ları başlatma ve durdurma

  • 3rd-party kütüphane client'larını initialize etme


Yaşam Döngüsü Aşamaları — Tam Görünüm

1. Bean Definition Okunur
   ↓
2. Bean Instantiation (new ile nesne oluşturma)
   ↓
3. Dependency Injection (bağımlılıkların enjekte edilmesi)
   ↓
4. Aware Interface Callbacks
   (BeanNameAware, BeanFactoryAware, ApplicationContextAware)
   ↓
5. BeanPostProcessor.postProcessBeforeInitialization()
   ↓
6. Initialization Callbacks:
   a) @PostConstruct
   b) InitializingBean.afterPropertiesSet()
   c) @Bean(initMethod = "...")
   ↓
7. BeanPostProcessor.postProcessAfterInitialization()
   ↓
 ★ ★ ★  BEAN KULLANIMA HAZIR  ★ ★ ★
   ↓
8. Destruction Callbacks:
   a) @PreDestroy
   b) DisposableBean.destroy()
   c) @Bean(destroyMethod = "...")
   ↓
9. Bean Yok Edilir (GC tarafından)

Bu sıralamayı ezberlemeniz gerekmez — ama her adımın ne işe yaradığını bilmeniz gerekir.


@PostConstruct ve @PreDestroy — En Yaygın Kullanım

Lifecycle callback'lerin en temiz ve en çok önerilen yoludur. Jakarta EE standart annotation'larıdır, Spring'e özgü değildir:

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

@Service
public class CacheService {

    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    private final DataSource dataSource;

    public CacheService(DataSource dataSource) {
        this.dataSource = dataSource;
        // ⚠️ DİKKAT: Constructor'da bağımlılıklar henüz tam hazır olmayabilir!
        // Başlatma mantığını buraya DEĞİL, @PostConstruct'a yazın
    }

    @PostConstruct
    public void init() {
        // Bean oluşturulup TÜM bağımlılıklar enjekte edildikten SONRA çalışır
        System.out.println("🚀 Cache servisi başlatılıyor...");
        loadInitialData();
        System.out.println("✅ Cache hazır. " + cache.size() + " kayıt yüklendi.");
    }

    @PreDestroy
    public void cleanup() {
        // Uygulama kapanmadan ÖNCE çalışır (graceful shutdown)
        System.out.println("🧹 Cache temizleniyor...");
        cache.clear();
        System.out.println("✅ Cache temizlendi.");
    }

    private void loadInitialData() {
        // Veritabanından sık kullanılan verileri cache'e yükle
        cache.put("appVersion", "2.1.0");
        cache.put("maintenanceMode", false);
        cache.put("maxUploadSize", 10_485_760L); // 10 MB
    }

    public Object get(String key) {
        return cache.get(key);
    }

    public void put(String key, Object value) {
        cache.put(key, value);
    }
}

@PostConstruct vs Constructor — Fark Ne?

@Service
public class ExampleService {
    private final UserRepository userRepository;

    // Constructor: Bağımlılıklar enjekte edilir
    public ExampleService(UserRepository userRepository) {
        this.userRepository = userRepository;
        // Burada userRepository kullanabilirsiniz AMA:
        // - Proxy'ler henüz tam oluşmamış olabilir
        // - @Transactional henüz aktif olmayabilir
        // - Diğer bean'ler henüz hazır olmayabilir
    }

    @PostConstruct
    public void init() {
        // Burası çalıştığında:
        // ✅ Tüm bağımlılıklar hazır
        // ✅ AOP proxy'leri oluşmuş
        // ✅ @Transactional çalışır
        // ✅ ApplicationContext tamamen yüklenmiş
        long userCount = userRepository.count();
        System.out.println("Toplam kullanıcı: " + userCount);
    }
}

💡 İpucu: Basit alan atamaları constructor'da, veritabanı çağrıları ve karmaşık initialization mantığı @PostConstruct'ta yapılmalıdır.


Tam Bir Lifecycle Örneği — Connection Pool Manager

@Component
@Slf4j
public class ExternalApiClient {

    private HttpClient httpClient;
    private final String apiBaseUrl;
    private final int connectionTimeout;

    public ExternalApiClient(
            @Value("${external.api.base-url}") String apiBaseUrl,
            @Value("${external.api.timeout:5000}") int connectionTimeout) {
        this.apiBaseUrl = apiBaseUrl;
        this.connectionTimeout = connectionTimeout;
        log.info("📦 ExternalApiClient CONSTRUCTED (henüz hazır değil)");
    }

    @PostConstruct
    public void initialize() {
        log.info("🔧 HTTP Client başlatılıyor...");
        this.httpClient = HttpClient.newBuilder()
                .connectTimeout(Duration.ofMillis(connectionTimeout))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .build();

        // Bağlantı testi
        try {
            var request = HttpRequest.newBuilder()
                    .uri(URI.create(apiBaseUrl + "/health"))
                    .GET()
                    .build();
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            log.info("✅ API bağlantısı başarılı. Status: {}", response.statusCode());
        } catch (Exception e) {
            log.warn("⚠️ API health check başarısız: {}", e.getMessage());
        }
    }

    @PreDestroy
    public void shutdown() {
        log.info("🔌 HTTP Client kapatılıyor...");
        // HttpClient'ın explicit close'u yok ama
        // thread pool'larını veya executor'ları burada kapatabilirsiniz
        log.info("✅ HTTP Client kapatıldı.");
    }

    public String get(String path) {
        try {
            var request = HttpRequest.newBuilder()
                    .uri(URI.create(apiBaseUrl + path))
                    .GET()
                    .build();
            var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            return response.body();
        } catch (Exception e) {
            throw new ApiException("API call failed: " + path, e);
        }
    }
}

InitializingBean & DisposableBean — Spring Interface'leri

Spring'e özgü interface'ler — @PostConstruct/@PreDestroy'a göre daha az tercih edilir çünkü Spring framework'e bağımlılık yaratır:

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.DisposableBean;

@Component
public class DatabaseConnectionManager implements InitializingBean, DisposableBean {

    private Connection connection;

    @Value("${spring.datasource.url}")
    private String jdbcUrl;

    @Override
    public void afterPropertiesSet() throws Exception {
        // @PostConstruct ile aynı zamanda çalışır
        // Tüm property'ler set edildikten sonra
        connection = DriverManager.getConnection(jdbcUrl);
        System.out.println("🗄️ Veritabanı bağlantısı kuruldu: " + jdbcUrl);
    }

    @Override
    public void destroy() throws Exception {
        // @PreDestroy ile aynı zamanda çalışır
        if (connection != null && !connection.isClosed()) {
            connection.close();
            System.out.println("🔒 Veritabanı bağlantısı kapatıldı.");
        }
    }
}

⚠️ Dikkat: InitializingBean ve DisposableBean kullanırsanız, sınıfınız Spring framework'e compile-time bağımlı olur. Bu, unit test'te Spring container'sız çalıştırmayı zorlaştırır. @PostConstruct/@PreDestroy ise Jakarta EE standardıdır ve framework bağımsızdır.


@Bean ile initMethod ve destroyMethod

@Configuration sınıfında bean tanımlarken lifecycle metotlarını belirtebilirsiniz. Bu özellikle 3rd-party sınıflar için kullanışlıdır — çünkü kaynak kodlarına annotation ekleyemezsiniz:

// 3rd-party kütüphaneden gelen sınıf — annotation ekleyemiyoruz
public class ElasticsearchClient {
    private final String clusterUrl;

    public ElasticsearchClient(String clusterUrl) {
        this.clusterUrl = clusterUrl;
    }

    public void connect() {
        System.out.println("🔍 Elasticsearch'e bağlanılıyor: " + clusterUrl);
        // Cluster'a bağlan, health check yap
    }

    public void disconnect() {
        System.out.println("🔌 Elasticsearch bağlantısı kapatılıyor...");
        // Açık bağlantıları kapat, buffer'ları flush et
    }

    public void search(String query) {
        System.out.println("Searching: " + query);
    }
}

// Configuration'da initMethod/destroyMethod ile lifecycle tanımla
@Configuration
public class SearchConfig {

    @Bean(initMethod = "connect", destroyMethod = "disconnect")
    public ElasticsearchClient elasticsearchClient(
            @Value("${elasticsearch.url}") String url) {
        return new ElasticsearchClient(url);
    }
}

destroyMethod'un Varsayılan Davranışı

// Spring Boot'ta @Bean'in destroyMethod varsayılanı "inferred" (tahminli)
// "close" veya "shutdown" adlı public metot otomatik çağrılır!

@Bean // destroyMethod = "(inferred)" — close() otomatik çağrılır
public HikariDataSource dataSource() {
    HikariDataSource ds = new HikariDataSource();
    ds.setJdbcUrl("jdbc:h2:mem:testdb");
    return ds; // HikariDataSource.close() otomatik çağrılır
}

// Otomatik destroy'u kapatmak isterseniz:
@Bean(destroyMethod = "")
public SomeResource noAutoDestroy() {
    return new SomeResource();
}

BeanPostProcessor — Tüm Bean'lere Müdahale

BeanPostProcessor, Spring container'daki her bean oluşturulurken araya giren güçlü bir mekanizmadır. Spring'in kendi iç mekanizmaları (@Autowired çözümleme, @Scheduled, AOP proxy oluşturma) bile BeanPostProcessor ile çalışır.

@Component
public class TimingBeanPostProcessor implements BeanPostProcessor {

    private final Map<String, Long> startTimes = new ConcurrentHashMap<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        startTimes.put(beanName, System.nanoTime());
        return bean; // Bean'i olduğu gibi döndür (veya değiştir/sarla)
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        Long startTime = startTimes.remove(beanName);
        if (startTime != null) {
            long duration = (System.nanoTime() - startTime) / 1_000_000; // ms
            if (duration > 100) { // 100ms'den uzun sürenler
                System.out.printf("⏱️ SLOW BEAN: %s initialized in %d ms%n",
                    beanName, duration);
            }
        }
        return bean;
    }
}

Loglama BeanPostProcessor

@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        if (bean.getClass().isAnnotationPresent(Service.class)) {
            System.out.println("🔧 Initializing service: " + beanName);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // AOP proxy oluşturma gibi işlemler burada yapılır
        // Orijinal bean yerine proxy döndürebilirsiniz
        return bean;
    }
}

⚠️ Dikkat: BeanPostProcessor HER bean için çağrılır — yüzlerce bean'iniz varsa performans etkisi olabilir. İçerideki mantığı hafif tutun.


Lifecycle Callback Çalışma Sırası

Birden fazla lifecycle yöntemi kullanılırsa, çalışma sırası belirlidir:

Initialization Sırası

1. @PostConstruct metodu          ← Jakarta EE standart (ÖNERİLEN)
2. InitializingBean.afterPropertiesSet()  ← Spring interface
3. @Bean(initMethod = "...")      ← @Configuration'da belirtilen

Destruction Sırası

4. @PreDestroy metodu             ← Jakarta EE standart (ÖNERİLEN)
5. DisposableBean.destroy()       ← Spring interface
6. @Bean(destroyMethod = "...")   ← @Configuration'da belirtilen

Bunu doğrulayan bir örnek:

@Component
public class LifecycleDemo implements InitializingBean, DisposableBean {

    @PostConstruct
    public void postConstruct() {
        System.out.println("1️⃣ @PostConstruct");
    }

    @Override
    public void afterPropertiesSet() {
        System.out.println("2️⃣ InitializingBean.afterPropertiesSet()");
    }

    // initMethod @Bean'de tanımlanırsa 3. sırada çalışır

    @PreDestroy
    public void preDestroy() {
        System.out.println("4️⃣ @PreDestroy");
    }

    @Override
    public void destroy() {
        System.out.println("5️⃣ DisposableBean.destroy()");
    }
}

Çıktı:

1️⃣ @PostConstruct
2️⃣ InitializingBean.afterPropertiesSet()
--- Uygulama çalışıyor ---
4️⃣ @PreDestroy
5️⃣ DisposableBean.destroy()

Aware Interface'leri — Bean'in Çevresini Tanıması

Bazı durumlarda bean'in kendi adını, factory'sini veya application context'i bilmesi gerekir:

@Component
public class ContextAwareBean implements
        BeanNameAware,
        BeanFactoryAware,
        ApplicationContextAware {

    private String beanName;
    private BeanFactory beanFactory;
    private ApplicationContext applicationContext;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("Bean adım: " + name);
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext ctx) {
        this.applicationContext = ctx;
        // ApplicationContext'ten aktif profilleri, environment'ı okuyabilirsiniz
        String[] profiles = ctx.getEnvironment().getActiveProfiles();
        System.out.println("Aktif profiller: " + Arrays.toString(profiles));
    }
}

💡 İpucu: Aware interface'leri nadiren doğrudan kullanılır. Çoğu durumda @Autowired veya constructor injection ile ApplicationContext veya Environment enjekte etmek daha temizdir.


Yaygın Hatalar ve Çözümleri

Hata 1: @PostConstruct'ta Uzun Süren İşlem

// ❌ YANLIŞ — Uygulama başlatma süresini 30 saniye uzatır!
@PostConstruct
public void init() {
    loadMillionRecordsIntoCache(); // 30 sn sürer
}

// ✅ DOĞRU — Asenkron başlatma
@PostConstruct
public void init() {
    // Sadece temel setup
    log.info("Cache servisi hazırlanıyor...");
}

// Uygulama başladıktan sonra arka planda yükle
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
    CompletableFuture.runAsync(this::loadMillionRecordsIntoCache);
}

Hata 2: Prototype Bean'de @PreDestroy

// ❌ @PreDestroy ÇALIŞMAZ — prototype bean'ler Spring tarafından yönetilmez!
@Component
@Scope("prototype")
public class PrototypeBean {
    @PreDestroy
    public void cleanup() {
        // Bu metot ASLA çağrılmaz!
    }
}

// ✅ Manuel temizlik yapın veya singleton wrapper kullanın

Hata 3: Constructor'da Veritabanı Çağrısı

// ❌ Constructor'da veritabanı çağrısı
public UserService(UserRepository repo) {
    this.repo = repo;
    this.adminCount = repo.countByRole(Role.ADMIN); // @Transactional henüz aktif değil!
}

// ✅ @PostConstruct kullanın
@PostConstruct
public void init() {
    this.adminCount = repo.countByRole(Role.ADMIN);
}

ApplicationRunner ve CommandLineRunner

@PostConstruct alternatifi olarak, uygulama tamamen başladıktan sonra çalışan runner'lar da var:

@Component
@Order(1) // Birden fazla runner varsa sıralama
public class DataSeeder implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {
        // Tüm bean'ler hazır, uygulama ayakta
        // Seed data yükle, migration çalıştır, vb.
        System.out.println("🌱 Seed data yükleniyor...");
    }
}

@Component
@Order(2)
public class CacheWarmer implements CommandLineRunner {

    @Override
    public void run(String... args) {
        System.out.println("🔥 Cache ısınıyor...");
    }
}

Sıralama: @PostConstruct → Bean hazır → ApplicationRunnerCommandLineRunner


Özet

  • Bean lifecycle: Instantiation → DI → Aware → BeanPostProcessor → Init → Kullanım → Destroy

  • `@PostConstruct` ve `@PreDestroy` en temiz, en önerilen yaklaşım — Jakarta EE standardı

  • `InitializingBean`/`DisposableBean` Spring'e bağımlılık yaratır — kaçının

  • `@Bean(initMethod, destroyMethod)` 3rd-party sınıflar için kullanın

  • `BeanPostProcessor` tüm bean'lere cross-cutting logic eklemek için (AOP, loglama, timing)

  • Prototype scope bean'lerde `@PreDestroy` çalışmaz — Spring yaşam döngüsünü yönetmez

  • Uzun süren initialization için `ApplicationRunner` veya `@EventListener(ApplicationReadyEvent.class)` tercih edin

  • Constructor'da bağımlılık ataması yapın, iş mantığını @PostConstruct'a bırakın

  • destroyMethod = "(inferred)" varsayılanı sayesinde close() adlı metotlar otomatik çağrılır