Spring · Bölüm 7

Spring Boot: auto-configuration, konfigürasyon ve production özellikleri

Spring Boot'un Framework'e neyi eklediğini, auto-configuration'ın koşullarla nasıl geri çekildiğini, konfigürasyonun hangi sırayla ezildiğini ve Actuator, probe'lar, graceful shutdown ile servisin production'a nasıl hazırlandığını öğretir.

~45 dk okumaQuiz: j23, j24, q6
Cevaplar:

Bu bölümde

Spring Boot'u "sihirli" olmaktan çıkarıp bir mekanizma olarak göreceksin: bir starter eklediğinde hangi bean'lerin neden oluştuğunu, kendi bean'ini tanımladığında Boot'un nasıl geri çekildiğini, aynı property beş yerde tanımlıyken hangisinin kazandığını ve bir servisin Kubernetes'te trafiği kaybetmeden nasıl açılıp kapandığını. Mülakatta bu konu genelde açık uçlu gelir ("Boot ile Framework farkı ne?", "auto-configuration nasıl çalışır?") ve hemen ardından production'a bağlanır ("liveness probe'a veritabanı kontrolü koyar mısın?", "secret'ları nerede tutuyorsun?"). Bankacılıkta ve büyük e-ticaret ölçeğinde onlarca servisin aynı standardı paylaşması gerektiği için "kendi starter'ını yazdın mı?" sorusu da sık gelir.

Konu anlatımı

Spring Framework ve Spring Boot: kim ne yapar?

Spring Boot, Spring Framework'ün yerine geçmez. Framework'ün üzerine kurulu, makul varsayılanlarla hemen çalışan (opinionated) bir katmandır. Boot ile yazdığın controller yine Spring MVC'nin DispatcherServlet'inde, transaction'ın yine Framework'ün PlatformTransactionManager'ında, bean'lerin yine Framework'ün IoC container'ında yaşar.

Spring Framework sağlarSpring Boot ekler
IoC container ve DIAuto-configuration: classpath'e ve koşullara bakarak DataSource, ObjectMapper, KafkaTemplate gibi bean'leri kendisi kurar
AOP, @Transactional altyapısıStarter'lar: tek bağımlılıkla uyumlu kütüphane setini getirir; sürümleri Boot'un BOM'u yönetir
Spring MVC, WebFluxEmbedded server: Tomcat/Jetty/Netty jar'ın içinde; java -jar app.jar ile çalışır
JdbcTemplate, transaction soyutlamalarıExternalized configuration: application.yml, profiller, ortam değişkenleri, tipli @ConfigurationProperties
Event, Environment, ResourceProduction özellikleri: Actuator (health, metrics, probe'lar), graceful shutdown

@SpringBootApplication üç anotasyonun birleşimidir:

  • @SpringBootConfiguration: bir @Configuration; sınıfı konfigürasyon kaynağı yapar ve testlerde ana konfigürasyonun bulunmasını sağlar.
  • @EnableAutoConfiguration: auto-configuration mekanizmasını açar.
  • @ComponentScan: anotasyonun bulunduğu paketten başlayarak bileşenleri tarar.

Boot olmasaydı aynı REST + PostgreSQL servisi için şunları elle yapardık: uyumlu Spring, Hibernate, Jackson, Hikari sürümlerini tek tek seçmek; DispatcherServlet'i web.xml veya WebApplicationInitializer ile kaydetmek; uygulamayı WAR paketleyip harici bir Tomcat'e deploy etmek; DataSource, connection pool, EntityManagerFactory, PlatformTransactionManager, ObjectMapper bean'lerini yazmak; property dosyalarını ve profilleri bağlamak; health ve metrik endpoint'lerini kendimiz geliştirmek.

SpringApplication.run çağrıldığında olanlar, kısaca:

  1. ApplicationStartingEvent; listener'lar ve initializer'lar yüklenir.
  2. Environment hazırlanır: property kaynakları (komut satırı, env, application.yml, profiller) sıralanır. ApplicationEnvironmentPreparedEvent.
  3. ApplicationContext oluşturulur, bean tanımları yüklenir (component scan + kullanıcı @Configuration'ları, ardından auto-configuration'lar).
  4. Context refresh: bean'ler oluşturulur, embedded web server başlar.
  5. ApplicationStartedEvent, ardından LivenessState.CORRECT.
  6. ApplicationRunner ve CommandLineRunner'lar çalışır.
  7. ApplicationReadyEvent, ardından ReadinessState.ACCEPTING_TRAFFIC.

Son iki adımın sırası önemlidir: açılış görevlerini runner'a koyarsan, readiness bunlar bitene kadar açılmaz (bkz. Bölüm 6).

Starter'lar ve bağımlılık yönetimi

Bir starter, belirli bir yetenek için gereken bağımlılıkları bir araya getiren, çoğunlukla kodu olmayan bir POM'dur. spring-boot-starter-data-jpa eklediğinde Hibernate, Spring Data JPA, HikariCP ve Spring ORM gelir.

Sürümleri spring-boot-dependencies BOM'u yönetir. spring-boot-starter-parent'tan türeyen veya BOM'u import eden bir projede Jackson, Hibernate, Kafka client, Testcontainers gibi yüzlerce kütüphanenin birlikte test edilmiş sürümleri hazırdır; bağımlılık eklerken sürüm yazmazsın. Bir kütüphanenin sürümünü değiştirmek gerekirse (ör. güvenlik açığı için Jackson yaması) Maven'da ilgili property'yi ezersin (<jackson-bom.version>); bu, dependency'yi tek tek pinlemekten daha güvenlidir çünkü tüm modüller aynı sürüme geçer.

Spring Boot 4 ile auto-configuration tek büyük spring-boot-autoconfigure jar'ından daha küçük, odaklı modüllere bölündü ve bazı starter'ların adı değişti; örneğin spring-boot-starter-web yerine spring-boot-starter-webmvc geldi. Geçişi kolaylaştırmak için tüm modülleri getiren "classic" starter'lar da sunuluyor.

Auto-configuration nasıl çalışır?

Auto-configuration üç parçadan oluşur: keşif, koşullar, ayarlar.

1) Keşif. Her auto-configuration jar'ı, META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports dosyasında auto-configuration sınıflarını her satıra bir tane olacak şekilde listeler. @EnableAutoConfiguration, bir DeferredImportSelector (AutoConfigurationImportSelector) aracılığıyla bu dosyaları okur. Bu dosya Boot 2.7 ile geldi; 2.7'de eski META-INF/spring.factories kaydı hâlâ destekleniyordu, Boot 3.0'da auto-configuration için spring.factories desteği kaldırıldı. Auto-configuration sınıfları component scan ile bulunmaz; Boot dokümantasyonu bunların asla scan hedefi olmaması gerektiğini açıkça söyler.

Deferred kelimesi kritik: auto-configuration'lar kullanıcının tüm bean tanımları işlendikten sonra değerlendirilir. Böylece "kullanıcı bu bean'i zaten tanımladı mı?" sorusu güvenle cevaplanabilir.

2) Koşullar. Her auto-configuration sınıfı ve içindeki @Bean metotları @Conditional... anotasyonlarıyla korunur:

KoşulSoruÖrnek
@ConditionalOnClass / @ConditionalOnMissingClassClasspath'te bu sınıf var mı?Redis client'ı varsa Redis auto-config devreye girer
@ConditionalOnBean / @ConditionalOnMissingBeanContext'te bu tipte/isimde bean var mı?Kullanıcı DataSource tanımladıysa Boot tanımlamaz
@ConditionalOnSingleCandidateBu tipte tek bir aday (veya tek primary) var mı?Birden çok RedisConnectionFactory varsa template kurulmaz
@ConditionalOnPropertyBir property belirli bir değerde mi?outbox.relay.enabled=true ise relay bean'i
@ConditionalOnWebApplicationServlet/reactive web uygulaması mı?MVC'ye özgü bean'ler
@ConditionalOnResource, @ConditionalOnExpressionKaynak var mı, SpEL ifadesi doğru mu?Nadiren

@ConditionalOnMissingBean Boot'un geri çekilme (back-off) mekanizmasıdır. Varsayılan olarak @Bean metodunun dönüş tipine bakar; name, value, type ile daraltılabilir. Boot dokümantasyonu bean koşullarının yalnızca auto-configuration sınıflarında güvenilir olduğunu, çünkü bunların kullanıcı tanımlarından sonra yüklendiğinin garanti edildiğini belirtir. Kendi @Configuration sınıfında @ConditionalOnMissingBean kullanırsan sonuç, tanımların işlenme sırasına bağlı ve kırılgan olur.

3) Ayarlar. Auto-configuration'lar değerlerini @ConfigurationProperties sınıflarından alır (spring.datasource.*, spring.data.redis.*, spring.kafka.*). Yani Boot'u ezmenin üç kademesi vardır, en hafiften en ağıra:

  1. Property ile ayarla: spring.kafka.producer.acks=all. Varsayılan bean kalır, davranışı değişir.
  2. Customizer bean'i tanımla: Boot'un bean'i kalır, üzerine ayar eklenir. Örnekler: Jackson2ObjectMapperBuilderCustomizer (Boot 4 / Jackson 3'te JsonMapperBuilderCustomizer), RestClientCustomizer, DefaultKafkaProducerFactoryCustomizer.
  3. Kendi bean'ini tanımla: Boot'un bean'i @ConditionalOnMissingBean sayesinde hiç oluşmaz. Boot'un o bean için uyguladığı tüm property'leri ve varsayılanları kaybedersin.

Kendi ObjectMapper'ını new ObjectMapper() ile tanımlamak tipik bir üçüncü kademe hatasıdır: spring.jackson.* ayarları uygulanmaz ve Jackson 2'de Boot'un otomatik kaydettiği modüller (ör. java.time desteği için JavaTimeModule) kaybolur; Instant alanlı bir DTO'yu serileştirirken hata alırsın. Doğrusu:

java
@Configuration(proxyBeanMethods = false)
public class JacksonConfig {

    @Bean
    Jackson2ObjectMapperBuilderCustomizer moneyAndDates() {
        return builder -> builder
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .featuresToEnable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
    }
}

Bean override neden hata verir? Spring Boot 2.1'den beri spring.main.allow-bean-definition-overriding varsayılan false'tur. Aynı isimde iki bean tanımı kaydedilmeye çalışılırsa BeanDefinitionOverrideException alırsın. Auto-configuration ile çakışma ise normalde olmaz, çünkü Boot zaten geri çekilir. Bu hatayı genelde iki kullanıcı konfigürasyonunun aynı isimli @Bean metodu tanımlamasında görürsün.

Auto-configuration'ı tamamen kapatmak için:

java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class ReportingApplication { }

veya spring.autoconfigure.exclude property'si. Exclude, back-off'tan farklıdır: sınıf hiç değerlendirilmez.

Neyin neden yüklendiğini görmek: condition evaluation report

Uygulamayı --debug ile (veya debug=true property'siyle) başlatırsan Boot, çekirdek logger'ları debug seviyesine alır ve konsola Conditions Evaluation Report yazar:

text
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource', ... (OnClassCondition)

Negative matches:
-----------------
   RedisAutoConfiguration#redisTemplate:
      Did not match:
         - @ConditionalOnMissingBean (names: redisTemplate; SearchStrategy: all) found beans named redisTemplate (OnBeanCondition)

Aynı bilgi çalışan uygulamada Actuator'ın /actuator/conditions endpoint'inden alınır (expose edilmişse). Yanında /actuator/beans (hangi bean'ler var, bağımlılıkları), /actuator/configprops (bağlanmış @ConfigurationProperties değerleri) ve /actuator/env (property kaynakları ve hangi kaynağın kazandığı) hata ayıklamada en çok kullanılanlardır.

Auto-configuration'ı test etmenin doğru aracı ApplicationContextRunner'dır: tam bir uygulama açmadan, belirli konfigürasyonlar ve property'lerle küçük bir context kurar ve koşulların sonucunu doğrular.

java
class CorrelationIdAutoConfigurationTest {

    private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(CorrelationIdAutoConfiguration.class));

    @Test
    void registersFilterByDefault() {
        runner.run(ctx -> assertThat(ctx).hasSingleBean(CorrelationIdFilter.class));
    }

    @Test
    void backsOffWhenDisabled() {
        runner.withPropertyValues("platform.correlation.enabled=false")
              .run(ctx -> assertThat(ctx).doesNotHaveBean(CorrelationIdFilter.class));
    }
}

Kendi starter'ını yazmak

Onlarca serviste aynı standardı (correlation id, ortak Kafka error handler, log formatı, tracing) tekrar tekrar kopyalamak yerine bir şirket içi starter yazılır. İskelet:

java
@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(prefix = "platform.correlation", name = "enabled",
        havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(CorrelationProperties.class)
public class CorrelationIdAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    CorrelationIdFilter correlationIdFilter(CorrelationProperties properties) {
        return new CorrelationIdFilter(properties.headerName());
    }
}

@ConfigurationProperties(prefix = "platform.correlation")
public record CorrelationProperties(
        @DefaultValue("true") boolean enabled,
        @DefaultValue("X-Correlation-Id") String headerName) {}

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

text
com.example.platform.correlation.CorrelationIdAutoConfiguration

Tasarım ilkeleri:

  • Her bean @ConditionalOnMissingBean ile korunsun. Kullanan servis ezmek isterse ezebilmeli.
  • Kapatma anahtarı olsun (@ConditionalOnProperty).
  • Opsiyonel bağımlılıklar @ConditionalOnClass ile korunsun; kütüphane classpath'te yoksa auto-config sessizce devre dışı kalmalı.
  • Component scan kullanma; gereken sınıfları @Import et.
  • Sıralama gerekiyorsa @AutoConfiguration(after = KafkaAutoConfiguration.class) kullan. Boot dokümantasyonuna göre bu sıra yalnızca bean tanımlarının sırasını etkiler; bean'lerin oluşturulma sırası bağımlılıklara göre belirlenir.
  • spring-boot-configuration-processor'ı ekle; @ConfigurationProperties için IDE otomatik tamamlama metadata'sı üretilir.

Externalized configuration: hangi değer kazanır?

Spring Boot konfigürasyonu birçok kaynaktan okur ve bunları bir öncelik sırasına dizer. Resmi dokümantasyondaki sıra (sonraki öncekini ezer, yani listenin altı daha güçlüdür):

  1. Varsayılan property'ler (SpringApplication.setDefaultProperties)
  2. @Configuration sınıflarındaki @PropertySource anotasyonları (context refresh sırasında eklendikleri için logging.*, spring.main.* gibi erken okunan ayarlar için geç kalır)
  3. Config data: application.properties / application.yml dosyaları
  4. random.* değerleri (RandomValuePropertySource)
  5. OS ortam değişkenleri
  6. Java system property'leri (-Dkey=value)
  7. JNDI attribute'ları
  8. ServletContext init parametreleri
  9. ServletConfig init parametreleri
  10. SPRING_APPLICATION_JSON (env veya system property içinde inline JSON)
  11. Komut satırı argümanları (--server.port=9090)
  12. Testlerde @SpringBootTest(properties = ...) ve slice testlerinin properties attribute'u
  13. Testlerde @DynamicPropertySource
  14. Testlerde @TestPropertySource
  15. DevTools aktifse $HOME/.config/spring-boot altındaki global ayarlar

Config data dosyalarının kendi içindeki sıra (yine alttaki kazanır):

  1. Jar içindeki application.yml
  2. Jar içindeki application-{profile}.yml
  3. Jar dışındaki application.yml
  4. Jar dışındaki application-{profile}.yml

Akılda tutulacak sonuçlar:

  • Komut satırı > system property > ortam değişkeni > dosya. Kubernetes'te env ile verilen değer, imajın içindeki application.yml'ı ezer.
  • Profile özel dosya, genel dosyayı ezer. Aynı konumda application-prod.yml, application.yml'daki aynı anahtarı ezer.
  • Jar dışındaki dosya, jar içindekini ezer. Ops ekibi imajı yeniden build etmeden bir dosya mount edebilir.
  • Aynı konumda hem .properties hem .yml varsa .properties kazanır. Dokümantasyon tek format kullanmayı önerir.
  • Birden çok profil aktifse (dev,local), aynı anahtar için son yazılan profil kazanır.

spring.config.import ile ek kaynaklar alınabilir. İçe aktarılan değerler, import eden dosyanın değerlerini ezer. Konum yoksa uygulama ConfigDataLocationNotFoundException ile açılmaz; optional: öneki bunu yumuşatır:

yaml
spring:
  config:
    import:
      - optional:file:./local-overrides.yml
      - optional:configtree:/etc/secrets/

Relaxed binding ve ortam değişkenleri

@ConfigurationProperties'e bağlanırken Boot isimlerde esnektir: spring.datasource.url, spring.data-source.url (kebab-case), spring.dataSource.url (camelCase) ve SPRING_DATASOURCE_URL (ortam değişkeni) aynı alana bağlanır. Kanonik ve önerilen form küçük harfli kebab-case'dir.

Bir property adını ortam değişkenine çevirme kuralları:

  1. Noktaları (.) alt çizgiyle (_) değiştir.
  2. Tireleri (-) kaldır.
  3. Büyük harfe çevir.

spring.main.log-startup-info → SPRING_MAIN_LOGSTARTUPINFO. Liste indeksleri alt çizgiyle çevrelenir: my.services[0].url → MY_SERVICES_0_URL.

@Value relaxed binding'i sadece sınırlı ölçüde destekler. Boot dokümantasyonu, @Value kullanılacaksa property'ye kanonik formda (@Value("${checkout.payment-timeout}")) atıf yapılmasını önerir; böylece env ile ezme de çalışır.

Profiller

Profil, ortama göre bean ve konfigürasyon setlerini açıp kapatmanın yoludur.

yaml
# application.yml
spring:
  application:
    name: order-service
  profiles:
    group:
      prod: [prod-db, prod-kafka]   # --spring.profiles.active=prod üçünü birden açar

checkout:
  payment-timeout: 2s

---
spring:
  config:
    activate:
      on-profile: local
checkout:
  payment-timeout: 30s   # lokal debug için uzun timeout

Kurallar:

  • Aktif profil spring.profiles.active ile seçilir (komut satırı, env SPRING_PROFILES_ACTIVE veya dosya). Hiçbiri aktif değilse default profili aktiftir.
  • spring.profiles.active ve spring.profiles.default profile özel bir dosyada veya on-profile ile aktive edilen bir dokümanda kullanılamaz; Boot bunu geçersiz konfigürasyon sayar.
  • Bean seviyesinde @Profile("!prod") kullanılır; profil ifadeleri !, &, | destekler.

Production'da profil kullanımı: 12-factor prensibine göre aynı imaj dev, stage ve prod'da çalışır; sadece konfigürasyon değişir. Profiller bu konfigürasyon farkını taşımak için iyidir, ama iki yaygın kötüye kullanım var: profil sayısının ortam × bölge × müşteri çarpımıyla patlaması (prod-eu-tenantA) ve iş mantığının profile göre dallanması. Ortama özgü değerler (URL, pool boyutu) Kubernetes ConfigMap'ten env olarak gelmeli; profiller "lokal mi, test mi, gerçek altyapı mı" gibi kaba ayrımlar için kalmalı.

@ConfigurationProperties ve @Value

İki mekanizma da property okur, ama farklı işler için tasarlanmıştır:

Özellik@ConfigurationProperties@Value
Relaxed bindingTamSınırlı
IDE metadata desteğiVar (configuration processor ile)Yok
SpELYokVar
Gruplama ve tip güvenliğiPrefix altındaki tüm ayarlar tek tipli nesnedeTek tek değer
Doğrulama@Validated + Bean ValidationYok
Immutabilityrecord ileAlan enjeksiyonu (veya constructor parametresi)
java
@ConfigurationProperties(prefix = "checkout")
@Validated
public record CheckoutProperties(
        @NotNull Duration paymentTimeout,                 // "2s", "500ms" otomatik Duration'a çevrilir
        @Min(1) @Max(500) int maxItemsPerOrder,
        @NotNull DataSize maxAttachmentSize,               // "5MB"
        @NotEmpty Map<String, BigDecimal> shippingFees) {}

Kayıt için @EnableConfigurationProperties(CheckoutProperties.class) veya ana sınıfta @ConfigurationPropertiesScan kullanılır. Boot 3.0+'da tek constructor'lı sınıflar ve record'lar için @ConstructorBinding yazmak gerekmez.

Neden @ConfigurationProperties tercih edilir?

  • Açılışta doğrulama. maxItemsPerOrder: 0 yazılırsa uygulama açılmaz ve hangi property'nin hangi kuralı ihlal ettiği yazılır. @Value ile bu değer ilk kullanıldığında, belki gece 3'te, sorun çıkarır. @Validated için spring-boot-starter-validation gerekir.
  • Tip dönüşümü. Duration, DataSize, enum, List, Map doğrudan bağlanır. @Value("${timeout}") long timeoutMs gibi birimi isme gömülü değerler hata kaynağıdır.
  • Tek yerde toplanmış konfigürasyon. Bir servisin hangi ayarlarla çalıştığı tek bir record'dan okunur; test için new CheckoutProperties(...) yazılır.
  • Metadata. spring-boot-configuration-processor, IDE'ye otomatik tamamlama ve dokümantasyon sağlar.

@Value ne zaman uygun? Tek, izole bir değer (@Value("${outbox.batch-size:100}") int batchSize) veya SpEL gerektiren nadir durumlar. Üçüncü değerde bir record'a geçmek iyi bir eşiktir.

Secret yönetimi

Veritabanı şifresini application.yml'a yazıp commit etmek, repo private olsa bile kabul edilemez: git geçmişi kalıcıdır, repo'ya erişen herkes (CI sistemi, eski çalışanlar, fork'lar) şifreyi görür; şifre yeni bir commit ile silinse bile geçmişte kalır (temizlemek geçmişi yeniden yazmayı gerektirir) ve asıl düzeltme şifreyi rotate etmektir. Doğru yaklaşım:

  • Kubernetes Secret → ortam değişkeni. SPRING_DATASOURCE_PASSWORD olarak verilir, relaxed binding ile bağlanır. Basit ama env değerleri process listesinde, crash dump'larda ve yanlış yapılandırılmış log'larda görünebilir.
  • Kubernetes Secret → volume (config tree). Boot'un configtree: desteğiyle /etc/secrets/spring.datasource.password dosyası bir property olur. Boot dokümantasyonu env'deki secret'ların dezavantajlarını bu yolla aşmayı önerir.
  • Harici secret yöneticisi: HashiCorp Vault (Spring Cloud Vault), AWS Secrets Manager veya GCP Secret Manager. spring.config.import ile bağlanır; dinamik ve kısa ömürlü veritabanı kimlik bilgileri ve merkezi rotasyon sağlar. Bankacılıkta denetim izi (kim, ne zaman, hangi secret'ı okudu) için genelde bu yol zorunludur.

Ek önlemler: /actuator/env ve /actuator/configprops Boot 3.0'dan beri değerleri varsayılan olarak maskeler (show-values: never); bunu açma. Secret'ları log'lama (toString()'i olan bir properties record'u log'a yazılırsa şifre de gider). Rotasyonu tasarla: şifre değiştiğinde pod'ların yeniden başlatılması mı, Hikari'nin yeni bağlantılarda yeni şifreyi alması mı?

Actuator: health, probe'lar, metrikler

Actuator, uygulamanın iç durumunu HTTP ve JMX üzerinden açar. Varsayılan olarak yalnızca health endpoint'i HTTP üzerinden expose edilir. Diğerleri açıkça açılır:

yaml
management:
  endpoints:
    web:
      exposure:
        include: health, info, prometheus, metrics
  endpoint:
    health:
      show-details: when-authorized
      probes:
        enabled: true
      group:
        readiness:
          include: readinessState, db, redis
  server:
    port: 8081   # actuator'ı ana trafikten ayır

Liveness ve readiness farklı soruları cevaplar ve Kubernetes'te farklı tepkiler doğurur:

LivenessReadiness
Soru"Uygulama kendi iç durumu bozulmuş, kurtulamayacak halde mi?""Şu an trafik almaya hazır mı?"
Başarısız olunca KubernetesContainer'ı yeniden başlatırPod'u Service endpoint'lerinden çıkarır, öldürmez
İçermesi gerekenSadece uygulamanın kendi durumu (livenessState)Uygulama durumu + trafiği karşılamak için şart olan bağımlılıklar
İçermemesi gerekenVeritabanı, Redis, başka servislerOpsiyonel bağımlılıklar

Spring Boot dokümantasyonu bu konuda nettir: liveness dış sistemlere bağlı olmamalıdır. Veritabanı 30 saniyeliğine erişilemez olursa ve liveness DB'ye bakıyorsa, Kubernetes tüm pod'ları aynı anda yeniden başlatır; DB geri geldiğinde yüzlerce pod aynı anda açılıp bağlantı havuzlarını doldurmaya çalışır. Bu, kısmi bir arızayı tam bir kesintiye çeviren klasik cascading failure'dır.

Readiness'a DB eklemek ise tartışmalıdır: tüm pod'lar aynı DB'yi kullanıyorsa ve DB gittiyse hepsi birden "not ready" olur, Service'in hiç endpoint'i kalmaz ve istemciler bağlantı hatası alır. Yine de bu, 500 dönmekten çoğu zaman daha iyidir ve DB'si olmadan hiçbir işi yapamayan bir servis için mantıklıdır. Karar, bağımlılığın kritikliğine göre verilir.

Probe'ların varsayılan durumu sürüme göre değişir: Spring Boot 3.x'te liveness ve readiness health grupları yalnızca uygulama Kubernetes ortamında çalıştığı algılanırsa otomatik açılır, başka ortamlarda management.endpoint.health.probes.enabled=true ile açılır. Spring Boot 4'te ise varsayılan olarak her ortamda açıktır. Endpoint'ler /actuator/health/liveness ve /actuator/health/readiness'tır.

Uygulama kendi durumunu ApplicationAvailability üzerinden yönetebilir:

java
@Component
public class KafkaLagReadinessGuard {

    private final ApplicationEventPublisher publisher;

    public KafkaLagReadinessGuard(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void onCriticalBacklog() {
        // Pod'u öldürme, sadece yeni HTTP trafiğini kes.
        AvailabilityChangeEvent.publish(publisher, this, ReadinessState.REFUSING_TRAFFIC);
    }

    public void onBacklogCleared() {
        AvailabilityChangeEvent.publish(publisher, this, ReadinessState.ACCEPTING_TRAFFIC);
    }
}

Metrikler: Actuator, Micrometer üzerinden JVM, HTTP, Hikari, Kafka client metriklerini otomatik toplar; micrometer-registry-prometheus eklenince /actuator/prometheus açılır. Kendi iş metriklerin (orders.placed, outbox.lag) için MeterRegistry enjekte edilir.

Güvenlik: Actuator endpoint'leri hassas bilgi taşır (env, heapdump, threaddump). Ayrı bir management portu (management.server.port) kullanıp bu portu cluster dışına açmamak ve sadece gerekenleri expose etmek temel kuraldır.

Graceful shutdown

Bir pod kapatılırken (deploy, scale-down, node bakımı) yarım kalan istekler müşteriye 502 olarak döner. Graceful shutdown, sunucunun yeni isteği kabul etmeyi bırakıp mevcut istekleri bitirmesine süre tanır.

Spring Boot 2.3 ile gelen graceful shutdown, Boot 3.4'ten beri varsayılan olarak açıktır (Tomcat, Jetty, Reactor Netty; servlet ve reactive). Kapatmak için server.shutdown=immediate. Bekleme süresi spring.lifecycle.timeout-per-shutdown-phase ile ayarlanır (varsayılan 30 saniye).

Kubernetes'te kapanış sırası ve Boot'un rolü:

diyagram yükleniyor…
sequenceDiagram
    participant K as Kubernetes
    participant E as Endpoints / kube-proxy
    participant P as Pod (Spring Boot)
    K->>P: preStop hook (ör. sleep 10)
    K->>E: Pod'u endpoint listesinden çıkar (paralel, gecikmeli yayılır)
    Note over P: preStop sırasında hâlâ trafik gelebilir ve karşılanır
    K->>P: SIGTERM
    P->>P: Web server yeni bağlantı kabul etmez, mevcut istekleri bitirir
    P->>P: SmartLifecycle bean'leri durur (Kafka listener'lar), @PreDestroy
    K-->>P: terminationGracePeriodSeconds (varsayılan 30s) dolarsa SIGKILL

İki pratik ayrıntı:

  • preStop sleep. Kubernetes pod'u endpoint'lerden çıkarma ile SIGTERM'i paralel yürütür; kube-proxy ve ingress'lerin güncellenmesi birkaç saniye sürer. SIGTERM'i hemen alan uygulama yeni bağlantıları reddederse, henüz güncellenmemiş load balancer'lardan gelen istekler hata alır. preStop: sleep 5-10 bu boşluğu kapatır.
  • Süre bütçesi. preStop + timeout-per-shutdown-phase toplamı terminationGracePeriodSeconds'tan küçük olmalıdır; yoksa Kubernetes uygulamayı temiz kapanış bitmeden öldürür.

Kafka consumer'ları için de aynı mantık geçerlidir: listener container'ları SmartLifecycle'dır; kapanışta poll döngüsünü durdurur, işlenen kaydı bitirip offset'i commit eder. İşlem bir kayıt için 40 saniye sürebiliyorsa 30 saniyelik bütçe yetmez ve kayıt yeniden işlenir (consumer'ın idempotent olması bu yüzden şarttır, bkz. Bölüm 13).

Spring Boot 3 ile gelenler

Boot 3.0 (Kasım 2022) büyük bir kırılma sürümüydü:

  • Java 17 minimum. Record, sealed class, text block ve pattern matching özgürce kullanılabilir.
  • Jakarta EE 9+ (javax → jakarta). javax.persistence.* → jakarta.persistence.*, javax.servlet → jakarta.servlet, javax.validation → jakarta.validation. Sadece import değişikliği gibi görünür, ama transitif bağımlılıklarda javax kullanan eski kütüphaneler (eski bir SOAP client, eski bir Swagger sürümü) uyumsuz kalır; geçişin asıl zor kısmı budur.
  • Spring Framework 6.
  • Observability: Spring Cloud Sleuth yerine Micrometer Tracing; Micrometer Observation API ile metrik ve trace aynı instrumentation'dan üretilir.
  • ProblemDetail (RFC 7807, sonrasında RFC 9457) ile standart hata gövdesi; spring.mvc.problemdetails.enabled.
  • AOT ve GraalVM native image desteği.
  • Auto-configuration için spring.factories kaldırıldı, AutoConfiguration.imports zorunlu.
  • Bazı property'ler yeniden adlandırıldı (ör. spring.redis.* → spring.data.redis.*). Geçişte spring-boot-properties-migrator bağımlılığı eski property'leri açılışta raporlar ve geçici olarak eşler.

3.x serisinin önemli eklemeleri:

SürümEkleme
3.1Testcontainers için @ServiceConnection, Docker Compose desteği
3.2RestClient ve JdbcClient (Framework 6.1), virtual thread desteği (spring.threads.virtual.enabled=true, Java 21+)
3.4Graceful shutdown varsayılan açık, structured logging (ECS, GELF, Logstash formatları)

RestClient, RestTemplate'in akıcı (fluent) API'li senkron halefidir; WebClient'ın reactive bağımlılığı olmadan aynı ergonomiyi verir:

java
@Component
public class CargoClient {

    private final RestClient restClient;

    public CargoClient(RestClient.Builder builder, CargoProperties props) {
        this.restClient = builder.baseUrl(props.baseUrl()).build();
    }

    public ShipmentQuote quote(long productId, BigDecimal weightKg) {
        return restClient.get()
                .uri("/quotes?productId={id}&weight={w}", productId, weightKg)
                .retrieve()
                .body(ShipmentQuote.class);
    }
}

Timeout'ları ayarlamayı unutma: varsayılan HTTP client'ların timeout'ları ya çok uzun ya sınırsızdır. RestClient.Builder'a connect ve read timeout'u tanımlı bir ClientHttpRequestFactory ver; Boot bunun için ClientHttpRequestFactorySettings gibi yardımcılar ve sürüme göre değişen spring.http.client.* property'leri sunar (kullandığın sürümün dokümanından kontrol et). Timeout'suz bir client, yavaş bir kargo API'sinde tüm istek thread'lerini tıkayabilir.

JdbcClient, JdbcTemplate ve NamedParameterJdbcTemplate üzerine akıcı bir API'dir; Boot, NamedParameterJdbcTemplate varsa onu otomatik kurar.

Virtual thread'ler (spring.threads.virtual.enabled=true) Tomcat'in istek thread'lerini ve Boot'un auto-configure ettiği applicationTaskExecutor gibi executor'ları virtual thread'e geçirir. Bloklayan I/O ağırlıklı servislerde aynı donanımla çok daha fazla eşzamanlı istek taşınabilir. Dikkat edilecekler: Java 21-23'te synchronized blok içinde bloklayan çağrı carrier thread'i "pin"ler (Java 24'te JEP 491 ile büyük ölçüde çözüldü); bağlantı havuzu (Hikari) hâlâ sınırlıdır, yani 10.000 virtual thread 10 bağlantı için sıraya girer. Darboğaz thread'den veritabanına kayar.

Spring Boot 4 hakkında

Spring Boot 4.0, Kasım 2025'te Spring Framework 7 ile birlikte çıktı. Resmi migration guide'a göre öne çıkanlar:

  • Java 17 minimum kaldı; en güncel LTS (Java 21 veya 25) önerilir.
  • Jakarta EE 11 hizası: Servlet 6.1 ve Jakarta Persistence 3.2 baseline. Undertow, Servlet 6.1 ile henüz uyumlu olmadığı için desteklenmiyor.
  • Modüler auto-configuration ve yeniden adlandırılmış starter'lar (spring-boot-starter-webmvc gibi); geçiş için "classic" starter'lar.
  • Jackson 3 tercih edilen JSON kütüphanesi. Paket adı com.fasterxml.jackson → tools.jackson; Jackson2ObjectMapperBuilderCustomizer → JsonMapperBuilderCustomizer. Jackson 2 desteği deprecated olarak sürüyor.
  • Liveness ve readiness probe'ları varsayılan açık.
  • HTTP service client'lar (@HttpExchange arayüzleri) için auto-configuration ve API versioning desteği.
  • @MockBean ve @SpyBean kaldırıldı; yerine Spring Framework'ün @MockitoBean ve @MockitoSpyBean'i.
  • Null-safety anotasyonları JSpecify'a geçti.
  • Spring Retry'ın bağımlılık yönetimi kaldırıldı; Spring Framework 7 çekirdeğe @Retryable ve @ConcurrencyLimit gibi resilience özellikleri ekledi.

Mülakatta Boot 4 ayrıntısı nadiren sorulur; sorulursa "Boot 3'teki javax→jakarta gibi büyük bir kırılma değil, ama Jackson 3 ve modülerleşme geçişin en çok emek isteyen kısmı" demek yeterli bir çerçevedir.

Quiz'deki sorular: çözüm ve nedenleri

j23: Spring Boot ile Spring Framework farkı ne?

Senaryo: Ekibe yeni katılan biri soruyor: "Boot, Spring'in yerine mi geçti? Boot olmasaydı aynı REST + PostgreSQL servisini nasıl kurardık?"

Beklenen cevap, konu anlatımının ilk iki başlığının özetidir:

  • Framework çekirdektir: IoC/DI, AOP, MVC/WebFlux, transaction yönetimi, veri erişim soyutlamaları.
  • Boot, Framework'ün üzerine kurulu opinionated bir katmandır, yerine geçmez. Ekledikleri: auto-configuration (classpath ve koşullara göre bean'ler), starter'lar ve BOM ile uyumlu sürüm yönetimi, embedded server ve çalıştırılabilir jar, externalized configuration ve profiller, Actuator ve graceful shutdown.
  • @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan.
  • Boot olmadan: sürüm uyumu, DispatcherServlet kaydı, WAR + harici Tomcat, DataSource/pool/EntityManagerFactory/TransactionManager/ObjectMapper bean'leri, property bağlama ve health endpoint'leri elle yapılırdı.

Cevabı güçlendiren noktalar: Boot'un "sihir" olmadığını, koşullu bean tanımları olduğunu söylemek; --debug raporuyla bunun görülebileceğini eklemek; kendi bean'ini tanımlayınca Boot'un geri çekildiğini (@ConditionalOnMissingBean) belirtmek. Zayıf cevap "Boot, Spring'in daha kolay hali" deyip durur.

Production karşılığı: Boot'un varsayılanları "makul" ama "senin için doğru" olmak zorunda değil. Hikari pool boyutu (varsayılan 10), HTTP client timeout'ları, Jackson'ın bilinmeyen alan davranışı, OSIV'in açık gelmesi (bkz. Bölüm 8) gibi varsayılanları bilinçli gözden geçirmek senior beklentisidir.

j24: Konfigürasyon: hangi değer kazanır?

Senaryo: application.yml'da server.port: 8080 ve spring.datasource.url var, prod'da prod profili aktif, servis Kubernetes'te env değişkenleriyle çalışıyor.

Doğru cevaplar: a, b, c, f.

  • a) --server.port=9090 yml'deki değeri ezer. Doğru. Komut satırı argümanları, test kaynakları ve DevTools hariç en yüksek önceliklidir.
  • b) SPRING_DATASOURCE_URL relaxed binding ile bağlanır ve dosyayı ezer. Doğru. OS ortam değişkenleri config data dosyalarının üstündedir.
  • c) application-prod.yml aynı anahtar için application.yml'ı ezer. Doğru, aynı konumda (jar içi veya jar dışı) olduklarında. Jar dışındaki genel bir application.yml, jar içindeki application-prod.yml'ı ezer; konum, profilden daha güçlüdür.
  • f) @Profile("!prod") prod dışındaki tüm profillerde aktif olur. Doğru. Profil ifadeleri !, &, | destekler. Not: hiçbir profil aktif değilken default profili aktiftir ve !prod koşulu da sağlanır; yani profil vermeyi unutan bir prod deploy'u sahte gateway'i açar. Bu yüzden sahte bileşenleri @Profile("!prod") yerine pozitif bir profile (@Profile("local | test")) bağlamak daha güvenlidir.

Yanlışlar:

  • d) Tipli, doğrulanabilir konfigürasyon için @Value daha uygundur. Yanlış. @ConfigurationProperties (record ile immutable) tip güvenliği, @Validated ile açılışta doğrulama, IDE metadata'sı ve tam relaxed binding verir.
  • e) Repo private ise DB şifresini yml'e commit etmek kabul edilebilir. Yanlış. Git geçmişi kalıcıdır ve erişim zamanla genişler; secret'lar Kubernetes Secret, Vault veya bulut secret yöneticisinden gelir.

Quiz'deki öncelik listesine düzeltme: Quiz'in sadeleştirilmiş listesinde "OS ortam değişkenleri, Java system property'leri" aynı satırda verilmiş. Resmi sırada Java system property'leri, OS ortam değişkenlerinden daha önceliklidir (-Dserver.port=9090, SERVER_PORT=8081'i ezer). Ayrıca test kaynakları kendi içinde properties attribute → @DynamicPropertySource → @TestPropertySource sırasıyla güçlenir. Tam liste konu anlatımında.

q6: Kendi RedisTemplate bean'imi tanımladım, Boot ne yapar?

Senaryo: spring-boot-starter-data-redis eklendi. Bir @Configuration sınıfında @Bean RedisTemplate<String, Object> redisTemplate(...) ve kendi @Bean ObjectMapper'ın tanımlandı.

Doğru cevap: b. Auto-configuration koşullarla korunur; senin bean'in olduğu için Boot geri çekilir ve seninkini kullanır. Kendi ObjectMapper'ını tanımlamak ise Boot'un Jackson ayarlarını (spring.jackson.*, otomatik modül kaydı) devre dışı bırakır; doğru yol customizer'dır.

Önemli bir ayrıntı (quiz'de eksik): RedisAutoConfiguration'daki koşul tipe göre değil isme göredir:

java
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { ... }

@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { ... }

Quiz'deki senaryoda metodun adı redisTemplate olduğu için Boot geri çekilir. Ama metodu orderCacheTemplate diye adlandırsaydın Boot kendi redisTemplate bean'ini (RedisTemplate<Object, Object>, JDK serileştirmeli) de oluştururdu; context'te iki template olurdu. RedisTemplate<String, Object> isteyen enjeksiyonlar generic tip eşleşmesiyle seninkini bulur, ama RedisTemplate<Object, Object> isteyen bir kütüphane Boot'unkini alır ve Redis'e okunaksız JDK-serialized byte'lar yazılır; ham (raw) RedisTemplate isteyen bir enjeksiyon ise birden çok aday bulur ve isim eşleşmesi yoksa NoUniqueBeanDefinitionException ile açılmaz. Ders: "Boot geri çekilir" genellemesi doğrudur, ama hangi koşulla geri çekildiğini condition report'tan veya auto-configuration kaynağından kontrol etmek gerekir. StringRedisTemplate ise tipe göre korunur.

Yanlışlar:

  • a) Boot her zaman kendi bean'lerini de oluşturur, BeanDefinitionOverrideException ile açılmaz. Auto-configuration geri çekilmek için tasarlanmıştır. Bu exception, aynı isimde iki tanım kaydedilmeye çalışıldığında ve override kapalıyken (Boot 2.1+ varsayılanı) çıkar; auto-config ile kullanıcı bean'i arasında normalde oluşmaz çünkü koşul sağlanmaz ve Boot'un tanımı hiç kaydedilmez.
  • c) Auto-config sınıfları org.springframework.boot paketlerinin component scan'iyle bulunur. Yanlış. AutoConfiguration.imports dosyalarından okunur; component scan ile hiçbir ilgisi yoktur ve olmamalıdır.
  • d) Kullanıcı @Bean'leri auto-config'lerden sonra işlenir ve onların üzerine yazar. Sıralama tam tersidir: auto-config'ler DeferredImportSelector ile kullanıcı tanımlarından sonra değerlendirilir; @ConditionalOnMissingBean bu sayede "kullanıcı zaten tanımladı mı?" sorusunu cevaplayabilir.

Quiz'deki bir ifadeye küçük düzeltme: Quiz AutoConfiguration.imports için "Boot 2.7+, eski spring.factories yerine" diyor. Doğru, ama 2.7'de iki mekanizma birlikte destekleniyordu; auto-configuration için spring.factories desteği Boot 3.0'da kaldırıldı.

Daha iyi çözüm: RedisTemplate'i tanımlarken serializer'ları açıkça ver ve Boot'un ObjectMapper'ını kullan:

java
@Configuration(proxyBeanMethods = false)
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory,
                                                       ObjectMapper objectMapper) {   // Boot'un mapper'ı
        var template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        var valueSerializer = new GenericJackson2JsonRedisSerializer(objectMapper.copy());
        template.setValueSerializer(valueSerializer);
        template.setHashValueSerializer(valueSerializer);
        return template;
    }
}

objectMapper.copy() ile Boot'un ayarlarını koruyan ama Redis'e özgü değişiklikleri (ör. polymorphic tip bilgisi) HTTP katmanına sızdırmayan ayrı bir kopya kullanılır.

Üç seviyede cevap

Soru: "Spring Boot auto-configuration nasıl çalışır?"

Spring Boot classpath'e bakıp gerekli bean'leri otomatik oluşturuyor. Örneğin spring-boot-starter-data-jpa eklediğimde DataSource ve repository'ler kendiliğinden geliyor. @SpringBootApplication anotasyonu bunu sağlıyor.

@SpringBootApplication içindeki @EnableAutoConfiguration, jar'lardaki AutoConfiguration.imports dosyalarından auto-config sınıflarını yükler. Bu sınıflar @ConditionalOnClass, @ConditionalOnMissingBean, @ConditionalOnProperty gibi koşullarla korunur. Örneğin Hikari classpath'teyse ve ben DataSource tanımlamadıysam Boot kendi DataSource'unu kurar; tanımladıysam geri çekilir. Değerleri spring.datasource.* gibi @ConfigurationProperties sınıflarından alır. Neyin neden yüklendiğini --debug ile görürüm.

Üç parça: keşif, koşul, ayar. Keşif AutoConfiguration.imports dosyalarıyla olur ve bir DeferredImportSelector üzerinden çalışır; yani auto-config'ler benim tüm tanımlarım işlendikten sonra değerlendirilir. Bu sıra, @ConditionalOnMissingBean'in güvenilir olmasının sebebi; aynı koşulu kendi @Configuration'ımda kullanırsam sonuç tanım sırasına bağlı ve kırılgan olur. Koşulların detayına bakarım: RedisTemplate isme göre, StringRedisTemplate tipe göre korunuyor; metot adını değiştirince iki template oluştuğunu bir projede bu yüzden gördük. Ezmek gerektiğinde önce property, sonra customizer, en son kendi bean'imi tanımlarım, çünkü bean'i değiştirmek Boot'un o bean'e uyguladığı tüm ayarları kaybettirir; ObjectMapper bunun klasik örneği. Hata ayıklamada --debug raporu ve /actuator/conditions kullanırım; şirket içi starter yazarken her bean'i @ConditionalOnMissingBean ile korur, bir kapatma property'si koyar ve ApplicationContextRunner ile koşulları test ederim. Boot'un varsayılanlarının "makul" ama bizim yükümüz için doğru olmayabileceğini de bilirim: pool boyutu, client timeout'ları, OSIV gibi değerleri açıkça yazarım.

Junior "otomatik" kelimesinde kalıyor; mekanizma yok. Mid parçaları doğru sayıyor. Senior sıralamanın (deferred import) neden önemli olduğunu, koşulların ayrıntısının nasıl sürpriz yaratabileceğini, ezme stratejilerinin maliyet sırasını ve starter yazarken test yöntemini anlatıyor. "Varsayılanlara körü körüne güvenmem" vurgusu da senior işareti.

Takip sorusu: "Şirket içi starter'ın bir servisin kendi DefaultErrorHandler'ı ile çakışıyor. Nasıl tasarlardın ki ikisi de mutlu olsun?"

Soru: "Aynı property birden çok yerde tanımlıysa hangisi kazanır? Prod'da secret'ları nasıl yönetirsin?"

application-prod.yml varsa o kullanılır, yoksa application.yml. Şifreleri de application-prod.yml'a yazıyoruz, prod dosyasına sadece yetkililer erişiyor.

Öncelik sırası kabaca: komut satırı, Java system property, ortam değişkeni, sonra dosyalar; dosyalarda profile özel olan genel olanı, jar dışındaki jar içindekini ezer. Kubernetes'te ortam değişkenleri yml'ı ezer; SPRING_DATASOURCE_URL relaxed binding ile bağlanır. Secret'ları git'e koymam; Kubernetes Secret'tan env olarak veririm.

Sıra: varsayılanlar ve @PropertySource en altta, sonra config dosyaları, env, system property, SPRING_APPLICATION_JSON, komut satırı, en üstte test kaynakları. Dosyalarda konum profilden güçlü: jar dışındaki genel dosya jar içindeki profil dosyasını bile ezer. Bir değerin nereden geldiğinden emin değilsem tahmin etmem, /actuator/env'de property kaynaklarını görürüm. Tasarımım şu: imaj tek, ortam farkları ConfigMap'ten env ile, profil sadece "local/test/gerçek altyapı" ayrımı için. Tüm ayarlar @Validated record'larda; yanlış bir değer pod'u açılışta düşürür, readiness'a hiç ulaşmaz ve rollout durur. Secret'lar için env basit ama process ortamında ve dump'larda görünür; volume mount + configtree: veya Vault ile kısa ömürlü DB kimlik bilgilerini tercih ederim. Bankacılıkta secret erişiminin denetim izi gerektiği için Vault zorunluydu. Ayrıca rotasyonu tasarlarım: şifre değişince pod'ları sırayla yeniden başlatan bir mekanizma mı, yoksa dinamik credential mı? Bir de yan etkiyi kontrol ederim: properties record'unun toString()'i log'a düşerse secret sızar, Actuator'da maskelemeyi kapatmam.

Junior iki ciddi hata yapıyor: sırayı yalnızca profil üzerinden düşünüyor ve secret'ı dosyaya yazıyor. Mid sırayı ve doğru secret kaynağını biliyor. Senior sırayı resmi listeyle ve "konum > profil" inceliğiyle veriyor, belirsizlikte nasıl doğrulayacağını söylüyor, konfigürasyonu deploy güvenliğine (açılışta doğrulama, rollout'un durması) bağlıyor, secret yöntemlerini maliyet ve denetim açısından karşılaştırıyor ve rotasyonu düşünüyor.

Takip sorusu: "Prod'da checkout.payment-timeout'u deploy etmeden değiştirmen gerekiyor. Seçeneklerin neler?"

Soru: "Liveness ve readiness probe farkı nedir? Liveness'a veritabanı kontrolü koyar mısın?"

Liveness uygulamanın ayakta olup olmadığını, readiness hazır olup olmadığını kontrol eder. Liveness'a veritabanını koyarım, çünkü veritabanı yoksa uygulama zaten çalışamaz.

Liveness başarısız olursa Kubernetes container'ı yeniden başlatır, readiness başarısız olursa pod'u trafikten çıkarır. Liveness'a dış bağımlılık koymam; DB gidince tüm pod'lar yeniden başlar ve bir şey düzelmez. DB'yi readiness grubuna eklerim. Spring Boot'ta /actuator/health/liveness ve /readiness endpoint'leri var.

Liveness "bu process kendini kurtaramaz, öldür" sinyalidir; sadece uygulamanın iç durumuna bakmalı: deadlock'a girmiş bir executor, bozulmuş bir iç state. DB'yi liveness'a koymak, 30 saniyelik bir DB failover'ını tüm pod'ların aynı anda yeniden başladığı ve sonra hepsinin aynı anda bağlantı açmaya çalıştığı bir thundering herd'e çevirir; Boot dokümantasyonu da liveness'ın dış sistemlere bağlı olmaması gerektiğini açıkça söyler. Readiness'a ne koyacağım bağımlılığın kritikliğine bağlı: DB'siz hiçbir işi yapamayan bir sipariş servisinde DB'yi readiness'a eklerim, ama Redis cache'i opsiyonelse eklemem; Redis giderse yavaşlarım ama cevap veririm. Tüm pod'ların aynı anda not-ready olmasının "Service'te endpoint kalmaması" anlamına geldiğini de bilerek karar veririm. Uzun açılışlar için liveness'ı agresif yapmak yerine startup probe kullanırım; açılış warm-up'ını runner'a koyarım ki readiness ancak o bittikten sonra açılsın. Uygulama kendi yükünü fark ettiğinde (ör. Kafka backlog'u kritik) AvailabilityChangeEvent ile geçici REFUSING_TRAFFIC yayınlayabilir. Probe'ları ana porttan ayrı bir management portuna alırım ve probe endpoint'lerinin kendisinin ucuz olduğundan emin olurum; her probe'da ağır bir DB sorgusu atmak da bir yük kaynağıdır. Boot 3.x'te probe'lar sadece Kubernetes algılanınca otomatik açılıyordu, Boot 4'te varsayılan açık; bunu açıkça konfigüre ederim.

Junior'ın cevabı doğrudan production kesintisi üretir: liveness'a DB koymak. Mid doğru ayrımı yapıyor. Senior bunu bir failure mode olarak anlatıyor (failover → toplu restart → thundering herd), readiness kararını bağımlılığın kritikliğine bağlıyor, "tüm pod'lar not-ready" yan etkisini biliyor ve startup probe, warm-up, management portu gibi operasyonel detayları ekliyor.

Takip sorusu: "Deploy sırasında her seferinde birkaç yüz 502 görüyorsun. Nereden başlarsın?"

Soru: "Spring Boot 2'den 3'e geçişte neler değişti, nelerle uğraştınız?"

Boot 3'te Java 17 zorunlu oldu ve javax paketleri jakarta oldu. IntelliJ ile import'ları değiştirdik.

Java 17 minimum, Jakarta EE geçişi (javax.persistence → jakarta.persistence), Spring Framework 6, Sleuth yerine Micrometer Tracing, ProblemDetail desteği ve native image desteği geldi. Bazı property'ler değişti, spring-boot-properties-migrator ile bulduk. Auto-config için artık AutoConfiguration.imports kullanılıyor.

Import değişikliği işin kolay kısmıydı; asıl iş transitif bağımlılıklardaydı. javax kullanan eski kütüphaneler (bizde eski bir SOAP client ve bir Swagger sürümü) Jakarta sürümüne geçmeden derleme geçse bile runtime'da ClassNotFoundException verdi; bu yüzden önce bağımlılık ağacını javax için taradık. Geçişi iki adımda yaptık: önce 2.7'ye çıkıp tüm deprecation uyarılarını temizledik, sonra 3.0'a geçtik; spring-boot-properties-migrator'ı geçici ekleyip açılış raporundaki property değişikliklerini düzelttik ve sonra kaldırdık. Şirket içi starter'larda spring.factories'ten AutoConfiguration.imports'a geçmek zorunluydu; unutulan bir starter sessizce yüklenmeyi bıraktığı için condition report ile her servisi doğruladık. Tracing tarafında Sleuth'tan Micrometer Tracing'e geçişte trace header formatını (B3 ve W3C) eski servislerle uyumlu tuttuk, yoksa servisler arası trace'ler kopuyordu. Sonraki fırsatları da değerlendirdik: 3.2 ile RestClient, virtual thread'leri bir servis üzerinde yük testiyle denedik ve darboğazın Hikari havuzuna kaydığını gördük. Boot 4 için de benzer bir plan: Jackson 3 paket değişikliği ve modüler starter'lar en çok emek isteyecek kısım.

Junior değişikliği "import'ları değiştirdik"e indiriyor. Mid değişiklik listesini doğru biliyor. Senior geçişi bir risk yönetimi süreci olarak anlatıyor: transitif bağımlılık taraması, ara sürüm stratejisi, starter kayıt değişikliğinin sessiz başarısızlığı, tracing uyumluluğu, yeni özellikleri ölçerek benimseme.

Takip sorusu: "Virtual thread'leri açtınız ve p99 latency kötüleşti. Ne bakarsın?"

Sorulmadı ama sorulabilir

Çalıştırılabilir jar ve container imajı

Boot'un ürettiği "fat jar" içinde uygulama sınıfları BOOT-INF/classes, bağımlılıklar BOOT-INF/lib altında durur ve özel bir JarLauncher bunları iç içe jar olarak yükler. Container imajı oluştururken tek katmanlı bir fat jar kopyalamak, her kod değişikliğinde 80 MB'lık bütün katmanın yeniden push edilmesi demektir. Boot 2.3'ten beri layered jar desteği bağımlılıkları, snapshot bağımlılıklarını ve uygulama kodunu ayrı katmanlara ayırır; değişen yalnızca küçük uygulama katmanı olur. spring-boot:build-image ise Cloud Native Buildpacks ile Dockerfile yazmadan katmanlı ve güvenlik güncellemesi alınabilir imaj üretir.

Mülakatta tek cümle: "Layered jar ile bağımlılık katmanı cache'te kalır, her deploy'da sadece birkaç yüz KB'lık uygulama katmanı değişir."

Açılış süresini kısaltmak

Açılış süresi Kubernetes'te ölçeklenme hızını ve deploy süresini belirler. Seçenekler ve bedelleri: gereksiz auto-config'leri exclude etmek (ucuz, etkisi sınırlı); spring.main.lazy-initialization (hızlı ama hatayı çalışma anına taşır, bkz. Bölüm 6); Class Data Sharing / AOT cache (JVM sınıf yükleme maliyetini azaltır, imaj hazırlığı ister); GraalVM native image (milisaniyeler içinde açılış ve düşük bellek, ama uzun build süresi, reflection kısıtları ve kütüphane uyumluluğu sorunları). Önce ölçerim: Actuator'ın startup endpoint'i (BufferingApplicationStartup ile) hangi bean'in ne kadar sürdüğünü gösterir.

Mülakatta tek cümle: "Optimizasyondan önce /actuator/startup ile hangi bean'in yavaş olduğunu ölçerim; çoğu zaman suçlu Boot değil, açılışta yapılan bir ağ çağrısıdır."

Konfigürasyonu deploy etmeden değiştirmek

Seçenekler: (1) ConfigMap'i güncelleyip pod'ları rolling restart etmek: basit ve öngörülebilir; (2) Spring Cloud Config + @RefreshScope veya /actuator/refresh: bean'leri yeniden oluşturur, ama hangi bean'in yenilendiğini anlamak zorlaşır ve pod'lar arasında farklı değerlerle çalışılan bir pencere oluşur; (3) feature flag sistemi (LaunchDarkly, Unleash veya şirket içi): iş kararları için (kampanyayı aç/kapat) en doğru araç. Timeout, pool boyutu gibi teknik ayarlar için (1), iş anahtarları için (3) tercih edilir.

Mülakatta tek cümle: "Teknik ayarlar için rolling restart, iş anahtarları için feature flag; sıcak refresh'i ancak etkisini iyi bildiğim yerde kullanırım."

Test slice'ları

@SpringBootTest tüm context'i açar. Slice anotasyonları sadece ilgili katmanın auto-config'lerini yükler: @WebMvcTest (controller, MockMvc, Jackson, validation; servisler @MockitoBean ile), @DataJpaTest (JPA, repository'ler; varsayılan olarak her test transaction'da ve sonunda rollback), @JsonTest, @RestClientTest. Hızlıdırlar ve katman sınırlarını zorlar. @DataJpaTest varsayılan olarak DataSource'u gömülü bir veritabanıyla değiştirmeye çalışır; PostgreSQL'e özgü davranışları test etmek için Testcontainers + @ServiceConnection kullanılır. Boot 3.4+'da @AutoConfigureTestDatabase container'dan gelen bir veritabanını algılar ve değiştirmez; daha eski sürümlerde veya veritabanı property ile verildiğinde @AutoConfigureTestDatabase(replace = NONE) gerekir.

Mülakatta tek cümle: "Controller'ı @WebMvcTest, repository'yi Testcontainers'lı @DataJpaTest ile, uçtan uca akışı az sayıda @SpringBootTest ile test ederim."

Structured logging

Boot 3.4'ten beri log'ları ECS, GELF veya Logstash formatında JSON olarak yazmak için ek kütüphane gerekmez (logging.structured.format.console=ecs). JSON log'lar Elasticsearch/Loki gibi sistemlerde alan bazlı aranabilir; MDC'deki trace id, correlation id gibi değerler otomatik alan olur. Düz metin log'u regex ile parse etmek hem kırılgan hem pahalıdır.

Mülakatta tek cümle: "Production'da log'ları structured JSON yazarım, trace id'yi her satıra koyarım; log'dan trace'e tek tıkla geçebilmek gerekir."

Actuator endpoint güvenliği

heapdump bellekteki her şeyi (secret'lar, müşteri verisi) içerir; env ve configprops maskelense bile yapılandırma hakkında bilgi sızdırır; shutdown (varsayılan kapalı) uygulamayı durdurur. Kural: management portunu ayır ve dışarı açma, sadece gereken endpoint'leri expose et, Spring Security ile koru. Boot dokümantasyonu shutdown ve heapdump dışındaki endpoint'lere erişimin varsayılan olarak kısıtsız olduğunu, ama HTTP üzerinden sadece health'in expose edildiğini belirtir; yani "expose" ile "erişim" ayrı kavramlardır.

Mülakatta tek cümle: "Actuator'ı ayrı bir management portunda, cluster içinde ve sadece gereken endpoint'lerle açarım; heapdump asla internete açık olmaz."

Sık yapılan hatalar

  • Boot'un bean'ini değiştirmek için kendi bean'ini sıfırdan tanımlamak → Boot'un o bean'e uyguladığı tüm property'ler ve varsayılanlar kaybolur (ObjectMapper → spring.jackson.* ve modüller gider) → önce property, sonra customizer; ancak gerekiyorsa kendi bean'in.
  • @ConditionalOnMissingBean'i kendi @Configuration sınıfında kullanmak → sonuç tanım sırasına bağlıdır, kırılgandır → bean koşullarını yalnızca @AutoConfiguration sınıflarında kullan.
  • Auto-config'lerin hep tipe göre geri çekildiğini varsaymak → bazıları isme göre korunur (redisTemplate) → condition report'a veya kaynağa bak.
  • Auto-configuration sınıfını component scan'e açık bir pakete koymak → koşullar ve sıralama devre dışı kalır → ayrı paket + AutoConfiguration.imports.
  • Öncelik sırasını yalnızca profil üzerinden düşünmek → env, system property ve komut satırı dosyaları ezer; jar dışındaki dosya jar içindeki profil dosyasını ezer → resmi sırayı bil, şüphede /actuator/env'e bak.
  • Secret'ı application-prod.yml'a yazmak → git geçmişinde kalıcıdır → Kubernetes Secret (tercihen volume/configtree), Vault veya bulut secret yöneticisi.
  • Tipli ve gruplu ayarlar için onlarca @Value → doğrulama yok, birim isimde gizli, test zor → @Validated @ConfigurationProperties record.
  • Liveness'a veritabanı veya başka servis kontrolü koymak → dış arıza tüm pod'ların yeniden başlamasına ve cascading failure'a yol açar → liveness sadece iç durum; kritik bağımlılıklar readiness'a.
  • preStop olmadan graceful shutdown'a güvenmek → endpoint güncellemesi gecikir, kapanan pod'a istek gelir → preStop sleep ve terminationGracePeriodSeconds > preStop + shutdown timeout.
  • Actuator'ın tüm endpoint'lerini ana porttan * ile açmak → heapdump, env gibi hassas veriler açığa çıkar → ayrı management portu, minimum exposure, güvenlik.
  • @Profile("!prod") ile sahte bileşen açmak → profil verilmeyen bir prod deploy'unda default profil aktif olur ve sahte bileşen devreye girer → sahte bileşeni pozitif profile bağla (local | test).
  • HTTP client'larda timeout ayarlamamak → yavaş bir bağımlılık tüm istek thread'lerini tüketir → connect ve read timeout'larını açıkça ver.

Hızlı tekrar

S: @SpringBootApplication hangi üç anotasyonun birleşimidir?

C: @SpringBootConfiguration (bir @Configuration), @EnableAutoConfiguration ve @ComponentScan.

S: Auto-configuration sınıfları nereden keşfedilir?

C: Jar'lardaki META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports dosyasından (Boot 2.7+; Boot 3.0'dan beri tek yol). Component scan ile değil.

S: Kendi bean'imi tanımlayınca Boot neden geri çekilir?

C: Auto-config'ler kullanıcı tanımlarından sonra (deferred) değerlendirilir ve bean'leri @ConditionalOnMissingBean ile korunur; kullanıcı bean'i varsa koşul sağlanmaz ve Boot'un tanımı hiç kaydedilmez.

S: Hangi auto-config'in neden uygulandığını nasıl görürsün?

C: --debug ile açılışta Conditions Evaluation Report, çalışan uygulamada /actuator/conditions.

S: Komut satırı, Java system property, OS env ve application.yml arasında öncelik sırası nedir?

C: Komut satırı > Java system property > OS ortam değişkeni > config dosyaları.

S: Config dosyaları arasında hangisi kazanır?

C: Jar dışındaki profil dosyası > jar dışındaki genel dosya > jar içindeki profil dosyası > jar içindeki genel dosya. Aynı konumda .properties, .yml'ı ezer.

S: spring.main.log-startup-info ortam değişkeni olarak nasıl yazılır?

C: SPRING_MAIN_LOGSTARTUPINFO: noktalar alt çizgi olur, tireler silinir, büyük harfe çevrilir.

S: @ConfigurationProperties'in @Value'ya üç üstünlüğü nedir?

C: Tam relaxed binding ve tip güvenliği (Duration, DataSize, koleksiyonlar), @Validated ile açılışta doğrulama, IDE metadata'sı. @Value'nun tek üstünlüğü SpEL.

S: Liveness ve readiness başarısız olunca Kubernetes ne yapar?

C: Liveness: container'ı yeniden başlatır. Readiness: pod'u Service endpoint'lerinden çıkarır, öldürmez.

S: Liveness probe'una neden veritabanı kontrolü konmaz?

C: DB arızasında Kubernetes tüm pod'ları aynı anda yeniden başlatır; restart DB'yi düzeltmez, cascading failure ve thundering herd üretir.

S: Spring Boot'ta graceful shutdown varsayılan açık mı, süresi nasıl ayarlanır?

C: Boot 3.4'ten beri varsayılan açık (server.shutdown=immediate ile kapatılır). Süre spring.lifecycle.timeout-per-shutdown-phase ile (varsayılan 30s).

S: Kubernetes'te graceful shutdown için neden preStop sleep eklenir?

C: Pod'un endpoint'lerden çıkarılması ve SIGTERM paralel yürür; load balancer'lar geç güncellenir. Sleep, yeni isteklerin kesilmesine zaman tanır.

S: Boot 3.0'ın en büyük kırılma değişiklikleri nelerdir?

C: Java 17 minimum, javax → jakarta, Spring Framework 6, Sleuth yerine Micrometer Tracing, auto-config için spring.factories desteğinin kalkması.

S: RestClient, JdbcClient ve virtual thread desteği hangi Boot sürümüyle geldi?

C: Spring Boot 3.2 (Spring Framework 6.1); virtual thread'ler Java 21+ ve spring.threads.virtual.enabled=true ister.

S: Spring Boot 4'te Jackson tarafında ne değişti?

C: Jackson 3 tercih edilen kütüphane oldu; paket tools.jackson, customizer JsonMapperBuilderCustomizer. Jackson 2 desteği deprecated olarak sürüyor.

Kaynaklar