Friday, March 20, 2026

🚀 Spring Boot 3.5 → 4.0.3 Migration Summary

Import Changes, API Adjustments & Compatibility Results

Migration summary from Spring Boot 3.5 → 4.0, including breaking changes, dependency updates, and fixes across Web, JPA, Jackson 3, GCP, Pub/Sub, Thymeleaf, and Testing.

  

🔁 Key Import & API Changes


1️⃣ RestTemplate Builder (Client API)

❌ Removed

org.springframework.boot.web.client.RestTemplateBuilder

✅ Replaced With

org.springframework.boot.restclient.RestTemplateBuilder

✅ Dependency Change

Spring Boot 3

spring-boot-starter-web

Spring Boot 4

spring-boot-starter-webmvc
spring-boot-starter-restclient

📌 Reason Spring Boot 4 separates responsibilities:

  • Server → spring-boot-starter-webmvc
  • Client → spring-boot-starter-restclient


2️⃣ EntityManager Factory Builder

❌ Removed

org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder

✅ Replaced With

org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean

📌 Reason Spring Boot 4 removes Boot-specific abstractions → use standard Spring Framework APIs.


3️⃣ Jackson Migration (🔥 Major Change)

❌ Removed (Explicit Jackson 2)

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

✅ Added (Boot 4 Default – Jackson 3)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-json</artifactId>
</dependency>

<dependency>
    <groupId>tools.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

✅ Boot 4 Now Uses

import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.JsonNode;
import tools.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import tools.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import tools.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import tools.jackson.dataformat.xml.XmlMapper;
import org.springframework.boot.json.JsonParseException;

⚠️ Compatibility Note

Libraries such as springdoc still use Jackson 2 internally → ✔️ Fully compatible (can run in parallel)


🔧 ObjectMapper Migration

Spring Boot 3

this.objectMapper
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);

Spring Boot 4 (Rebuild from existing)

this.objectMapper =
    ((JsonMapper) objectMapper)
        .rebuild()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .configure(JsonReadFeature.ALLOW_SINGLE_QUOTES, true)
        .build();

Alternative (New Instance)

this.objectMapper =
    JsonMapper.builder()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .configure(JsonReadFeature.ALLOW_SINGLE_QUOTES, true)
        .build();

📌 Jackson 3 Package Changes

 Article content

 Alternative (Direct ObjectMapper with builder-style API)

When you need a plain ObjectMapper without JsonMapper.builder(), use the mutate-style API:

import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.JsonNode;

var mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
📌 In Jackson 3, ObjectMapper is still valid for simple use cases. The disable(...) / enable(...) methods work directly on the instance — no need to use configure(feature, false) as in Jackson 2.

👉 Important: Annotations (@JsonProperty, etc.) remain unchanged → ✔️ No changes required in model/POJO classes


🔍 Migration Tip

grep -r "com.fasterxml.jackson.databind.ObjectMapper" src/main/java

4️⃣ HTTP Exception Constructor Change

❌ Old

new HttpMessageNotReadableException(message, cause);

✅ New

new HttpMessageNotReadableException(message, (HttpInputMessage) null);

📌 Reason Spring Framework 7 removed the Throwable constructor.

✅ ResponseEntity with Status Codes

Spring Boot 4 also introduces cleaner static factory methods for common HTTP status codes. The new ResponseEntity<>(null, HttpStatus.XXX) pattern should be replaced with the dedicated builder methods.

Spring Boot 3

ResponseEntity<List<ApplyBillCreditResponseVO>> responseEntity =
    new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);

new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);

Spring Boot 4

ResponseEntity<List<ApplyBillCreditResponseVO>> responseEntity =
    ResponseEntity.badRequest().build();

ResponseEntity.internalServerError().build();

📌 Common ResponseEntity Migration

 

 5️⃣ ResponseEntity Test Constructor Fix

❌ Broken in Boot 4

new ResponseEntity<>(null, HttpStatus.OK);

✅ Fixed

ResponseEntity.ok(null);

or

new ResponseEntity<String>((String) null, HttpStatus.OK);

6️⃣ Test Stack Migration

❌ Removed

spring-boot-starter-test

✅ Replaced With

spring-boot-starter-webmvc-test

✅ New Test Platform Versions

  • JUnit 6
  • Mockito 5
  • AssertJ 3.27
  • Awaitility 4.3


7️⃣ Lombok (⚠️ Build-Time Configuration Required)

✅ Dependency

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

✅ Maven Compiler Plugin Setup

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

✅ Spring Boot Plugin Exclusion

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <excludes>
            <exclude>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </exclude>
        </excludes>
    </configuration>
</plugin>

📌 Reason Lombok must run only at compile-time → not packaged into runtime artifact.


8️⃣ Google Cloud (Secret Manager + Storage)

✅ Dependencies

<!-- Google Cloud Dependencies -->
<dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>spring-cloud-gcp-starter-secretmanager</artifactId>
</dependency>

⚠️ Jackson Conflict Fix

Exclude Jackson 2 XML module:

<dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>google-cloud-storage</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
        </exclusion>
    </exclusions>
</dependency>

✅ Dependency Management (BOM)

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>spring-cloud-gcp-dependencies</artifactId>
            <version>8.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

9️⃣ Pub/Sub / Spring Integration

❌ Spring Boot 3

<dependency>
    <groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-core</artifactId>
</dependency>

✅ Spring Boot 4

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-integration</artifactId>
</dependency>

📌 Notes

  • spring-boot-starter-integration auto-configures integration
  • @EnableIntegration becomes available
  • Required for Pub/Sub + Spring Integration flows


Pub/Sub Dependency

<dependency>
    <groupId>com.google.cloud</groupId>
    <artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
</dependency>

🔟 Testcontainers Migration

Spring Boot 3

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>oracle-free</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>gcloud</artifactId>
</dependency>

Spring Boot 4

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-junit-jupiter</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-oracle-free</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-postgresql</artifactId>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers-gcloud</artifactId>
</dependency>

1️⃣1️⃣ Thymeleaf (⚠️ Known Issue in Boot 4)

Issue

org.thymeleaf:thymeleaf-spring6 → compatibility issue with Spring Framework 7

✅ Workaround (Manual Bean)

@Configuration
public class ThymeleafManualConfig {

    @Bean(name = "manualTemplateEngine")
    @Primary
    public TemplateEngine manualTemplateEngine() {
        ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
        resolver.setPrefix("templates/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode(TemplateMode.HTML);
        resolver.setCharacterEncoding("UTF-8");
        resolver.setCacheable(false);
        resolver.setCheckExistence(true);

        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(resolver);
        return engine;
    }
}

⚠️ Important

Avoid bean conflict:

templateEngine already exists → disable or rename

1️⃣2️⃣ Test (Jackson Mock Change)

Spring Boot 3

when(jsonMapperBuilder.enable(any(DeserializationFeature.class)))
    .thenReturn(jsonMapperBuilder);

when(jsonMapperBuilder.enable(any(JsonParser.Feature.class)))
    .thenReturn(jsonMapperBuilder);

Spring Boot 4

import tools.jackson.databind.json.JsonMapper;

@Mock
private JsonMapper objectMapper;

JsonMapper realMapper = JsonMapper.builder().build();

1️⃣3️⃣ Springdoc OpenAPI (⚠️ Jackson 2 Compatibility)

✅ Dependency (Spring Boot 4 Supported Version)

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>3.0.2</version>
</dependency>

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
    <version>3.0.2</version>
</dependency>

⚠️ Important Behavior

  • Spring Boot 4 → uses Jackson 3 (tools.jackson.*)
  • Springdoc (3.0.2) → still uses Jackson 2 (com.fasterxml.*) internally

1️⃣4️⃣ JavaTimeModule in Jackson 3

⚠️ Important Clarification

In Jackson 3, JavaTimeModule no longer needs to be registered explicitly. The Jackson 3 migration guide states that the former Java 8 modules are now built into jackson-databind, including jackson-datatype-jsr310 for java.time support.

✅ What this means

Old Jackson 2 style

mapper.registerModule(new JavaTimeModule());

Spring Boot 4 / Jackson 3

this.objectMapper =
    JsonMapper.builder()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .build();

📌 Result

  • JavaTimeModule is not required anymore in Jackson 3.
  • java.time support remains available out of the box.
  • This specifically applies to: LocalDate LocalDateTime Instant other Java 8 date/time types.

💡 Practical Note

If your DTOs already serialize time values as String (for example Instant.now().toString()), then removing new JavaTimeModule() does not change that payload behavior.

📌 Jackson 3 embeds the former Java 8 modules directly into jackson-databind, including JSR-310 support for java.time types, so JavaTimeModule no longer needs to be registered explicitly.


1️⃣5️⃣ HttpHeaders Change in Spring Framework 7

⚠️ Breaking Change

In Spring Framework 7, HttpHeaders no longer implements the MultiValueMap contract. The official Javadoc and Spring Framework 7 release notes both call this out explicitly.

❌ Old style

HashMap<String, List<String>> customHeaders = new HashMap<>(request.getHeaders());
builder.append(" Custom Headers: ").append(customHeaders);

✅ Updated style

HashMap<String, List<String>> customHeaders = new HashMap<>();
request.getHeaders().forEach(customHeaders::put);

builder.append(" Custom Headers: ").append(customHeaders);

📌 Why this change is needed

Since Spring Framework 7, HttpHeaders is no longer map-like in the same way as before, and several Map / MultiValueMap style usages are no longer valid or are discouraged. Spring’s Javadoc also notes that asMultiValueMap() is now only for backward compatibility and should generally be avoided.

✅ Other common fixes

Check existence

headers.containsHeader("My-Header")

instead of:

headers.containsKey("My-Header")

Spring Framework 7 introduced header-focused alternatives because HttpHeaders no longer extends MultiValueMap.

📌 Spring Framework 7 changed HttpHeaders so it no longer implements MultiValueMap directly. Map-style usage such as new HashMap<>(request.getHeaders()) may fail and should be replaced with header-focused access patterns like request.getHeaders().forEach(...) or containsHeader(...).

 

1️⃣6️⃣ Logback — Transitive via spring-boot-starter-webmvc

✅ Dependency Tree

spring-boot-starter-webmvc already brings Logback transitively:

spring-boot-starter-webmvc
 └─ spring-boot-starter
     └─ spring-boot-starter-logging
         ├─ logback-classic
         └─ logback-core 

Spring Boot already provides:

  • logback-classic
  • logback-core

✅ Safe to Remove

The following explicit dependencies are redundant and can be safely removed from pom.xml:

<!-- ❌ Remove — already provided transitively by spring-boot-starter-webmvc -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>
📌 Declaring these explicitly adds no value and may cause version conflicts if the explicit version differs from the one managed by the Spring Boot BOM. Let Spring Boot manage the Logback version automatically.

1️⃣7️⃣ Spring Retry → Spring Resilience (Spring Framework 7 / Spring Boot 4)

⚠️ Breaking Change

✅ Dependencies — Safe to Remove

The following dependencies are no longer needed (Spring Boot 4 includes resilience support built-in):

<!-- ❌ Remove — replaced by Spring Framework 7 built-in resilience -->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

✅ Configuration Class Migration

Spring Boot 3 (spring-retry)

import org.springframework.retry.annotation.EnableRetry;

@Configuration
@EnableRetry
public class RetryConfig {
}

Spring Boot 4 (Spring Resilience)

import org.springframework.context.annotation.Configuration;
import org.springframework.resilience.annotation.EnableResilientMethods;

@Configuration
@EnableResilientMethods
public class RetryConfig {
}

✅ @Retryable Annotation Migration

Spring Boot 3 (spring-retry)

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;

@Retryable(
    retryFor = {RuntimeException.class, Exception.class, HttpClientErrorException.Unauthorized.class},
    exclude = {HttpClientErrorException.class},
    maxAttempts = 4,
    backoff = @Backoff(delay = 1000, multiplier = 2)
)

Spring Boot 4 (Spring Resilience)

import org.springframework.resilience.annotation.Retryable;

@Retryable(
    includes = {RuntimeException.class, Exception.class, HttpClientErrorException.Unauthorized.class},
    excludes = {HttpClientErrorException.class},
    maxRetries = 4,
    delay = 1000,
    multiplier = 2
)

📌 Summary of Changes

        
 

1️⃣8️⃣ UriComponentsBuilder — fromHttpUrl Removed (Spring Framework 7)

⚠️ Breaking Change

❌ Removed Factory Methods

UriComponentsBuilder.fromHttpUrl(...)
UriComponentsBuilder.fromHttpRequest(...)
// etc.

✅ Replacement

UriComponentsBuilder.fromUriString(...)

🔁 Migration Example

Spring Boot 3

UriComponentsBuilder builder = UriComponentsBuilder
        .fromHttpUrl(uri.replace("{my_param}", myDto.getMyField()));

Spring Boot 4

UriComponentsBuilder builder = UriComponentsBuilder
        .fromUriString(uri.replace("{my_param}", myDto.getMyField()));

🧠 Why This Changed

Spring removed multiple confusing factory methods (fromHttpUrl, fromHttpRequest, etc.) and unified everything into fromUriString(...), which works for all URI types — HTTP, HTTPS, and generic URIs.

📌 Summary

 

 

1️⃣9️⃣ Trailing Slash Handling — UrlHandlerFilter (Spring Framework 7 / Spring Boot 4)

⚠️ Breaking Change

❌ Old Style (Spring Boot 3)

// Additional configuration to accept with and without forward slash /
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
    configurer.setUseTrailingSlashMatch(true);
}

✅ New Style (Spring Boot 4)

@Bean
public UrlHandlerFilter urlHandlerFilter() {
    return UrlHandlerFilter
            .trailingSlashHandler("/my_url/**")
            .wrapRequest()
            .build();
}

🧠 Why wrapRequest() Is the Right Choice

  • wrapRequest() — wraps the incoming request to strip the trailing slash, behaving closest to the legacy setUseTrailingSlashMatch(true) without issuing HTTP redirects
  • redirect() — would send a 301/308 redirect to the client, which changes the HTTP method and adds a round-trip

📌 Summary

 

✅ Final Conclusion

 

✔️ The full platform is fully functional on Spring Boot 4.0:

  • Web (Spring MVC)
  • JPA / Hibernate
  • Jackson 3
  • Pub/Sub + Spring Integration
  • GCP Secret Manager
  • Testcontainers
  • Lombok (compile-time only)
  • Observability / Actuator

👉 After applying all fixes → no runtime issues


💡 Final Insight

Spring Boot 4 is not just an upgrade — it’s a platform evolution:

  • Clear separation of responsibilities
  • Reduced framework magic
  • Stronger dependency control
  • Future-ready ecosystem (Java 21, Java 25 +, Framework 7)


🔗 References

Here are the official docs and community discussions used during the migration:

📘 Spring Boot & Framework


☁️ Google Cloud (GCP)


🔄 Jackson 3


🔌 Spring Integration


💬 Community Discussion


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 










🚀 Spring Boot 3.5 → 4.0.3 Migration Summary

Import Changes, API Adjustments & Compatibility Results Migration summary from Spring Boot 3.5 → 4.0, including breaking changes, depend...