Breaking Changes in Spring Boot 4.0.0 and How to Fix Them
Problem
Last week I upgraded our Spring Boot application from 3.2 to 4.0.0. The build passed, but the application failed to start. Configuration properties weren’t being loaded. Security filters were blocking all requests. Even the actuator health endpoint was returning 401 Unauthorized.
I hit these issues because Spring Boot 4.0.0 introduced major breaking changes. The minimum Java version is now 21. Spring Framework 7 replaced Spring Framework 6. Configuration properties changed. Security defaults shifted.
After spending two days fixing the upgrade, I learned the right way to approach this migration. The key is using the Spring Boot Properties Migrator tool before upgrading. It shows you exactly what will break and how to fix it.
In this post, I’ll show you the breaking changes I encountered and how I fixed them.
Environment Context
Here’s what I was running before the upgrade:
- Spring Boot: 3.2.5
- Java: 17
- Spring Framework: 6.1.x
- Spring Security: 6.2.x
The target after upgrade:
- Spring Boot: 4.0.0
- Java: 21
- Spring Framework: 7.0.x
- Spring Security: 6.4.x
What Happened: Common Breaking Changes
When I first ran the application after upgrading, I hit these issues immediately:
1. Java Version Requirement
The build failed with an error about Java version. Spring Boot 4.0.0 requires Java 21 as a minimum.
Before (pom.xml):
<properties> <java.version>17</java.version></properties>After (pom.xml):
<properties> <java.version>21</java.version></properties>This was the first thing I had to fix. I updated our CI/CD pipeline to use Java 21, updated my local development environment, and then the build passed.
2. Configuration Properties Not Loading
After the build passed, the application started but none of our custom configuration properties were loading. I saw warnings in the logs:
***************************APPLICATION FAILED TO START***************************
Description:
The following configuration properties are deprecated:
server.compression.enabled -> server.compression.enabledspring.datasource.schema -> spring.datasource.schemamanagement.endpoints.web.exposure.include -> management.endpoints.web.exposure.includeThe property names hadn’t changed, but some of the validation logic had. I needed to use the Properties Migrator tool to catch all of these issues before they became runtime problems.
3. Security Blocking All Requests
All HTTP requests were returning 401 Unauthorized. Even the actuator health endpoint was secured.
This happened because Spring Boot 4.0 changed the default security behavior. Web applications are now secured by default, and the default SecurityFilterChain configuration changed.
4. Dependency Version Conflicts
I got NoSuchMethodError from several third-party libraries. They were compiled against older versions of Spring Framework and weren’t compatible with Spring Framework 7.
I had to update several dependencies to their -jakarta variants and ensure all Spring dependencies were at version 7.x.
How to Solve: Use Properties Migrator
The most important tool I discovered was the Spring Boot Properties Migrator. This is a runtime dependency that analyzes your configuration and tells you exactly what will break.
Here’s how to use it:
Step 1: Add the Dependency BEFORE Upgrading
Add this to your project while still on Spring Boot 3.x:
Maven (pom.xml):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope></dependency>Gradle (build.gradle):
runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")Step 2: Run Your Application
Start your application normally. The migrator will:
- Print diagnostics about deprecated properties
- Automatically migrate properties at runtime
- Show you what needs to be changed in your configuration files
Step 3: Update Your Configuration
Fix each deprecated property the migrator identifies. Here are some common ones I encountered:
server.compression.enabled - unchanged, but validation logic changed
# Still valid in 4.0.0server: compression: enabled: truespring.datasource.schema - replaced by spring.sql.init.schema-locations
# Before (deprecated)spring: datasource: schema: classpath:schema.sql
# Afterspring: sql: init: schema-locations: classpath:schema.sqlmanagement.endpoints.web.exposure.include - unchanged but default values changed
# Before - actuator endpoints were open by default in some casesmanagement: endpoints: web: exposure: include: health,info,metrics
# After - explicitly configure what you needmanagement: endpoints: web: exposure: include: health,info,metrics,prometheusStep 4: Remove the Migrator
Once you’ve updated all your configuration properties and the application runs without warnings, remove the migrator dependency:
<!-- Remove this --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope></dependency>Specific Breaking Changes with Solutions
Jackson 2 to Jackson 3 Migration
Spring Boot 4.0 uses Jackson 3.x instead of Jackson 2.x. Most properties are deprecated:
Before:
spring: jackson: serialization: write-dates-as-timestamps: false default-property-inclusion: non_nullAfter:
spring: jackson: serialization: write-dates-as-timestamps: false default-property-inclusion: non_nullThe property names haven’t changed, but the underlying implementation has. The Properties Migrator will warn you about any Jackson-specific properties that need attention.
Security Configuration Changes
The default security behavior changed in Spring Boot 4.0. Web applications are now secured by default with basic authentication.
Before (Spring Boot 3.x):
@Configurationpublic class SecurityConfig {
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().permitAll() ); return http.build(); }}This configuration now behaves differently. permitAll() on anyRequest() still permits requests, but the default is now authenticated().
After (Spring Boot 4.0):
@Configurationpublic class SecurityConfig {
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatcher(EndpointRequest.toAnyEndpoint()) .hasRole("ACTUATOR_ADMIN") .anyRequest().authenticated() ) .httpBasic(withDefaults()); return http.build(); }}For actuator endpoints, the security rules changed. You need to explicitly configure who can access them:
http .authorizeHttpRequests((authorize) -> authorize .requestMatcher(EndpointRequest.to("health")).permitAll() .requestMatcher(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN") .anyRequest().authenticated() );Jakarta EE Namespace Updates
Several dependencies now require -jakarta variants. I hit this with Infinispan:
Before:
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core</artifactId></dependency>After:
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core-jakarta</artifactId></dependency>Check all your third-party dependencies and use the -jakarta variants where available.
Virtual Threads Configuration
Spring Boot 4.0 has improved support for virtual threads. If you want to use them:
application.yml:
spring: threads: virtual: enabled: trueThis requires Java 21+. For the best performance, use Java 24+ which has better virtual threads implementation.
Auto-Configuration Changes
Some auto-configuration classes changed their conditional registration. I had a custom DataSource auto-configuration that stopped working because the conditional property changed:
Before:
@Configuration@ConditionalOnProperty(name = "custom.datasource.enabled", havingValue = "true")public class CustomDataSourceAutoConfiguration { // ...}This still works, but the default behavior changed. Now you need to explicitly disable the default DataSource auto-configuration if you want to use a custom one:
After:
@Configuration@ConditionalOnProperty(name = "custom.datasource.enabled", havingValue = "true")@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class)public class CustomDataSourceAutoConfiguration { // ...}Before and After Code Examples
Here’s a complete example showing the key changes:
application.yml
Before (Spring Boot 3.2):
spring: datasource: url: jdbc:postgresql://localhost:5432/mydb schema: classpath:schema.sql initialization-mode: always
jackson: default-property-inclusion: non_null serialization: write-dates-as-timestamps: false
management: endpoints: web: exposure: include: health,info
server: compression: enabled: trueAfter (Spring Boot 4.0.0):
spring: datasource: url: jdbc:postgresql://localhost:5432/mydb
sql: init: schema-locations: classpath:schema.sql mode: always
jackson: default-property-inclusion: non_null serialization: write-dates-as-timestamps: false
threads: virtual: enabled: true
management: endpoints: web: exposure: include: health,info,metrics,prometheus
server: compression: enabled: trueSecurity Configuration
Before (Spring Boot 3.2):
@Configurationpublic class SecurityConfig {
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests((authorize) -> authorize .anyRequest().permitAll() ); return http.build(); }}After (Spring Boot 4.0.0):
@Configurationpublic class SecurityConfig {
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests((authorize) -> authorize .requestMatcher(EndpointRequest.to("health")).permitAll() .requestMatcher(EndpointRequest.toAnyEndpoint()).hasRole("ACTUATOR_ADMIN") .anyRequest().authenticated() ) .httpBasic(withDefaults()); return http.build(); }}pom.xml
Before (Spring Boot 3.2):
<properties> <java.version>17</java.version> <spring-boot.version>3.2.5</spring-boot.version></properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core</artifactId> </dependency></dependencies>After (Spring Boot 4.0.0):
<properties> <java.version>21</java.version> <spring-boot.version>4.0.0</spring-boot.version></properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.infinispan</groupId> <artifactId>infinispan-core-jakarta</artifactId> </dependency>
<!-- Add this before upgrading to find deprecated properties --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope> </dependency></dependencies>Common Issues and Solutions
Issue: Properties Migrator Not Catching Changes
If the Properties Migrator doesn’t show warnings for properties in @PropertySource files, it’s because it only analyzes main configuration files.
Solution: Manually review all @PropertySource annotations and update the referenced files.
Issue: Security Filter Chain Conflicts
If you get an error about multiple SecurityFilterChain beans:
Parameter 0 of method securityFilterChain in ... required a single bean, but 2 were foundSolution: Remove custom SecurityFilterChain beans or consolidate them into one using @Order.
Issue: Test Failures with Security
Tests that passed before are now failing with 401 Unauthorized errors.
Solution: Add security annotations to test methods:
@Test@WithMockUser(username = "admin", roles = "ADMIN")public void testAdminEndpoint() { // ...}Or use SecurityMockMvc:
@Testpublic void testAdminEndpoint() { mockMvc.perform(get("/admin") .with(SecurityMockMvcConfigurers.mockUser("admin"))) .andExpect(status().isOk());}Summary
Spring Boot 4.0.0 introduced major breaking changes. The key points:
- Java 21 is required - Update your build and CI/CD pipelines first
- Use the Properties Migrator - Add it before upgrading to catch deprecated properties
- Security defaults changed - Review your
SecurityFilterChainconfiguration - Dependencies updated - Check for
-jakartavariants and Spring Framework 7 compatibility - Configuration properties changed - Update
spring.datasource.*tospring.sql.init.*
The migration took me about two days for a mid-sized application. Most of that time was fixing configuration properties and security rules. Using the Properties Migrator tool saved hours of debugging.
If you’re planning to upgrade to Spring Boot 4.0.0, start by adding the Properties Migrator to your current 3.x project. Run it and fix all warnings before upgrading. This will save you a lot of headaches.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments