Migration to Java 25 and Spring Boot 4: honestly, it was chaos
“I’m ready… to break everything and figure it out later!” – SpongeBob SquarePants
Everyone always says that upgrading Spring is easy, right? Just run OpenRewrite, fix a few small things, and you’re done.
Well… let me tell you about our migration from Java 17/21 to Java 25 and from Spring Boot 3 to 4.0.1.
For a single service it now takes at most an hour to fix everything. Not too bad. But I’ve already done this migration a few times for clients. And I keep running into the same problems, over and over again.
So I thought: let me write this down. Maybe it helps someone else who has to go through this migration.
Why we did this in the first place
Our tech lead wanted to move to Java 25. His reason? He always wants the newest version of everything. And honestly, I was pretty excited to experience a migration like this.
On top of that, we had to:
- Upgrade to Spring Boot 4 and Spring Framework 7
- Fully finish the Jakarta migration
- Clean up old code before adding new features
All of our applications contain:
- REST APIs
- Custom HTTP clients
- JWT security
- File uploads
- A lot of tests with MockMvc
- Jackson for JSON everywhere
OpenRewrite: helpful, but not magic
We started with OpenRewrite and the official Spring recipes (source).
What Rewrite did well:
javax.*→jakarta.*- Updating Spring imports
- Fixing deprecated APIs
- Adjusting config classes
- Simplifying try-catch syntax
.additionalMessageConverters(new MappingJackson2HttpMessageConverter())→.additionalMessageConverters(new JacksonJsonHttpMessageConverter()).format(...)→.formatted(...)- Splitting YAML per profile
But… problems showed up pretty quickly.
OpenRewrite + JDK/JRE mismatch
The first time I ran OpenRewrite on Java 25 it crashed with:
java.lang.LinkageError: loader constraint violation: com.sun.tools.javac.parser.Tokens$Comment$CommentStyle
Cause: JDK and JRE were not the same version.
Fix:
- Always check that JDK and JRE are the same version
- For us: set both to Java 25
“Everything compiles!” … then the tests explode
After Rewrite and fixing all compilation errors, we started the tests. And then? Everything failed.
Most problems were in:
- MockMvc tests
- JSON serialization
- HTTP clients
- Jackson config
Jackson: the pain that never ends
Spring Boot 4 moves Jackson from:
Old location:
com.fasterxml.jackson.*
New location:
tools.jackson.*
Problems we ran into:
- Mixed imports in tests
- Constants like
WRITE_DATES_AS_TIMESTAMPSdisappeared - ObjectMapper conflicts, especially in tests with multiple primary beans
Before we had:
@Bean
@Primary
ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.dateFormat(...).build();
}
In Boot 4:
Jackson2ObjectMapperBuilderis deprecated- Boot creates its own primary
jacksonJsonMapper
Result:
NoUniqueBeanDefinitionException:
more than one 'primary' bean found: [objectMapper, jacksonJsonMapper]
New approach:
@Bean
public ObjectMapper objectMapper() {
var dateFormat = new SimpleDateFormat("dd-MM-yyyy");
return JsonMapper.builder()
.defaultDateFormat(dateFormat)
.findAndAddModules()
.build();
}
Source: Introducing Jackson 3 support in Spring
RestTemplate & HTTP changes
RestTemplateBuilder moved
Old location:
org.springframework.web.client.RestTemplateBuilder
New location:
org.springframework.boot.restclient.RestTemplateBuilder
In production code you’ll often migrate to RestClient, which Spring Boot 4 prefers.
Response API changed
Old way:
response.getStatusCodeValue()
New way:
response.getStatusCode().value()
Headers replaced
Old Apache way:
Header[] headers = response.getAllHeaders();
New Spring way:
HttpHeaders headers = response.getHeaders();
Timeouts are now builders
The old setters no longer exist. New:
HttpComponentsClientHttpRequestFactory requestFactory =
ClientHttpRequestFactoryBuilder.httpComponents()
.withDefaultRequestConfigCustomizer(rc -> rc
.setConnectTimeout(Timeout.ofSeconds(5))
.setConnectionRequestTimeout(Timeout.ofSeconds(5))
.setResponseTimeout(Timeout.ofSeconds(5))
)
.build();
Custom HttpClients now have to be added via .withHttpClientCustomizer(...).
Source: Spring Framework 7 HTTP Client
Apache HttpClient 4 → 5
Everything changed:
org.apache.http.*→org.apache.hc.*- Timeouts, SSL, connection managers, request builders → complete rewrite
Source: Apache HttpClient 5 Migration
Swagger & OpenAPI after Jakarta
After the migration, OpenAPI annotations disappear. Add:
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
Source: Swagger
Spring Security 7 WebSocket
AbstractSecurityWebSocketMessageBrokerConfigurer is gone. For us, this approach works:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.messaging.EnableWebSocketSecurity;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.nullDestMatcher().permitAll()
.simpSubscribeDestMatchers("/**").permitAll()
.simpDestMatchers("/**").permitAll()
.anyMessage().permitAll();
return messages.build();
}
@Bean
public AuthorizationManager<MessageAuthorizationContext<?>> authorizationManager() {
return (_, _) -> new AuthorizationDecision(true);
}
}
Source: Spring Security 7 WebSocket, Github Thread
Recurring problems
During the migration, most issues kept coming back, especially in tests:
- Jackson namespace constantly breaks
- ObjectMapper conflicts in tests
- HTTP client configuration has to be fully rewritten
- Response handling methods changed
- RestTemplate and builders have new imports or are replaced by RestClient
- Tests often fail even though compilation succeeds
On top of that, the compiler, serialization layer, SSL configuration, and auto-configuration all get affected at the same time by the upgrade.
What I would recommend
- Plan extra time for tests
- Expect Jackson problems and ObjectMapper conflicts
- Be prepared to rewrite HTTP client code
- Run your full test suite early and often
- Use OpenRewrite
Personally, I’m glad I got to do this migration myself. Despite the frustrations and all the documentation reading, it gave me a much deeper understanding of how Spring Boot and Spring Framework work and how much control they have over infrastructure and configuration.
Want to learn more about Spring Boot 4 and Spring Framework 7? Check out this post: Spring Boot 4 & Spring Framework 7.
Overview of all sources
| Resource | Link |
|---|---|
| OpenRewrite recipes | https://docs.openrewrite.org/recipes/java |
| Spring Framework 7 HTTP Client | https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-request-factories |
| Apache HttpClient 5 Migration | https://hc.apache.org/httpcomponents-client-5.6.x/migration-guide/preparation.html |
| Swagger (swagger-core) | https://github.com/swagger-api/swagger-core |
| Spring Security 7 WebSocket (PR) | https://github.com/spring-projects/spring-security/pull/17328 |
| Spring Security 7 (discussion thread) | https://github.com/spring-projects/spring-security/pull/17328 |
| Spring Boot 4 & Spring Framework 7 (overview) | https://www.baeldung.com/spring-boot-4-spring-framework-7 |