Spring Boot 4.1 and Spring Framework 7
What changed in the Boot 4 generation: Jakarta EE 11, Jackson 3, modular starters, new test packages, API versioning and built-in retries.
spring-boot-starter-webmvc (was spring-boot-starter-web)
tools.jackson.databind.json.JsonMapper (was com.fasterxml.jackson.databind.ObjectMapper)
@GetMapping(path = "/orders", version = "2")This course targets Spring Boot 4.1 on Spring Framework 7 and Java 25. Boot 4 is a major release, so code and answers written for Boot 3 are often close but wrong. Knowing what moved saves hours of confused compiler errors.
The platform underneath
- Java: Boot 4 needs at least Java 17. Use the newest LTS you can; on Java 21 and later,
spring.threads.virtual.enabled=trueruns request handling on virtual threads. - Jakarta EE 11: everything is still in
jakarta.*packages, now at Servlet 6.1, Persistence 3.2 and Bean Validation 3.1, with Hibernate 7 as the JPA provider. - Jackson 3: the JSON library moved to
tools.jackson.*packages (tools.jackson.databind.json.JsonMapper). The annotations such as@JsonPropertystay incom.fasterxml.jackson.annotation, andJacksonExceptionis now unchecked.
Modular Boot
Boot 4 split its large auto-configuration jar into one module per technology, each with its own starter. Spring MVC now comes from spring-boot-starter-webmvc, and each area has a matching test starter such as spring-boot-starter-webmvc-test. Classes moved with them:
| Boot 3 | Boot 4 |
|---|---|
o.s.boot.test.autoconfigure.web.servlet.WebMvcTest | o.s.boot.webmvc.test.autoconfigure.WebMvcTest |
o.s.boot.autoconfigure.jdbc.DataSourceAutoConfiguration | o.s.boot.jdbc.autoconfigure.DataSourceAutoConfiguration |
@MockBean | @MockitoBean (o.s.test.context.bean.override.mockito) |
com.fasterxml.jackson.databind.ObjectMapper | tools.jackson.databind.json.JsonMapper |
Here o.s is short for org.springframework. When an import from an older tutorial does not resolve, the class has usually moved rather than disappeared.
New in Framework 7 for API developers
- API versioning: mapping annotations take a
versionattribute, and Boot configures how the version is read withspring.mvc.apiversion.*. - Resilience:
@Retryableand@ConcurrencyLimitinorg.springframework.resilience.annotation, switched on with@EnableResilientMethods, cover what used to need Spring Retry. - HTTP service clients:
@ImportHttpServicesregisters interface-based clients backed byRestClient. - Null safety: the Spring APIs are annotated with JSpecify (
org.jspecify.annotations.Nullable), so tools can warn when you ignore a possible null.
Upgrading an existing service
Move to the latest Boot 3.5 first and remove every deprecation warning; Boot 4 removed most of what 3.x deprecated. Then switch the parent version, rename starters, fix imports and run the full test suite. Do not mix Jackson 2 and Jackson 3 types in one code path: a Jackson 2 ObjectMapper bean is not what Spring MVC uses to write responses any more.
Example
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
record OrderV1(long id, String status) {
}
record OrderV2(long id, String status, List<String> items) {
}
("/orders")
public class OrderController {
// Chosen when the request asks for version 1 (see spring.mvc.apiversion.* settings).
(version = "1")
List<OrderV1> listV1() {
return List.of(new OrderV1(1, "PAID"));
}
// Same path, new response shape for version 2 clients.
(version = "2")
List<OrderV2> listV2() {
return List.of(new OrderV2(1, "PAID", List.of("book", "lamp")));
}
}
When to use it
- Fix the imports of a controller test copied from a Boot 3 project so it compiles on Boot 4.
- Serve two response shapes from one endpoint while mobile clients upgrade, selected by a version header.
- Replace the Spring Retry dependency with Framework 7's built-in @Retryable during an upgrade.
More examples
Reading the API version from a header
The version attribute on a mapping does nothing until you tell Spring where the version comes from. A header keeps URLs stable; spring.mvc.apiversion.use.path-segment and use.query-parameter are the alternatives. Setting a default keeps existing clients that send no header working after you add version 2.
# Clients send X-API-Version: 2; requests without it get version 1.
spring.mvc.apiversion.use.header=X-API-Version
spring.mvc.apiversion.default=1Customising the Jackson 3 mapper
A customiser adjusts the JsonMapper that Boot builds instead of replacing it, so Boot's other settings and modules stay in place. Note the imports: the customiser interface is in org.springframework.boot.jackson.autoconfigure and the feature enum is in tools.jackson.databind. A Jackson 2 customiser carried over from a Boot 3 project does not touch the Jackson 3 mapper that Spring MVC uses.
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import tools.jackson.databind.DeserializationFeature;
@Configuration
public class JsonConfig {
@Bean
JsonMapperBuilderCustomizer strictJson() {
// Reject request bodies that contain fields the DTO does not declare.
return builder -> builder.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
}Retries without an extra library
@Retryable is part of Spring Framework 7 itself and does nothing until @EnableResilientMethods is present on a configuration class. Retrying only ResourceAccessException means connection failures are retried while a 400 from the remote service fails immediately, which is what you want: retrying a request the other side rejected only adds load.
import org.springframework.context.annotation.Configuration;
import org.springframework.resilience.annotation.EnableResilientMethods;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClient;
@Configuration
@EnableResilientMethods
class ResilienceConfig {
}
@Service
public class ExchangeRateService {
private final RestClient rates = RestClient.create("https://rates.example.com");
// Up to three more attempts on network errors, waiting 200 ms, then 400 ms, then 800 ms.
@Retryable(includes = ResourceAccessException.class, maxRetries = 3, delay = 200, multiplier = 2)
public String latest(String currency) {
return rates.get().uri("/latest/{currency}", currency).retrieve().body(String.class);
}
}
Discussion