İçeriğe geç

Bean Yaşam Döngüsü ve Scope'lar

Başlangıç 10 dk Sık karşılaşılır

Önce şunu oku: IoC ve Dependency Injection

30 saniyede özet

Spring bir nesneyi birkaç adımda hazırlar ve @Transactional gibi etiketleri çalıştıran aracı (proxy) en son eklenir. Bu yüzden @PostConstruct içinden çağrılan @Transactional sessizce hiçbir şey yapmaz.

Spring bir nesneyi tek hamlede değil, işe yeni başlayan biri gibi adım adım hazırlar. Bu sırayı bilmek, iki can sıkıcı hatanın sebebini açıklar.

İşe ilk gün: sözleşme, bilgisayar, oryantasyon ve en son şirket kartı.
Adım adım oku
  1. Spring önce kurucuyu çağırır, sonra bağımlılıkları verir: sözleşme imzalandı, bilgisayar teslim edildi.
  2. Ardından @PostConstruct çalışır: oryantasyon günü.
  3. Oryantasyonda @Transactional bir metodu çağırırsan çağrı çıplak nesneye gider; transaction açılmaz.
  4. @Transactional'ı çalıştıran proxy en son eklenir. Ancak ondan sonra dışarıdan gelen çağrılar proxy'den geçer.
  1. Bayt: @PostConstruct içinde başlangıç verisini yazan @Transactional bir metodu çağırdım.

  2. Sen: Güzel, uygulama açılınca tablo hazır olur.

  3. Bayt: Olmadı! Transaction hiç açılmamış, yarım kayıtlar kalmış.

  4. Bayt: Oryantasyon günü şirket kartıyla kapı açılmaz: kart henüz basılmadı.

Hangi adım ne zaman?

Bir bean'in @PostConstruct metodu çalışırken, constructor'dan değil alan enjeksiyonuyla gelen bağımlılıklar dolu mudur? Cevabı göster

Evet. @PostConstruct, bütün enjeksiyon bittikten sonra çalışır; hazırlık için doğru yer de bu yüzden orasıdır.

Bean yaşam döngüsü
1. Kurucu
2. Bağımlılık enjeksiyonu
3. Aware geri çağrıları (BeanNameAware, ApplicationContextAware)
4. BeanPostProcessor.postProcessBeforeInitialization()
5. @PostConstruct
6. InitializingBean.afterPropertiesSet()
7. @Bean(initMethod = "...")
8. BeanPostProcessor.postProcessAfterInitialization() ← AOP PROXY BURADA OLUŞUR
9. Bean hazır
...
10. @PreDestroy
11. DisposableBean.destroy()
12. @Bean(destroyMethod = "...")

Dikkat edilecek yer 8. adım. Proxy, bütün başlatma geri çağrıları bittikten sonra oluşturulur.

Kafam karıştı, daha basit anlat

Bean’in hayatı bir çalışanın ilk günü gibidir: önce işe alınır, sonra araçları verilir, sonra hazırlık yapar. En sonunda işten ayrılırken masasını toplar.

Hızlı kontrolBaşlangıç

Bir bean'in başlatma kancaları hangi sırayla çalışır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin izle

Bean yaşam döngüsü — sıra, proxy ve scope

Tohum 1
singletonproxy henüz yok
  1. Oluşturma

    Kurucu çağrılır
  2. Bağımlılıklar enjekte edilir
  3. Aware geri çağrıları

    BeanNameAware.setBeanName()
  4. ApplicationContextAware.setApplicationContext()
  5. BeanPostProcessor

    BeanPostProcessor.postProcessBeforeInitialization()
  6. Başlatma geri çağrıları

    @PostConstructproxy yok — sessiz hata
  7. InitializingBean.afterPropertiesSet()
  8. @Bean(initMethod = "...")
  9. BeanPostProcessor

    BeanPostProcessor.postProcessAfterInitialization()proxy burada
  10. Kullanım

    Bean kullanıma hazır
  11. Kapatma

    @PreDestroy
  12. DisposableBean.destroy()
  13. @Bean(destroyMethod = "...")
Hız
Adım 0

Şu an ne oldu?

Kurucu çalışıyor

Nesne oluşturuluyor. Constructor injection kullanılıyorsa bağımlılıklar tam burada verilir, field injection kullanılıyorsa alanlar hâlâ null.

Aklında kalsın: Kurucuda bağımlılık kullanabilmek, constructor injection’ın field injection karşısındaki somut avantajlarından biridir.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

İki ayarı mutlaka dene:

  • Varsayılan — @PostConstruct adımında kırmızı bir satır çıkıyor. Proxy henüz yok, @Transactional çağrısı ham nesneye gitti, transaction açılmadı. Hiçbir hata da verilmedi.
  • Scope’u prototype yap. Üç destroy adımı üstü çizili geçiliyor. Spring bu örneği teslim edip unuttu.
Kafam karıştı, daha basit anlat

Hazırlık adımı, bütün araçlar verildikten sonra çalışır. Araçlar gelmeden hazırlanmaya çalışırsan elin boş kalır.

Hızlı kontrolOrta

Bu bean açılışta ne yazdırır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
KurucuTuzagi.java
1@Component
2class ReportService {
3
4 @Autowired
5 private ReportRepository repo;
6
7 ReportService() {
8 System.out.println("kurucu: " + repo);
9 }
10
11 @PostConstruct
12 void init() {
13 System.out.println("init: " + (repo != null));
14 }
15}
Java 21UTF-8LF

Tuzaklar

Sessizce çalışmayan kod
@Service
public class CacheWarmer {
@PostConstruct
void warmUp() {
loadFromDb(); // @Transactional ama proxy henüz yok → transaction YOK
}
@Transactional
public void loadFromDb() { /* ... */ }
}

İki sorun birden var. Birincisi bu bir self-invocation — this.loadFromDb() proxySpring'in bean'in yerine koyduğu sarmalayıcı nesne. `@Transactional` gibi anotasyonlar burada çalışır — çağrı proxy'den geçmezse hiçbir şey olmaz.Sözlükte gör →’den geçmez. İkincisi, dışarıdan çağrılsa bile proxy henüz var olmadığı için yine çalışmazdı.

Doğru çözüm, başlatmayı tüm proxy’ler hazır olduktan sonraya ertelemektir:

Çalışan hâli
@Service
public class CacheWarmer {
@EventListener(ApplicationReadyEvent.class) // container tamamen hazır
void warmUp() {
cacheService.loadFromDb(); // başka bir bean → proxy devrede
}
}

Aşağıdaki örnek bir bankanın gün sonu servisinden. Her kanca doğru işte: SFTP bağlantısını açıp kapatmak, gelen ödemeleri işleyen döngüyü sırayla başlatıp durdurmak, günün kurlarını uygulama hazır olunca yüklemek ve her ekstre için yeni bir yazıcı kullanmak.

Derinleş · Bankanın gün sonu servisi: hangi kanca, ne zaman? 5 dosya · ~172 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/statement/ StatementSftpClient.java Kurumsal müşterinin SFTP sunucusuna bağlantı: @PostConstruct'ta açılır, @PreDestroy'da kapanır.

src/main/java/com/bank/statement/StatementSftpClient.java
@Component
class StatementSftpClient {
private static final Logger log = LoggerFactory.getLogger(StatementSftpClient.class);
private final StatementDeliveryProperties props;
private SftpSession session;
StatementSftpClient(StatementDeliveryProperties props) {
this.props = props;
}
// Runs once, after dependencies are injected, before anyone can call this bean.
// Corporate customers pull their end-of-day statements from this server.
@PostConstruct
void connect() {
session = SftpSession.open(props.host(), props.user(), props.keyPath());
log.info("statement SFTP session opened to {}", props.host());
}
// Runs on context close for singletons. Never runs for prototype beans.
@PreDestroy
void disconnect() {
if (session != null) session.close();
log.info("statement SFTP session closed");
}
void deliver(Path statementFile, String customerNo) {
session.put(statementFile, props.remoteDir().resolve(customerNo));
}
}

src/main/java/com/bank/payment/ IncomingPaymentPoller.java Gelen ödeme mesajlarını işleyen arka plan döngüsü: SmartLifecycle ile başlar ve durur; phase sırası kapanışta tersine döner.

src/main/java/com/bank/payment/IncomingPaymentPoller.java
@Component
class IncomingPaymentPoller implements SmartLifecycle {
private final IncomingPaymentInbox inbox; // messages received from the clearing system
private final ExecutorService loop = Executors.newSingleThreadExecutor();
private volatile boolean running;
IncomingPaymentPoller(IncomingPaymentInbox inbox) {
this.inbox = inbox;
}
@Override
public void start() { // after all singletons are ready
running = true;
loop.submit(this::pollUntilStopped);
}
private void pollUntilStopped() {
while (running) {
// Each batch credits the beneficiary accounts in one transaction and marks the
// messages processed, so a crash mid-batch replays it instead of losing money.
int credited = inbox.creditNextBatch(50);
if (credited == 0) {
try {
Thread.sleep(500); // nothing to do: back off instead of spinning
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return;
}
}
}
}
@Override
public void stop() { // on shutdown, before @PreDestroy of its dependencies
running = false;
loop.shutdown();
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
// Higher phase: started later, stopped earlier. The poller stops before
// lower-phase beans such as the web server and its graceful shutdown.
return Integer.MAX_VALUE - 1000;
}
}

src/main/java/com/bank/fx/ FxRateWarmup.java Günün döviz kurları uygulama hazır olunca bir kez yüklenir. Transaction burada çalışır, @PostConstruct'ta değil.

src/main/java/com/bank/fx/FxRateWarmup.java
@Component
class FxRateWarmup implements ApplicationRunner {
private final FxRateService rates;
FxRateWarmup(FxRateService rates) {
this.rates = rates;
}
// Runs once the context is fully started: today's buy/sell rates are loaded before
// the first currency exchange is priced. rates.loadToday() goes through a proxy,
// so its @Transactional applies — unlike a call from this bean's own @PostConstruct.
@Override
public void run(ApplicationArguments args) {
rates.loadToday();
}
}

src/main/java/com/bank/statement/ StatementWriter.java Her hesap ekstresi için yeni bir yazıcı: prototype. Spring onu kapatmaz; ObjectProvider ile alan, try-with-resources ile kapatır.

src/main/java/com/bank/statement/StatementWriter.java
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class StatementWriter implements AutoCloseable {
private final BufferedWriter out;
private final Path file;
StatementWriter(StatementDeliveryProperties props) throws IOException {
this.file = props.workDir().resolve(UUID.randomUUID() + ".csv");
this.out = Files.newBufferedWriter(file);
}
void line(LocalDate valueDate, String description, BigDecimal amount, BigDecimal balance) throws IOException {
out.write(String.join(";", valueDate.toString(), description,
amount.toPlainString(), balance.toPlainString()));
out.newLine();
}
Path file() {
return file;
}
@Override
public void close() throws IOException { // Spring will not call this: the caller must
out.close();
}
}
@Service
class EndOfDayStatements {
private final ObjectProvider<StatementWriter> writers; // a fresh writer per account
private final AccountMovements movements;
private final StatementSftpClient sftp;
EndOfDayStatements(ObjectProvider<StatementWriter> writers, AccountMovements movements,
StatementSftpClient sftp) {
this.writers = writers;
this.movements = movements;
this.sftp = sftp;
}
void produce(String customerNo, String iban, LocalDate day) throws IOException {
Path file;
try (StatementWriter writer = writers.getObject()) {
for (Movement m : movements.of(iban, day)) {
writer.line(m.valueDate(), m.description(), m.amount(), m.balanceAfter());
}
file = writer.file();
}
sftp.deliver(file, customerNo); // only after the file is flushed and closed
}
}

src/main/java/com/bank/ops/ LifecycleEvents.java Uygulama olaylarını log'lar: hangi kanca hangi sırayla çalıştı, okunabilir.

src/main/java/com/bank/ops/LifecycleEvents.java
@Component
class LifecycleEvents {
private static final Logger log = LoggerFactory.getLogger(LifecycleEvents.class);
@EventListener
void started(ApplicationStartedEvent event) {
log.info("context refreshed, payment poller started, FX warmup not yet run");
}
@EventListener
void ready(ApplicationReadyEvent event) {
log.info("FX rates loaded, application is ready to serve traffic");
}
@EventListener
void closing(ContextClosedEvent event) {
log.info("closing: poller stops first, then @PreDestroy closes the SFTP session");
}
}

Prototype beanYaşam döngüsünü Spring konteynerinin yönettiği nesne. Sen `new` demezsin; konteyner kurar, bağımlılıklarını verir ve gerekirse yok eder.Sözlükte gör →’lerin destroy’u çalışmaz. Spring prototype bean’i oluşturur, bağımlılıklarını enjekte eder, sana verir ve takibi bırakır. Örneği artık tutmadığı için:

  • @PreDestroy asla çağrılmaz
  • DisposableBean.destroy() asla çağrılmaz
  • destroyMethod asla çağrılmaz
Sızıntı
@Scope("prototype")
@Component
public class ReportGenerator implements AutoCloseable {
private final FileWriter writer = new FileWriter("/tmp/report");
@PreDestroy
public void cleanup() throws IOException {
writer.close(); // ASLA ÇAĞRILMAZ
}
}

Prototype bir bean kaynak tutuyorsa onu kapatmak çağıranın sorumluluğudur — try-with-resources veya açık bir close() çağrısı gerekir.

Hızlı kontrolBaşlangıç

`@PostConstruct` içinden aynı bean'in `@Transactional` metodunu çağırıyorsunuz. Ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

`@PreDestroy` hangi durumda hiç çalışmaz?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Scope’lar

ScopeKaç örnekdestroy çalışır mı
singleton (varsayılan)Container başına birEvet
prototypeHer enjeksiyon/istekte yeniHayır
requestHer HTTP isteğinde birEvet (istek bitince)
sessionHer oturumda birEvet (oturum bitince)

Singleton olmanın en önemli sonucu: bean’ler durumsuz olmalıdır. Tek bir örnek tüm eşzamanlı istekleri karşılar, dolayısıyla bir alana istek verisi yazmak doğrudan bir yarış durumudur.

Klasik hata
@Service
public class OrderService {
// singleton — bu alan tüm istekler arasında paylaşılıyor
private Order currentOrder; // yarış durumu
public void process(Order order) {
this.currentOrder = order; // başka bir istek bunu ezebilir
}
}
Kafam karıştı, daha basit anlat

Singleton bean, bütün müşterilere hizmet eden tek bir çalışandır. Bir müşterinin bilgisini onun masasına yazarsan, sonraki müşteri onu okur.

Hızlı kontrolBaşlangıç

Varsayılan bean kapsamı (`scope`) nedir ve ne anlama gelir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Singleton bir bean'de istek verisini bir alana yazmak neden sorunludur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Üç hazırlık yöntemi, hangisi?

Spring’e bağımlılıkNot
@PostConstructYok (JSR-250 standardı)Tercih edilen
InitializingBeanVar — arayüzü implemente etmek gerekirSınıfı Spring’e bağlar
@Bean(initMethod)YokKaynak kodu sende olmayan sınıflar için

Modern kodda genellikle yalnızca @PostConstruct görürsün. @Bean(initMethod) ise üçüncü parti bir sınıfı yapılandırırken işe yarar — sınıfa anotasyon ekleyemezsin ama yapılandırmada init metodunu söyleyebilirsin.

Kendini sına

Şimşek turu1/5

Spring bir bean'i tek hamlede hazırlar.

Soru 1/2Başlangıç

Prototype scope'lu bir bean'de `@PreDestroy` ne zaman çağrılır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Hazırlık sırası: önce @PostConstruct, sonra afterPropertiesSet(), sonra initMethod. Proxy bunların hepsinden sonra eklenir.
  2. 2 Proxy en sonda geldiği için @PostConstruct içinden çağrılan @Transactional metot ham nesneye gider: transaction hiç açılmaz, hata da verilmez.
  3. 3 Prototype scope'ta Spring nesneyi teslim edip unutur, @PreDestroy hiç çalışmaz. Kaynağı kapatmak çağıranın işidir.
Sonraki kapı Security bir isteği 401 ile geri çevirdi. Senin yazdığın audit interceptor'ı bu isteği kaydeder mi? Spring MVC — Bir İstek Controller'a Nasıl Ulaşır? · 10 dk

4 kart sonraki derste seni bekliyor

0/5 kart bu dersten toplandı