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.

Syntaxspring-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=true runs 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 @JsonProperty stay in com.fasterxml.jackson.annotation, and JacksonException is 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 3Boot 4
o.s.boot.test.autoconfigure.web.servlet.WebMvcTesto.s.boot.webmvc.test.autoconfigure.WebMvcTest
o.s.boot.autoconfigure.jdbc.DataSourceAutoConfigurationo.s.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
@MockBean@MockitoBean (o.s.test.context.bean.override.mockito)
com.fasterxml.jackson.databind.ObjectMappertools.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 version attribute, and Boot configures how the version is read with spring.mvc.apiversion.*.
  • Resilience: @Retryable and @ConcurrencyLimit in org.springframework.resilience.annotation, switched on with @EnableResilientMethods, cover what used to need Spring Retry.
  • HTTP service clients: @ImportHttpServices registers interface-based clients backed by RestClient.
  • 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

Example · java

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) {
}

@RestController
@RequestMapping("/orders")
public class OrderController {

    // Chosen when the request asks for version 1 (see spring.mvc.apiversion.* settings).
    @GetMapping(version = "1")
    List<OrderV1> listV1() {
        return List.of(new OrderV1(1, "PAID"));
    }

    // Same path, new response shape for version 2 clients.
    @GetMapping(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.

Example · properties
# Clients send X-API-Version: 2; requests without it get version 1.
spring.mvc.apiversion.use.header=X-API-Version
spring.mvc.apiversion.default=1

Customising 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.

Example · java
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.

Example · java
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

  • Be the first to comment on this lesson.