← Back to list

Spring Boot 3.5.x → 4.0.x Migration Rehberi

Versiyon numarasına bakınca 3.5'ten 4.0'a diye tek bir adım gibi görünüyor oysa Spring Boot 4.0 altında duran tüm ekosistemin eş zamanlı…

Tugce Dev Journal in MoneyPay · 2026-04-18 17:18 · 12 claps · 20.8 min read
#java #spring-boot #migration #spring #tomcat
Open on Medium ↗
Wiki topics: 🛠️ · Crafts & DIY

Spring Boot 3.5.x → 4.0.x Migration Rehberi

Versiyon numarasına bakınca 3.5'ten 4.0'a diye tek bir adım gibi görünüyor oysa Spring Boot 4.0 altında duran tüm ekosistemin eş zamanlı major upgradelerini kapsıyor. Spring Framework 7, Spring Security 7, Jackson 3, Hibernate 7.1, Jakarta EE 11, Tomcat 11 ve JUnit 6'nın hepsi bu release ile birlikte geliyor. Bu katmanların herhangi birinde yapılacak bir major upgrade bile tek başına dikkat ister. Düşünün hepsinin aynı anda çıkması demek bir Spring Boot upgrade’inin artık birkaç saatlik işlemden öte planlı bir refactor projesi haline gelmesi demek.

Üstüne bir de şunu ekleyin! Spring Boot 4 ile birlikte proje kendi içindeki modül yapısını baştan aşağıya yeniden düzenledi. Autoconfigure sınıfları tek büyük spring-boot-autoconfigure jar’ının altından çıkıp her teknoloji için ayrı modüllere dağıldı. Bu, derleme seviyesinde büyük bir kırılma üretmiyor çünkü starterlar eski isimlerini koruyor; ama application.properties içinde org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration gibi tam sınıf adı yazan her satırı kırıyor.

Bu yazıda, 3.5.13'teki bir uygulamayı 4.0.5'e taşırken karşılaşacağım değişiklikleri ve genel 3.5.x’ten 4.0.x’e geçerken karşılaşacağımız değişiklikleri sırayla ele alacağım. Kod seviyesinde bakmamız gereken yerler property rename’leri, sessiz davranış değişiklikleri ve migration sırasına dair öneriler dahil.

Baseline: Java, Jakarta EE, Kotlin

Spring Boot 4 minimum Java 17 istiyor; 3.x de aynısını istiyordu ama 4'te Java 21'in kullanımı çok daha değerli hale geldi çünkü virtual threads için auto-configuration geliyor. Prod için 21'e geçmediyseniz, bu upgrade güzel bir fırsat. Kotlin kullanıyorsanız minimum 2.2 gerekli, GraalVM native image için 25 ve sonrası.

Jakarta EE tarafında daha önemli bir şey var, Servlet 6.1 baseline. Bu, hâlâ javax.* paketlerinde bağımlılığı olan eski librarylerle yolunuzun ayrılması demek. 3.x'te çoğu ekip bu geçişi zaten yaptı ama Actuator tarafında dikkat edilmesi gereken bir nokta var. Endpoint parametrelerindeki org.springframework.lang.Nullable artık desteklenmiyor, org.jspecify.annotations.Nullable'a geçmek zorundasınız. Bu kırılma compile time hatası vermeyebilir ama endpoint çağrıldığında "null parameter not allowed" gibi runtime hatalarıyla karşılaşırsınız.

Paket Taşımaları

Spring Boot 4'ün en geniş etkili değişikliği bu oldu benim için. Her teknoloji kendi spring-boot-<technology> modülüne çıkarılmış ve autoconfigure sınıfları yeni paket yollarına taşınmış. Örneklerle anlatayım ama bakınca ya bunlar aynı değil mi demeyin diye önden tüyo verim autoconfigure önündeki kırılıma bakabilirsiniz:

  • org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationorg.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
  • org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationorg.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration
  • org.springframework.boot.autoconfigure.data.redis.RedisAutoConfigurationorg.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration (burada sınıf adı da değişmiş, "Data" prefix'i eklenmiş)
  • org.springframework.boot.autoconfigure.jackson.JacksonAutoConfigurationorg.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration
  • Web MVC için tüm org.springframework.boot.autoconfigure.web.servlet.*org.springframework.boot.webmvc.autoconfigure.*
  • Security için tüm org.springframework.boot.autoconfigure.security.*org.springframework.boot.security.autoconfigure.*

Bu değişikliklerin asıl sinsiliği derleme değil runtimeda ortaya çıkıyor. Java kodumda import yazdığım yerleri IDEnin auto-fixi temizledi. Ama şu yerlerde elle arama yapmak zorunda kaldım:

  • spring.autoconfigure.exclude satırı içeren properties/yml dosyaları,
  • @ImportAutoConfiguration({...}) kullanan slice testler,
  • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports dosyaları,
  • @SpringBootTest(classes = ...) içinde FQCN referansları,
  • @ConditionalOnClass(name = "...") kullanımları,
  • @WebMvcTest + excludeAutoConfiguration kombinasyonları.

Benim durumumda bu dosyaları manual bulup düzeltmek zorunda kaldım. Bir grep komutu yeterli ama atlanması çok kolay: grep -rn "org.springframework.boot.autoconfigure" src/.

Starter isimleri de bazı yerlerde değişmiş:

  • spring-boot-starter-webspring-boot-starter-webmvc (eski isim hâlâ çalışıyor ama deprecate edildi)
  • spring-boot-starter-oauth2-clientspring-boot-starter-security-oauth2-client
  • spring-boot-starter-oauth2-resource-serverspring-boot-starter-security-oauth2-resource-server
  • spring-boot-starter-oauth2-authorization-serverspring-boot-starter-security-oauth2-authorization-server
  • spring-boot-starter-aopspring-boot-starter-aspectj
  • spring-boot-starter-tomcat (WAR deploy için) → spring-boot-starter-tomcat-runtime
  • hibernate-jpamodelgenhibernate-processor (Maven'ın <annotationProcessorPaths> bloğunda elle güncellemem gerekti)

Geçiş sürecinde spring-boot-starter-classic diye geçici bir köprü buldum. Tüm eski bağımlılıkları getiriyor ve buildi hızlı yeşile çekiyor. Uzun vadede modüler starterlara bölmek gerek ama ilk geçişte işi çok kolaylaştırıyor. Ben yine de önermem hazır geçiş yaparken şimdiden temizleyin.

Jackson 2 → Jackson 3: En Yorucu Kırılma

Spring Boot 4'te birincil JSON işleyicisi Jackson 3. Bu sadece versiyon bumpı değil; groupId ve paket yolu baştan sona değişti. Jackson’ın group ID’si com.fasterxml.jackson değil, artık tools.jackson. Paket yolları da paralel:

  • com.fasterxml.jackson.databind.ObjectMappertools.jackson.databind.ObjectMapper
  • JsonObjectSerializerObjectValueSerializer
  • JsonValueDeserializerObjectValueDeserializer
  • Jackson2ObjectMapperBuilderCustomizerJsonMapperBuilderCustomizer
  • @JsonComponent@JacksonComponent
  • @JsonMixin@JacksonMixin

Tek istisna jackson-annotations paketi! com.fasterxml.jackson.core altında kalmış yani @JsonProperty, @JsonIgnore gibi tipik anotasyonlar import değiştirmeden çalışmaya devam ediyor. Ama databind tarafındaki her şey bozuluyor. Custom ObjectMapper bean tanımlayan projelerin, özel serializer/deserializer yazanların, ObjectMapper.readTree() tipi API kullananların kesin compile hatası aldığını gördüm.

Property rename’lerine de dikkat etmek lazım:

  • spring.jackson.read.*spring.jackson.json.read.*
  • spring.jackson.write.*spring.jackson.json.write.*
  • spring.jackson.parser.*spring.jackson.json.read.* (buraya birleşmiş)

Davranış tarafında iki önemli şey var. Birincisi Jackson 3 classpath’te bulduğu tüm modülleri otomatik register ediyor. Bu da sürpriz serialization davranışlarına yol açabiliyor. spring.jackson.find-and-add-modules=false ile devre dışı bırakılabilir. İkincisi Spring Data Redis'te GenericJackson2JsonRedisSerializer deprecate edildi, yerine GenericJacksonJsonRedisSerializer.builder().build() geldi. Yeni sınıfta no-arg constructor yok, builder pattern zorunlu. Ve çok önemli bir uyarı söylüyorum, bu iki serializer aynı binary formata yazmıyor. Yani prod cache'inizi flush etmeden upgrade yaparsanız, eski formatta yazılmış cache entrylerini deserialize edemediği için exceptionlar uçuşur.

Jackson 2 ile yan yana yaşamak isteyenler için spring-boot-jackson2 modülü var; spring.jackson.use-jackson2-defaults=true ile Jackson 2 davranışını taklit ediliyor. Bu geçici bir köprü yani geçmeye karar verirseniz benim tavsiyem bu işi de aradan çıkarın. Jersey 4.0 gibi Jackson 3'ü hâlâ desteklemeyen libraryler varsa mecbur kullanılır ama kalıcı çözüm değil.

Hibernate 7 / Spring Data

Bir diğer değişiklik de Hibernate 7.1 ve Hibernate Validator 9.0 değişiklikleri. Özellikle Mongo kullananlar adına biraz yorucu bir değişiklik olabilir. Hibernate annotation processor’ın artifact’i hibernate-jpamodelgen'den hibernate-processor'a dönüştü. Eğer eski isimli bir <path> bırakırsanız JPA metamodeli üretemez ve derleme sessizce çalışmaya devam etse bile type-safe queryleriniz bozulur. hibernate-proxool ve hibernate-vibur artık yayınlanmıyor dolayısyla bu connection poolları kullananların alternatif bulması gerekiyor.

Property tarafında spring.dao.exceptiontranslation.enabledspring.persistence.exceptiontranslation.enabled. MongoDB kullananlar için daha büyük bir reorganizasyon var. Connection-level propertyler spring.data.mongodb.*'tan spring.mongodb.*'a taşınmış, data-layer olanlar spring.data.mongodb.*'ta kalmış:

  • spring.data.mongodb.hostspring.mongodb.host
  • spring.data.mongodb.portspring.mongodb.port
  • spring.data.mongodb.databasespring.mongodb.database
  • spring.data.mongodb.usernamespring.mongodb.username
  • spring.data.mongodb.passwordspring.mongodb.password
  • spring.data.mongodb.urispring.mongodb.uri
  • spring.data.mongodb.ssl.enabledspring.mongodb.ssl.enabled
  • spring.data.mongodb.replica-set-namespring.mongodb.replica-set-name

auto-index-creation, field-naming-strategy, repositories.type gibi data-layer propertyleri yerinde duruyor. Bir de Spring Data MongoDB artık UUID ve BigDecimal için default representation sağlamıyor maalesef. spring.mongodb.representation.uuid ve spring.data.mongodb.representation.big-decimal değerlerini explicit set etmek gerekiyor.

Spring Security 7

Spring Authorization Server artık Spring Security 7'nin parçası yani ayrı bir version propertysi yok, spring-security.version kullanılıyor. OAuth2 starterları spring-boot-starter-security- prefixiyle yeniden adlandırıldı (yukarıda bahsettim). Testing tarafında kritik bir değişiklik var. @WithMockUser ve @WithUserDetails eskiden spring-boot-starter-test'in içinde geliyorlardı artık spring-boot-starter-security-test gerektiriyor. Sadece spring-boot-starter-test kullanıyorsanız ve security testleriniz varsa derleme hatası alırsınız.

Ayrıca Spring Security 7'nin kendi migration guide’ını da okudum ama bence herkes okumalı. Burada alışkanlıklarımı baştan aşağıya değiştirmek zorunda kaldığım birkaç yer oldu. SecurityFilterChain API'si, lambda DSL davranışı ve method security defaultları önemli değişiklikler içeriyor.

Lambda DSL Artık Zorunlu, .and() Yok

Spring Security 5.2'den beri lambda DSL destekleniyordu ama eski zincirleme DSL ile kullanabiliyorduk. 7'de eski stil tamamen kaldırılmış. Yani bu şekilde yazdığımız her config bozuluyor:

// Eski (7'de çalışmaz)
http
    .authorizeHttpRequests()
        .requestMatchers("/blog/**").permitAll()
        .anyRequest().authenticated()
        .and()
    .formLogin()
        .loginPage("/login")
        .permitAll()
        .and()
    .rememberMe();

// Yeni (7'de zorunlu)
http
    .authorizeHttpRequests(authorize -> authorize
        .requestMatchers("/blog/**").permitAll()
        .anyRequest().authenticated()
    )
    .formLogin(formLogin -> formLogin
        .loginPage("/login")
        .permitAll()
    )
    .rememberMe(Customizer.withDefaults());

authorizeRequests()authorizeHttpRequests()

Bu değişiklik 6.x boyunca deprecate edildi, 7'de tamamen kaldırıldı. Eski authorizeRequests() arkada AccessDecisionManager + voter patterni kullanırdı fakat yeni authorizeHttpRequests() ise AuthorizationManager API'sini kullanıyor. Runtime davranışı çoğunlukla aynı kalıyor ama custom AccessDecisionVoter yazdığımız yerlerde sıkıntı çıkıyor. Her biri AuthorizationManager<RequestAuthorizationContext> implementasyonu olarak yeniden yazılmak zorunda.

AntPathRequestMatcher ve MvcRequestMatcher Gitti → PathPatternRequestMatcher

Bu beni en çok şaşırtan kısımdı çünkü AntPathRequestMatcher'ı hemen hemen her projede görebiliriz. Artık her ikisi de kaldırılmış yerine PathPatternRequestMatcher gelmiş. Spring MVC tarafında zaten PathPattern matching stratejisi baskındı. Security de onunla hizalandı.

// Eski
http.securityMatcher(new AntPathRequestMatcher("/api/**"));

// Yeni
http.securityMatcher(PathPatternRequestMatcher.withDefaults().matcher("/api/**"));

Bu değişikliği özellikle requestMatchers(HttpMethod.GET, "/api/**") kullanımlarımda da kontrol etmem gerekti; lambda DSL içinden gelen kısayol API hâlâ çalışıyor ama altta PathPattern kullanıyor ve bazı regex-esque patternlerimin davranışı hafifçe değişebiliyor. Özellikle ** wildcard'ının trailing slash davranışı Ant ile tam aynı değil. Mutlaka integration testlerinizi yeniden koşmanızı öneririm.

AntPathMatcher’da eskiden

  • /api yazdım → hem GET /api hem GET /api/ geleni yakalardı

PathPattern’da

  • /api yazdım → sadece GET /api yakalar
  • /api/ gelen istekle eşleşmez, ayrı bir pattern yazmam lazım (/api/ diye ya da /api{sep:[/]?})

Bu, Spring 6.0'da AntPathMatcher için de değişti aslında (setUseTrailingSlashMatch deprecate edildi) ama PathPattern hiç böyle bir iyi niyet göstermiyor yani ne yazdıysan onu eşliyor. Pratikte başıma gelen durumu anlatacak olursam, security configimde requestMatchers("/health") yazmışım, frontend ya da load balancer health-check için /health/ istek atıyor. Ant zamanında ikisi de permitAll kuralına uyup geçiyordu. PathPattern'e geçince /health/ isteği anyRequest().authenticated() kuralına düşüp 401 almaya başladı. Integration testlerim sadece /health'e atıyordu, trailing slash'i test etmiyordu. Bilin bakalım bunu nerde fark ettim tabiki de productionda yakaladım.

Ayrıca daha tehlikeli olan kısım. Ant’ta şunu yazabilirdim:

// Ant'ta geçerli
"/api/**/admin/**"

Yani api altında herhangi bir derinlikte admin varsa onu ve altındaki her şeyi yakala diyebiliyordum. Bunu neden kaldırmışlar bilmiyorum ama PathPattern bunu reddediyor, application startta bir exception atıp ayağa kalkmıyor.

PatternParseException: No more pattern data allowed after {*...} or ** pattern element

PathPattern’da ** sadece patternin sonunda bulunabiliyor. Ortada ** yazmak istiyorsam iki seçeneğim var:

  • Eğer tek bir path segmenti yeterliyse *'a çevir: /api/*/admin/** (api'den sonra tek segment, sonra admin ve altı)
  • Gerçekten derinlik lazımsa birden fazla matcher yazmam gerek: /api/admin/**, /api/*/admin/**, /api/*/*/admin/**... ya da regex tabanlı bir matchera geçmem gerek.

Custom DSL: apply()with()

Kendi AbstractHttpConfigurer implementasyonumu eklediğim yerlerde eski API'yi kullanıyordum. 6.2'den itibaren .apply() deprecate edilmişti ama 7'de tamamen kaldırıldı.

// Eski
http.apply(new MyCustomDsl());

// Yeni
http.with(new MyCustomDsl(), Customizer.withDefaults());
// veya özelleştirmek istersem:
http.with(new MyCustomDsl(), dsl -> dsl.someOption(true));

shouldFilterAllDispatcherTypesdispatcherTypeMatchers

Error dispatchlerin nasıl değerlendirileceğini kontrol ettiğim yer değişmiş. Bu semantik olarak daha temiz çünkü “hangi dispatcher typeları nasıl yetkilendireyim” sorusunu net sorabiliyorum.

// Eski
http.authorizeHttpRequests(auth -> auth
    .shouldFilterAllDispatcherTypes(false)
    .anyRequest().authenticated()
);

// Yeni
http.authorizeHttpRequests(auth -> auth
    .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll()
    .anyRequest().authenticated()
);

Method Security: @EnableGlobalMethodSecurity Tamamen Öldü

6.x boyunca iki farklı annotation vardı. Eski @EnableGlobalMethodSecurity(prePostEnabled = true) ve yeni @EnableMethodSecurity. 7'de eski olan tamamen kaldırılmış.

// Eski (7'de yok)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)

// Yeni (default'ta prePostEnabled=true zaten)
@EnableMethodSecurity(securedEnabled = true)

Method security artık default olarak AuthorizationManager patternine geçmiş. Bu da@PreAuthorize / @PostAuthorize gibi annotationların runtime davranışının sessizce değişebileceği anlamına geliyor. Özellikle custom PermissionEvaluator yazdığınız yerlerde API'leri AuthorizationManager<MethodInvocation> üzerinden tekrar entegre etmek gerekiyor. Eski AccessDecisionVoter tabanlı custom yetkilendirme kodunuz varsa spring-security-access modülünü dependency olarak ekleyerek geçici olarak yaşatabilirsiniz ama bu kalıcı çözüm değil. Hazır bu işe girişmişken bunu da güncellemenizi tavsiye ederim.

Bir diğer mikro değişiklik ise AuthorizationManager#check methodunun kaldırılmış olması. Yerine sadece 5.5'ten beri var olan #authorize kalmış. Custom implementasyonlarımı check yerine authorize döndürecek şekilde yazabilirsiniz.

OAuth2: PKCE Default Açık, Password Grant Öldü

OAuth2 client tarafında iki önemli default değişikliği yakaladım. PKCE (Proof Key for Code Exchange) artık default olarak enabled. Önceden istemli açmam gerekiyordu ama şimdi aksini istiyorsam açıkça disable etmeliyim. OAuth 2.0 Password Grant tipi tamamen kaldırıldı. RFC 6749'da da deprecate edilmişti zaten ama hâlâ kullanan bir entegrasyonlarınız varsa Authorization Code + PKCE’ye geçmek zorundasınız.

Resource server tarafında JWT doğrulama API’leri büyük oranda aynı kaldı ama OAuth2ResourceServerConfigurer'ın bazı builder metotları lambda DSL zorunluluğuna uyarlandı.

Jackson: SecurityJackson2Modules → SecurityJacksonModules

Session serialization veya RememberMe cookiesi için SecurityJackson2Modules'u kendi ObjectMapper'ıma register ettiğim yerlerde yeni sınıfa geçmem gerekti:

// Eski
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(loader));

// Yeni
JsonMapper.Builder builder = JsonMapper.builder();
SecurityJacksonModules.configure(builder, loader);
ObjectMapper mapper = builder.build();

Bu, Spring Boot 4 ana hattındaki Jackson 3 geçişiyle birebir hizalı.

WebFlux Tarafı da Aynı

ServerHttpSecurity (yani WebFlux'un imperative yerine reactive muadili) de aynı değişikliklere uğradı: lambda DSL zorunlu, .and() yok, authorizeExchange lambda içinde yazılıyor. Reactive ve imperative için tek migration stratejisi uygulayabiliyorum.

Testing: JUnit 6, Mockito, @MockBean Değişimi

JUnit 5'ten 6'ya geçiş tipik test kodunu şaşırtıcı derecede az etkileyecek gibi görünüyor. Core anotasyonlar (@Test, @BeforeEach, @DisplayName, @ExtendWith, Assertions) org.junit.jupiter.api.* altında aynı yerde duruyor. Kaldırılanlar tabiki de 4'den kalanlar olmuş: junit-platform-runner (JUnit 4 köprüsü olan @RunWith(JUnitPlatform.class)) ve junit-platform-jfr (Flight Recorder events). Platform artifactları artık Jupiter/Vintage ile aynı versiyon numarasıyla yayınlanıyor.

Asıl kırılma Mockito + Spring Boot entegrasyonunda olmuş. @MockBean ve @SpyBean Spring Boot 4'te tamamen kaldırıldı. Yerlerine @MockitoBean ve @MockitoSpyBean geldi. Bu değişiklik 3.4'te deprecate edilmişti ama 3.x boyunca kademeli olarak yeni anotasyona geçtiyseniz sorun çıkmaz. Geçmediyseniz tek bir search-replace ile yüzlerce test bozulabilir. Paylaşılan mocklar için yeni pattern @MockitoBean(types = { UserService.class, AuditService.class }) şeklinde class-level kullanımı gerekir.

Daha az görünür olan ama bana göre en tuzak dolu iki değişiklik var. @SpringBootTest artık MockMvc veya TestRestTemplate için otomatik auto-configuration yapmıyor. MockMvc için @AutoConfigureMockMvc'i açıkça eklemek zorundasınız. TestRestTemplate için @AutoConfigureTestRestTemplate + spring-boot-resttestclient test dependency + spring-boot-restclient runtime dependency gerekli ve paketi de değişti: org.springframework.boot.resttestclient.TestRestTemplate. Ekstra yeni bir test client geldi: RestTestClient (@AutoConfigureRestTestClient ile açılıyor). Sessiz tuzak şurada, eski @Autowired TestRestTemplate null almayıp "başlatılmadı" diye context fail ediyor ve hata mesajı net olmuyor.

Bir küçük anotasyon taşıması daha oldu. @PropertyMapping org.springframework.boot.test.autoconfigure.properties paketinden org.springframework.boot.test.context paketine geçti.

Actuator / Management

İki önemli default değişikliği yakaladım. Liveness ve readiness probeları artık default enabled. Eğer istemezseniz management.endpoint.health.probes.enabled=false ile kapatıyorsunuz.

MongoDB kullananlar için property rename’leri:

  • management.health.mongo.*management.health.mongodb.*
  • management.metrics.mongo.*management.metrics.mongodb.*

3.4'te deprecate edilen management.endpoint.<id>.enabled propertysi 4'te tamamen kaldırıldı yerini management.endpoint.<id>.access=read_only|unrestricted|none aldı.

Logging

Logback’in default charseti Log4j2 ile harmonize edildi. Bununla birlikte file appenderlar UTF-8, console appenderlar console un kendi charseti varsa onu, yoksa UTF-8 kullanıyor. Türkçe karakter veya emoji içeren log satırları yazıyorsanız Windows konsol ortamlarında görüntülenen hâli biraz farklılaşabilir. Log dosyasındaki bytelar doğru olur ama görsel olarak bozulabilir. Linux / Mac’te değişiklik fark etmez.

Observability: Micrometer, OpenTelemetry

Observability tarafı Spring Boot 4'te görece en hissedilen kırılmalardan biri oldu benim için. Hem yapısal hem de property-seviye değişiklikler var. Biraz daha detaylı anlatalım.

Actuator’dan Ayrılma

Spring Boot 3'te observability dünyası büyük oranda spring-boot-actuator monolitinin içinde yaşıyordu. Micrometer metric registryleri, observation API desteği, tracing bridgeleri hepsi actuator ayağa kalktığında devreye giriyordu. 4'te bu monolit parçalandı ve odaklı modüller haline geldi.

  • spring-boot-micrometer-observation → Observation API'nin core'u, tracing ve metrics'in dayandığı temel.
  • spring-boot-micrometer-tracing → tracing bridgeleri (Brave ve OpenTelemetry için iki ayrı varyant).
  • spring-boot-opentelemetry → yeni modül, OpenTelemetry SDK ve OTLP exporterlarını paketliyor.
  • spring-boot-zipkin → yeni modül, Zipkin exporterı artık ayrı.
  • spring-boot-starter-opentelemetry → bütün observability bağımlılıklarını tek satırla getiren yeni starter.

Bu ayrışmanın benim için pratik anlamı “sadece metric istiyorum, distributed tracing istemiyorum” diyen bir servis için artık OpenTelemetry jarlarını getirmek zorunda değilim. Eskiden her şey actuator altında bundlelanırdı.

Yeni Starter: spring-boot-starter-opentelemetry

En büyük değişiklik bu. Spring Boot 3'te bir servise OpenTelemetry entegre etmek için ortalama 4–5 dependency eklemem gerekiyordu:

<!-- Spring Boot 3 — manuel dependency coğrafyası -->
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-otlp</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
  <artifactId>opentelemetry-sdk</artifactId>
</dependency>
<!-- + actuator -->
<!-- Spring Boot 4'te tek satıra indi -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>

Bu starter içeride şunları getiriyor: OpenTelemetry API, micrometer-registry-otlp (metric export için), micrometer-tracing-bridge-otel (trace bridge için) ve OTLP span exporter. Yani tek satır dependency ile OpenTelemetry-ready bir uygulama kuruyorum.

Küçük bir not: bu starter actuator’a bağımlı değil. Actuator’un diğer özelliklerini (health endpoint, prometheus scrape endpoint vs.) istemiyorsam sadece observability için OpenTelemetry kullanmak istiyorsam spring-boot-starter-actuator olmadan da ayağa kaldırabiliyorum.

Üç Farklı Entegrasyon Yolu

Spring Boot 4 ile birlikte OpenTelemetry’yi uygulamama bağlamanın üç yolu ortaya çıktı. Hangisini seçeceğim projenin ihtiyacına göre değişiyor.

  • OpenTelemetry Java Agent (-javaagent:opentelemetry-javaagent.jar) → Zero code change, bytecode instrumentation ile her şeyi otomatik sarıyor. En kolay yol ama library versiyon uyumsuzluklarına karşı kırılgan.
  • OpenTelemetry’nin kendi Spring Boot Starter’ı (io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter) → OTel ekibinin resmi starterı, stable işaretli ama alpha dependency'leri pull ediyor.
  • Spring Boot’un kendi spring-boot-starter-opentelemetry'si → Spring ekibinin önerdiği yol. Micrometer Observation API'sini kullanıyor, OTLP bridge'i ile OpenTelemetry'ye yayın yapıyor.

Ben üçüncü yolu tercih ediyorum çünkü hem Spring ekibi destekliyor hem de Micrometer Observation API’sine zaten alıştım. @Observed anotasyonuyla kendi methodlarıma span ekleyebiliyorum. Ayrıca Spring ekosistemi (WebMVC, WebFlux, scheduler, data) otomatik instrument oluyor.

HTTP Client Otomatik Instrument: Büyük Tuzak

Bu çok önemli ve benim ilk sefer yanıldığım yer oldu. Spring Boot 4'te RestTemplate, RestClient ve WebClient otomatik instrument edilmek için builder üzerinden oluşturulmak zorunda artık.

// Yanlış — trace context propagation ÇALIŞMAZ
@Bean
RestTemplate restTemplate() {
    return new RestTemplate();
}

// Yanlış — WebClient aynı şekilde
@Bean
WebClient webClient() {
    return WebClient.create("http://upstream");
}

Doğrusu builder inject etmek:

// Doğru — otomatik instrument edilir, trace header'ları propagate olur
@Bean
RestTemplate restTemplate(RestTemplateBuilder builder) {
    return builder.build();
}

@Bean
RestClient restClient(RestClient.Builder builder) {
    return builder.baseUrl("http://upstream").build();
}

@Bean
WebClient webClient(WebClient.Builder builder) {
    return builder.baseUrl("http://upstream").build();
}

Spring Boot builderları auto-configure ediyor ve trace context propagation infrastructure’ını onların üzerine yerleştiriyor. Direkt new yazdığımda bu katmanı atlıyor, uygulama ayağa kalkıyor, metrik üretiyor ama tracelerim kopuyor yani upstream servise gönderdiğim HTTP isteğinde traceparent headerı yok dolayısıyla da distributed trace zincirim parçalanıyor. Bu sessizliği yakalamak için tracelerimi end-to-end kontrol etmem gerek. Jaeger/Tempo UI'da request görünüyor ama spanlar arasında parent-child ilişkisi yok. Bu yüzden eski yazım tarzımızı builderlara kesinlikle çevirmemiz gerektiğini düşünüyorum.

Property Değişiklikleri: OTLP Endpointleri

Spring Boot 4'te OTLP endpointleri için property hiyerarşisi net bir yapıya oturdu.

  • Metric export → management.otlp.metrics.export.url
  • Trace export → management.opentelemetry.tracing.export.otlp.endpoint
  • Log export → management.opentelemetry.logging.export.otlp.endpoint

Spring Boot 3.1'den beri spring-boot-docker-compose modülü var. Bu modül devde şunu yapıyor. Proje klasörümdeki compose.yaml / docker-compose.yml dosyasını tarıyor, içinde tanıdığı servisleri buluyor (PostgreSQL, Redis, MongoDB, Kafka, ayrıca Zipkin, OTel Collector, Grafana gibi observability servisleri de) ve Spring Boot'u o servislere bağlamak için gerekli propertyleri kendiliğinden set ediyor.

Yani devdeki compose.yaml'imde şöyle bir servis varsa:

services:
  otel-collector:
    image: otel/opentelemetry-collector:latest
    ports:
      - "4318:4318"
      - "4317:4317"

Uygulamayı ./mvnw spring-boot:run ile ayağa kaldırdığımda Spring Boot şunu fark ediyor. "Hey composeda bir OTel collector var, 4318 portuna HTTP OTLP konuşuyor". Hiçbir application.properties satırı yazmadığım halde management.otlp.metrics.export.url, management.opentelemetry.tracing.export.otlp.endpoint gibi propertyleri otomatik olarak http://localhost:4318/... diye set ediyor.

Production ortamında genellikle docker-compose çalıştırmıyorum. Kubernetes, ECS, Cloud Run, bir PaaS — ortam ne olursa olsun bir compose.yaml dosyası yok. Dolayısıyla yukarıdaki auto-detection mekanizması tetiklenmiyor, Spring Boot hangi collectore bağlanacağını bilmiyor.

Bu yüzden prodda endpointleri elle vermem gerek ya da genelde bunu .properties dosyasına hardcode etmek yerine environment variable üzerinden verebilirsiniz

# Spring Boot 4 — OTLP backend (örneğin bir collector)
management.otlp.metrics.export.url=http://otel-collector:4318/v1/metrics
management.opentelemetry.tracing.export.otlp.endpoint=http://otel-collector:4318/v1/traces
management.opentelemetry.logging.export.otlp.endpoint=http://otel-collector:4318/v1/logs
management.tracing.sampling.probability=1.0
# k8s deployment manifest
env:
  - name: MANAGEMENT_OTLP_METRICS_EXPORT_URL
    value: http://otel-collector.observability:4318/v1/metrics
  - name: MANAGEMENT_OPENTELEMETRY_TRACING_EXPORT_OTLP_ENDPOINT
    value: http://otel-collector.observability:4318/v1/traces

Trace Context Propagation: W3C Default

Spring Boot 4 default olarak W3C Trace Context’i kullanıyor. Yani HTTP istekleri arasında şu headerlar propagate oluyor:

  • traceparent → W3C standardı
  • tracestate → opsiyonel state taşıyıcı
  • baggage → cross-cutting concern (user-id, tenant-id) taşımak için

B3 propagation (Zipkin’in eski formatı) veya custom propagator istiyorsam io.opentelemetry.context.propagation.TextMapPropagator beani tanımlayıp devreye sokuyorum. 3.x'te Brave kullananlar için default B3'tü. Spring Boot 4'te W3C'ye geçiş bir davranış değişikliği yarattı. Upstream servisleriniz B3 bekleyen yaşlı bir sistem ise propagatorı elle ayarlamak zorundasınız.

Async Context: ContextPropagatingTaskDecorator

Bu da ince bir detay. Bir request içinde yeni bir thread spawn ettiğinizde (ör. @Async, CompletableFuture.supplyAsync(), kendi ExecutorService'im) trace context default olarak taşınmıyor. Spring Boot 4'te yeni bir ContextPropagatingTaskDecorator beani geldi ve bunu ThreadPoolTaskExecutor'a bağladığımda tüm async spawnlarda context otomatik taşınıyor. Bu ne demek:

@Bean
TaskDecorator taskDecorator() {
    return new ContextPropagatingTaskDecorator();
}

@Bean
ThreadPoolTaskExecutor asyncExecutor(TaskDecorator taskDecorator) {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setTaskDecorator(taskDecorator);
    executor.initialize();
    return executor;
}

3.x’te bunu ya manuel yapıyordum (Brave’in ScopePassingSpanSubscriber adapterı) ya da context kaybediyordum. 4'te Spring tarafında first-class destek var.

Micrometer 1.16: Metric Tag Rename’leri

Micrometer 1.10.x’ten 1.16.x’e geçerken bazı HTTP client/server metric taglerinde değişiklikler var. En sinsi olanları biraz açıklamak istiyorum.

  • http.server.requests metriğinde uri tagi daha katı template normalization kullanıyor → /users/123 yerine /users/{id} görüyorum. Bu yaygın bir davranış değil, bazı custom matcherlarınız varsa çalışmayabiliyor
  • http.client.requests metriğinde uri tagi de benzer normalizationa tabi
  • OpenTelemetry Semantic Convention’a uyum için bazı metrik isimleri yeniden adlandırıldı (ör. http.server.requests → bazı durumlarda http.server.request.duration)
  • Cumulative histogram yapısı OTel tarafında deltaya çevrilebiliyor. Grafana querylerinizde rate() vs increase() fonksiyonlarını etkileyebiliyor.

Grafana dashboardlarınızı upgrade öncesi yedeklemenizi ve her panelin querysini 4.x sürümünde manuel kontrol etmenizi şiddetle öneriyorum.

Micrometer Tracing 1.6: Bridge Tercihi

Micrometer Tracing 1.x’ten 1.6'ya geçerken iki bridge var:

  • micrometer-tracing-bridge-otel → OpenTelemetry bridge, yeni default ve önerilen
  • micrometer-tracing-bridge-brave → Brave (Zipkin ekosistemi) bridge, hâlâ destekleniyor ama tavsiye edilmiyor

Spring Boot 4 dokümantasyonu açıkça OTel bridge’i öneriyor. Yeni proje kurarken ikinciyi seçmemeyi tavsiye ederim. Mevcut Brave-bazlı setupınız varsa OTel’e migrate etmek için OneUptime’ın yazdığı guide’a bakabilirsiniz. (How to Migrate from Micrometer Tracing to OpenTelemetry — OneUptime)

Log Correlation: Appender Elle Config

Spring Boot 4 default olarak Logback/Log4j2'ye trace/span ID’yi basan appenderı otomatik kurmuyor. MDC’ye trace ID’sini yazmak için kendim logback-spring.xml dosyamda pattern tanımlamak zorundayım.

<configuration>
    <springProperty scope="context" name="appName" source="spring.application.name"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level [${appName},%X{traceId:-},%X{spanId:-}] %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

Micrometer Observation bridge’i MDC’ye traceId ve spanId anahtarlarını otomatik koyuyor, ben sadece log patternında %X{traceId:-} olarak çağırıyorum. :- default value syntax'ı, trace context olmadığında (ör. application startup loglarında) boş string basıyor.

Web Katmanı: Undertow, Jersey, Elasticsearch

Servlet 6.1 baseline desteklemediği için Undertow desteği tamamen kaldırıldı. Bilmeyenler için Undertow, Spring Boot uygulamalarında varsayılan Tomcat’in yerine kullanılabilen Red Hat tarafından geliştirilen yüksek performanslı, hafif ve esnek gömülü bir web sunucusudur. Kullanıyorsanız Tomcat, Jetty veya Reactor Netty’e geçmek zorundasınız. Bu sadece pom.xml'deki dependency değişimi değil. Undertow-spesifik configleriniz (HTTP/2 ayarları, access log formatı, connection pool tuning) de değiştirdiğiniz web serverın eşdeğer configlerine taşınmalı.

WAR deploy edenler için spring-boot-starter-tomcat adının spring-boot-starter-tomcat-runtime'a dönüştüğünü yukarıda yazdım. Statik kaynaklarda /fonts/** artık default location listesinde. Eğer /fonts/ dizini farklı bir amaçla kullanılıyorsa PathRequest.toStaticResources().excluding(StaticResourceLocation.FONTS) ile hariç tutulmalı.

Jersey 4.0 desteği geldi ama Jackson 3 ile uyumlu değil . Yani Jersey + JSON kullanıyorsanız spring-boot-jackson2 bridge'i zorunlu. Bu da aslında sonradan tekrar refactor yapmanız gereken bir durum oluşturuyor. Elasticsearch low-level RestClient deprecate edildi yerine Rest5Client auto-configuration geldi. RestClientBuilderCustomizerRest5ClientBuilderCustomizer. Managed dependency listesinden elasticsearch-rest-client ve elasticsearch-rest-client-sniffer çıkarıldı ve her şey co.elastic.clients:elasticsearch-java altında birleşti.

Tomcat 10.1 → 11 Geçişi: Ne Değişti

Spring Boot 3.5.x arkada Tomcat 10.1.x kullanıyordu, 4.0.x arkada Tomcat 11.0.x kullanıyor. Major version bumpıyla birlikte Servlet 6.0 → 6.1, JSP 3.1 → 4.0, EL 5.0 → 6.0, WebSocket 2.1 → 2.2, Authentication 3.0 → 3.1 spec’leri değişmiş. Compile time’da hiçbir şey değişmemiş gibi görünüyor çünkü Spring Boot tüm bu API’leri soyutluyor ama runtime’da dikkat etmemiz gereken birkaç sessiz davranış değişikliği vardı.

maxParameterCount Default'u 10.000'den 1.000'e Düştü

Bu en tuzaklı değişiklik. Tomcat 10.1'de bir request içinde işleyebildiğim query parameter + form parameter toplamı default olarak maksimum 10.000'di. Tomcat 11'de bu rakam 1.000'e indirildi. DDoS hash collision saldırılarına karşı sıkılaştırma amaçlı.

Şöyle bir yan etki yaşanabilir. Çok sayıda checkbox içeren formu olan legacy bir uygulamanız varsa (ör. “100 tane ürünü toplu şekilde işaretle ve güncelle”), 3.5.x’te sorunsuz çalışırken 4.0.x’e geçince random bir noktada IllegalStateException: Maximum number of request parameters (1000) exceeded diye patlamaya başlayacaktır. Spring Boot tarafında property ile eski davranışı geri getirmemiz mümkün tabi ki:

# Tomcat 10 davranışı (10.000 limiti)
server.tomcat.max-parameter-count=10000

# Ya da tamamen limitsiz (tavsiye etmem)
server.tomcat.max-parameter-count=-1

Aynı şekilde multipart formlarda server.tomcat.max-part-count diye ek bir default geldi. Dosya yükleme endpointlerin varsa bunu da kontrol etmeni öneririm.

SecurityManager Tamamen Kaldırıldı

Tomcat 10.1'de SecurityManager deprecate edilmişti ama hala çalışıyordu. Tomcat 11'de tamamen kaldırıldı. Modern Spring Boot uygulamalarının çoğunda zaten SecurityManager kullanılmıyor. Bu daha çok legacy enterprise deploylarında -Djava.security.manager ile ayağa kaldırılan uygulamaları etkiliyor. Eğer startup scriptinde böyle bir parametre görüyorsan kaldırman gerek:

# Artık hiçbir şey yapmıyor, JVM uyarı basıyor
java -Djava.security.manager -Djava.security.policy=my.policy -jar app.jar

# Doğru başlatma
java -jar app.jar

Bu aslında Java 17'nin kendi değişikliği (Java tarafında deprecated), Tomcat 11 sadece artifact seviyesinde de desteği kaldırdı.

Cookie Parsing: RFC 6265'e Tam Uyum

Tomcat 10.1, quoted cookie valuelardaki tırnakları soyuyordu. Yani Cookie: foo="bar" şeklinde gelen cookieyi foo=bar olarak okuyordu. Tomcat 11 RFC 6265'e katı uyduğu için tırnakları cookie value'nun parçası olarak kabul ediyor. Bu ne demek diye sorarsanız ufak bir örnekle açıklama yapim:

  • İstek header’ı: Cookie: session="abc123"
  • Tomcat 10.1'in okuduğu: session = abc123
  • Tomcat 11'in okuduğu: session = "abc123" (tırnaklar dahil)

Çoğu modern browser ve HTTP client tırnak içinde cookie göndermediği için pratikte nadir karşılaşılan bir senaryo. Ama eğer upstream bir servis (başka bir microservice, legacy auth gateway vs.) quoted cookie üretiyorsa, session cookie’nizin değeri sessizce "<gerçek value>" oluyor ve lookup başarısız oluyor. Authentication flowlarınızı integration testle gözden geçirmenizi öneririm.

HTTP/2 Server Push Kaldırıldı

Tomcat 11 HTTP/2 Server Push özelliğini tamamen kaldırdı. Servlet 6.1 spec’i server push’u artık optional olarak tanımlıyor bu yüzden de Tomcat ekibi de kaldırmayı tercih etti. Kodunda PushBuilder kullandığın yerler varsa onları değiştirmen gerekecek:

// Tomcat 11'de null döner
PushBuilder push = request.newPushBuilder();
if (push != null) {
    push.path("/css/style.css").push();
}

newPushBuilder() artık her zaman null döndürüyor. Kod patlamıyor ama server push hiç tetiklenmiyor. Kod derleniyor, exception atmıyor, uygulama ayağa kalkıyor. Ama daha önce push ile kazandığın performans avantajını kaybetmişsin ve bunun farkına varman zor. Çünkü görünürde hiçbir şey bozuk değil, sadece önceden uçarken şimdi yürüyorsun. Eğer resourceları preload etmek istiyorsan artık iki alternatif var: response’a Link: <...>; rel=preload HTTP headerı eklemek, ya da HTML <head>'ine <link rel="preload" href="..."> tagi yazmak. Bu ikisi de browser tarafında "bu resource'u da ihtiyacın olacak, hemen indirmeye başla" anlamı taşıyor. Server push'un işlevini browser-driven prefetch olarak yeniden kuruyorsun.

Byte-to-Character Conversion Artık Sessiz Kalmıyor

Bu incelikli bir değişiklik. Tomcat 10.1'de bir byte dizisini character’a çevirirken encoding uyumsuzluğu olduğunda Tomcat bunu sessizce yutar replacement character (? veya \uFFFD) basardı. Tomcat 11'de aynı durumda exception atıyor. Etkisi olabilecek yerler:

  • Garip encodingli request body geldiğinde (özellikle Content-Type: application/x-www-form-urlencoded; charset=... eksik olduğunda)
  • URL’de encode edilmemiş non-ASCII karakter varken
  • Custom Valve yazdığın yerlerde byte/char dönüşümü manuel yapıyorsan

Pratikte bu bana henüz bir sorun çıkarmadı ama error loglarında yeni MalformedInputException tipinde hatalar görebilirsin. Bunlar Spring Boot 4 upgrade'inin sonucu, Spring tarafında bir bug değil.

Spring Boot 4 + Tomcat 11 Kombinasyonunda CVE Konusu

Spring Boot 4.0.5 ilk çıktığında Tomcat 11.0.20 bundle etmişti. 11.0.20'de üç adet CVE (CVE-2026–34486, CVE-2026–34487, CVE-2026–34483) bulundu ve 11.0.21'de kapatıldı. Spring Boot 4.0.6 çıkana kadar ara dönemde override etmekte fayda var:

<properties>
  <tomcat.version>11.0.21</tomcat.version>
</properties>

Bu property override edildiğinde tomcat-embed-core, tomcat-embed-el, tomcat-embed-websocket, tomcat-jdbc gibi tüm Tomcat bileşenleri otomatik olarak 11.0.21'e çekiliyor çünkü BOM'da hepsi bu property'yi kullanıyor.

Virtual Thread Desteği (Güzel Haber)

Tomcat 11'in getirdiği pozitif değişikliklerden biri de Java 21'in virtual threadlerine first-class support geldi. Spring Boot 4'te şu property ile aktif edebilirsin:

spring.threads.virtual.enabled=true

Bu açıldığında Tomcat request thread pool’u yerine her isteği kendi virtual threadinde servis ediyor. I/O-heavy workloadlar için anlamlı bir throughput iyileşmesi yaşadım. CPU-bound için fark yok hatta bazı edge case’lerde kötüleşebiliyor (pinning sorunları nedeniyle). Synchronized blok içinden JDBC.executeQuery() gibi blocking çağrılar yapan legacy kodun varsa virtual threadler orada pin oluyor ve throughput kazanamıyorsun.

Bana sorarsanız bu ayarı Java 25'e geçince açmanız en faydalısı olacaktır. Java 21'de ciddi pinning sorunu var. Java 24'te büyük oranda çözüldü. Java 25 LTS’te bu çözüm stabilize edildi. Yani virtual threadleri gerçekten güvenle açmak istiyorsan Java 25 hedeflemen mantıklı. Ama Java 25'te de pin durumları olduğunu söylemeden edemeyeceğim. JEP 491'in çözdüğü şey synchronized bloğu içinde blocking çağrı yapınca carrier threadin pin kalması senaryosuydu ama pinning tamamen tarihe karışmış değil.

Build / Maven

İki önemli değişiklik var. Optional dependencyler artık uber jardan default olarak hariç tutuluyor. Eğer eski davranışı istiyorsanız <includeOptional>true</includeOptional> eklemelisiniz. Classic uber-jar loader tamamen kaldırıldı. Bu da demek oluyor ki <loaderImplementation>CLASSIC</loaderImplementation> configiniz varsa silin. Gradle tarafında CycloneDX plugin minimum 3.0.0.

Property Rename’leri: Toplu Liste

Bir yerde toparlayayım:

  • spring.session.redis.*spring.session.data.redis.*
  • spring.session.mongodb.*spring.session.data.mongodb.*
  • spring.dao.exceptiontranslation.enabledspring.persistence.exceptiontranslation.enabled
  • spring.kafka.retry.topic.backoff.randomspring.kafka.retry.topic.backoff.jitter
  • MongoDB connection propertyleri: spring.data.mongodb.<connection-level>spring.mongodb.* (data-layer olanlar yerinde)
  • spring.jackson.read.* / spring.jackson.write.* / spring.jackson.parser.*spring.jackson.json.* altında birleşmiş
  • Actuator Mongo isimleri: mongomongodb
  • DevTools Live Reload artık default disabled → açmak için spring.devtools.livereload.enabled=true

★ Bunları tek tek aramak yerine spring-boot-properties-migrator modülünü runtime dependency olarak ekleyin; uygulama boot olurken tüm eski property isimlerini yenilerine eşleyip uyarı basar. Bir kere build yeşile geldikten sonra migratorı çıkarabilirsiniz.

Kaldırılan / Deprecate Edilen API’ler

Ölüler listem:

  • @MockBean@MockitoBean
  • @SpyBean@MockitoSpyBean
  • MockitoTestExecutionListener → Mockito'nun kendi MockitoExtension
  • HttpMessageConverters bean tipi → ClientHttpMessageConvertersCustomizer veya ServerHttpMessageConvertersCustomizer
  • Embedded executable jarın Unix launch scriptleri → kaldırıldı (sorun çıkaran java -jar x.jar wrapperları)
  • Classic uber-jar loader → kaldırıldı
  • Spock entegrasyonu → kaldırıldı (Spock Groovy 5'i henüz desteklemiyor)
  • Spring Session Hazelcast → Hazelcast ekibine devredildi
  • Spring Session MongoDB → MongoDB ekibine devredildi
  • Pulsar Reactive → kaldırıldı
  • Spring Retry dependency management → Spring Framework 7'ye taşındı

PropertyMapper’ın Sessiz Davranış Değişikliği

Kod seviyesinde compile düzeyi kırılma vermeyen ama davranışı sessizce değiştiren bir şey var. Spring Boot 4'te PropertyMapper null değerler için adapter/predicate methodlarını artık default olarak çağırmıyor.

Örneğin:

map.from(source::getX).to(destination::setX);

3.x’te bu satır source.getX() null döndürdüğünde destination.setX(null) çağırıyordu. 4.x'te hiç çağırmıyor. null geldiğinde setter skip ediliyor. Eski davranışı geri getirmek için .always() ara methodu eklenmeli:

map.from(source::getX).always().to(destination::setX);

Config binding kullanan library yazarları için büyük bir davranış değişikliği! Uygulama kodunuzda PropertyMapper kullanıyorsanız her kullanımı gözden geçirmek gerekiyor.

Migration Stratejisi: Sıralı Bir Plan

Bir major upgrade’i tek PR’da yapmaya çalışmak en yaygın hata. Şu sırayla gitmeyi öneriyorum.

Önce 3.5.13'e geçin — 3.5 serisinin son patchi. Burada tüm deprecationları temizleyin. mvn compile -X ile tüm deprecation uyarılarını listeleyip sıfırlayın. @MockBean@MockitoBean geçişini mutlaka bu fazda yapın. 3.4+ zaten yeni anotasyonları destekliyor. Bu faz mümkün olan her teknik borcu ödemek için, yani 4.0 geçişinde yüzünüzün sadece gerçek 4.0 değişiklikleriyle karşılaşmasını sağlar.

Sonra spring-boot-properties-migrator'ı test profiline ekleyin. Bu, property rename'lerini boot zamanında raporlayan bir modül. Her eski property için "yenisi şu" diye log basar. Bir kere 4.0'a geçtikten sonra bu raporları takip ederek propertyleri güncellersiniz.

Parentı 4.0.5'e çekin ve ilk buildi çalıştırın. Bekleyeceğiniz hatalar: Jackson 2 sınıf importları, kalmış @MockBean / @SpyBean kullanımları, FQCN ile yazılmış autoconfigure sınıf referansları, custom HttpMessageConverters bean tanımları, Undertow bağımlılıkları. Bunları dilim dilim çözün ve her dilim ayrı commit olsun.

Test katmanını ayrı bir PR’da ele alın. @SpringBootTest artık MockMvc ve TestRestTemplate vermediği için integration testlerinizin çoğu sessizce çalışmaz hale gelecek. @AutoConfigureMockMvc ve @AutoConfigureTestRestTemplate ekleyin ve testleri tek tek yeşile çekin.

mvn dependency:tree'i okumayı ihmal etmeyin. Geçici bir köprü olarak spring-boot-jackson2 veya spring-boot-starter-classic eklediyseniz, bunlara ileride silmek üzere TODO düşün ki kalıcı olmasınlar.

Upgrade sonrası production doğrulamasında sıkı olun. JSON response formatlarını karşılaştırın (Jackson 3 defaultları 2'den farklı olabilir; null handling, date format, enum serialization özellikle). Redis’e yazılmış cache entrylerini flushlayın. Actuator endpointlerini dashboardda karşılaştırın. Micrometer metriklerinin taglerini görsel olarak yeni ile eski arasında kıyaslayın. Database migration scriptleri yeni Hibernate 7.1 altında davranışsal fark verebilir, özellikle tablo/column isimlendirmesinde.

Sessiz Tuzaklar

Sonuna birkaç tane daha sinsi detayı ekleyeyim. Redis cache uyumsuzluğunu yukarıda yazdım. Binary format değişikliği prodda flush yapılmadıysa exception yağmuruna dönüşebilir. Jackson 3'ün otomatik modül bulma davranışı, classpathte tanımadığınız bir Jackson modülünün varlığında serialization davranışınızı sessizce değiştirebilir. PropertyMapper null skip davranışı bug raporları gelmeden kendiliğinden farkına varılmaz. Micrometer metric tag renameleri, Grafana alert kurallarınızı geriye uyumsuzluk olmadan etkilemez ama grouping bozulur. Starter isim değişiklikleri kademeli deprecate, spring-boot-starter-web hâlâ çalışıyor ama 5.0'da ölecek, yeni kod yazıyorsanız şimdiden webmvc'i tercih edin.

Bir de upstream patch senkronizasyonu konusu var. Spring Boot 4.0.5 örneğin Tomcat 11.0.20 bundle ediyor. Yayınlandığı tarihten birkaç gün sonra Tomcat 11.0.21 çıktı ve 11.0.20'deki üç CVE’yi kapattı. BOM henüz 11.0.21'i bundle etmediği için bu CVE’ler raporlarınıza düşer. Spring Boot’un property override mekanizmasıyla çözülür: <tomcat.version>11.0.21</tomcat.version> satırını kendi pom'unuzun <properties>'ine ekleyin, parent pom'un ilgili tüm Tomcat jar versiyonlarını bu property’ye bağladığı için tek satırla upgrade ediyorsunuz. Aynı pattern Jackson BOM, Netty, SnakeYAML vb. için de geçerli. Parent pom'unu grep "\.version>" ile tarayıp doğru property adını bulun.

Sonuç

3.5'ten 4.0'a geçiş pratikte üç major upgrade’in birleşimi: framework (Spring 7), JSON (Jackson 3), ORM (Hibernate 7). Bu yüzden bir saatlik iş değil, planlı bir sprint işi.

Kritik önerilerim:

Tek PR’a sıkıştırmayın. Baseline → modüler importlar → Jackson 3 → Test refactor şeklinde fazlara ayırın. properties-migrator'ı kullanın, en ucuz erken uyarı sistemi. Jackson 3 geçişini en son tutun, modül re-organizasyonu ve test refactorü kendi başına yeterince gürültülü. Releaseden önce binary format yazan her cache'i flush edin (Redis, Hazelcast, local caches). Spring Framework 7 ve Spring Security 7 migration guidelarını paralel okuyun. Spring Boot guide'ı onların üzerine inşa edilmiş ve tek başına yeterli değil.

Son olarak, geçici köprüleriniz (spring-boot-jackson2, spring-boot-starter-classic) kalıcı hale gelmeden silin. Spring Boot 4.0.6, 4.0.7 çıktıkça override ettiğiniz propertyleri kontrol edin ve yeni patchler gelmişse sizin manuel override'ınız onları gölgeler ve sessizce eski versiyonda takılı kalırsınız.

Kaynaklar


메타데이터
post_id
5cb065bf4437
slug
spring-boot-3-5-x-4-0-x-migration-rehberi-5cb065bf4437
url
https://medium.com/moneypay-tech/spring-boot-3-5-x-4-0-x-migration-rehberi-5cb065bf4437
canonical_url
https://medium.com/moneypay-tech/spring-boot-3-5-x-4-0-x-migration-rehberi-5cb065bf4437
author_url
https://medium.com/@tugce.dev.journal
status
ok
fetched_at
2026-07-14 23:28:58