Baeldung

Java, Spring and Web Development tutorials

 

Spring Boot 4 & Spring Framework 7 – What’s New
2025-09-12 01:33 UTC by Ralf Ueberfuhr

1. Overview

In late 2022, Spring Boot 3 and Spring Framework 6 brought the most significant shift in the ecosystem since its inception. They introduced a Java 17 baseline, a migration from javax.* to jakarta.*, and early support for GraalVM native images.

Now, in 2025, the next generation is almost here: Spring Boot 4 and Spring Framework 7.

Both releases continue the modernization journey. They adopt recent Java language features and provide tighter Jakarta EE 11 alignment. They also improve developer productivity and offer resilient application support out of the box.

In this article, we’ll walk through the major themes of these releases, with explanations and code samples that highlight what developers can expect.

2. Baseline Upgrades

Before diving into features, it’s important to note the new baselines.

Because of the wide adoption in the industry at the moment, Java 17 is still the minimum requirement. Java 21 and Java 25 are strongly recommended to take advantage of new JVM features like virtual threads. We can find an official statement in the Spring Blog.

With Spring Framework 7, Jakarta EE 11 is now fully adopted. That means we’re moving up to Servlet 6.1, JPA 3.2, and Bean Validation 3.1.

On the Kotlin side, version 2.2 and above is now supported. This brings smoother coroutine integration and makes working with reactive code feel even more natural.

3. Spring Boot 4

With its fourth major release, Spring Boot brings several improvements. It enhances performance, observability, maintainability, and configuration support. These changes further strengthen its role as the foundation for modern cloud-native Java applications.

3.1. Native Image Improvements

Spring Boot 4 continues its strong push toward GraalVM native image support. It’s fully aligned to GraalVM 24. Ahead-of-Time (AOT) processing has been enhanced, meaning faster build times and reduced startup memory footprint.

For example, Spring Data introduces AOT Repositories, i.e., that AOT processing will turn query methods into source code that will be compiled together with the application.

3.2. Observability: Micrometer 2 and OpenTelemetry

Cloud-native apps rely on good observability. Spring Boot 3 introduced Spring Observability. Spring Boot 4 upgrades to Micrometer 2 and integrates an OpenTelemetry starter. This is making traces, logs, and metrics work seamlessly together.

3.3. Improved SSL Health Reporting

If a certificate chain contains certificates that will expire soon, we now see them in a new expiringChains entry. The WILL_EXPIRE_SOON status is gone. Instead, expiring certificates are reported as VALID.

These changes make it easier for teams to monitor SSL certificate validity in production environments without false alarms.

3.4. Modularization

One of the first milestones for Spring Boot 4 was the refactoring of its own codebase into a more modular structure.

In Spring Boot 3, many of the core modules (like auto-configuration, starter dependencies, and build tools) were bundled in larger artifacts. While convenient, this sometimes made it harder to manage dependencies, contributed to classpath scanning overhead, and increased the native-image footprint.

Starting with Spring Boot 4, the team has begun splitting auto-configurations and support code into smaller, more focused modules. This internal modularization means:

  • Faster builds and native-image generation. GraalVM AOT processing doesn’t need to deal with unnecessary hints and metadata.

  • Cleaner dependency management. Optional integrations (like Micrometer, OpenTelemetry, or specific persistence technologies) are in separate modules instead of bundled together.

  • Improved maintainability for the Spring team and contributors. Modules map more directly to features.

As a developer, we may not notice this change directly in our pom.xml or build.gradle. If we use starter dependencies, we don’t need any changes. For example, when we need JPA with Hibernate, we just add this dependency to our pom.xml:

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

The difference is under the hood: the JPA auto-configuration, the Hibernate integration, and the validation setup are now part of separate modules, which allows the framework to be more selective when processing configurations at runtime or during AOT compilation.

If we do not use starter dependencies, we have to take care of the changes. We can find details about the new modules in the Spring Boot GitHub Wiki.

3.5. New @ConfigurationPropertiesSource Annotation

Another feature for better modularization is the new annotation named @ConfigurationPropertiesSource. This annotation does not change how configuration properties are bound at runtime. Instead, it acts as a hint for the spring-boot-configuration-processor during build time.

When the processor generates metadata for @ConfigurationProperties classes, it usually collects information from the same module where the class is defined. In modular projects, however, we sometimes rely on nested types or base classes that live in different modules, where source code is not available during build time. In such cases, the generated metadata may be incomplete — for example, property descriptions or default values might be missing.

By marking a class with @ConfigurationPropertiesSource, we instruct the processor to generate full metadata for it, even if it is not directly annotated with @ConfigurationProperties. In practice, this means we no longer need to worry about missing metadata when working across modules. The processor takes care of it for us.

4. Spring Framework 7

Spring Framework 7 arrives with a mix of long-requested features and thoughtful refinements across testing, API design, and core infrastructure. These changes modernize the framework while reducing boilerplate for everyday development.

4.1. Testing Improvements

Spring uses Context Caching during tests to find a balance between test performance and isolation. We can find details about that and the resulting pitfalls, and possible solutions in this article.

Spring Framework 7 introduces test context pausing. Previously, long-lived integration tests consumed resources even when idle. Now, Spring can pause and resume contexts stored in the context cache, saving memory and speeding up test execution in large suites. This is helpful, e.g., for JMS listener containers or scheduled tasks.

Additionally, a new RestTestClient makes it easier to test REST endpoints similar to WebTestClient, but without pulling in reactive infrastructure:

This brings REST testing closer to the simplicity of WebTestClient, but without requiring reactive dependencies:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class HelloWorldApiIntegrationTest {
    RestTestClient client;
    @BeforeEach
    void setUp(WebApplicationContext context) {
        client = RestTestClient.bindToApplicationContext(context)
            .build();
    }
    @Test
    void shouldFetchHelloV1() {
        client.get()
            .uri("/api/v1/hello")
            .exchange()
            .expectStatus()
            .isOk()
            .expectHeader()
            .contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
            .expectBody(String.class)
            .consumeWith(message -> assertThat(message.getResponseBody()).containsIgnoringCase("hello"));
    }
}

4.2. API Versioning

One of the most requested new features comes with first-class API versioning.

Traditionally, we had to roll our own solutions—through URL path conventions (/v1/), custom headers, or media types. Now, the framework provides native support. We can now specify a version attribute, as shown in this sample:

@RestController
@RequestMapping("/hello")
public class HelloWorldController {
    @GetMapping(version = "1", produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHelloV1() {
        return "Hello World";
    }
    @GetMapping(version = "2", produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHelloV2() {
        return "Hi World";
    }
 
}

We could specify the version at the controller level too:

@RestController
@RequestMapping(path = "/hello", version = "3")
public class HelloWorldV3Controller {
    @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHello() {
        return "Hey World";
    }
}

Then, we need to configure the mapping strategy, which can be one of:

  • path-based mapping (e.g. /api/v1/hello vs. /api/v2/hello)
  • query-parameter-based (e.g. /hello?version=1 vs. /hello?version=2)
  • request-header-based (e.g. X-API-Version: 1 vs. X-API-Version: 2)
  • mediatype-header-based (e.g. Accept: application/json; version=1 vs. Accept: application/json; version=2)

The following configuration uses path-based mapping:

@Configuration
public class ApiConfig implements WebMvcConfigurer {
    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        configurer.usePathSegment(1);
    }
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer.addPathPrefix("/api/v{version}", HandlerTypePredicate.forAnnotation(RestController.class));
    }
}

Spring will automatically resolve the version. This makes it far easier to evolve APIs without breaking existing clients.

4.3. Smarter HTTP Clients With @HttpServiceClient

Another notable feature is the declarative HTTP client support. Inspired by Feign, but lighter and fully integrated.

In older Spring versions, we needed to create a proxy for the HttpInterface. Smarter solutions were possible, but built individually. For example, in this repository, we can find a sample with a custom @HttpClient annotation and a custom Bean Registrar (which was also improved with Spring Framework 7, as we can see in this article).

Now, we have a built-in solution with the @HttpServiceClient annotation. Let’s see an example:

@HttpServiceClient("christmasJoy")
public interface ChristmasJoyClient {
    @GetExchange("/greetings?random")
    String getRandomGreeting();
}

Then, we need to activate classpath scanning and configure the service group that the client is assigned to:

@Configuration
@Import(HttpClientConfig.HelloWorldClientHttpServiceRegistrar.class)
public class HttpClientConfig {
    static class HelloWorldClientHttpServiceRegistrar extends AbstractClientHttpServiceRegistrar {
        @Override
        protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
            findAndRegisterHttpServiceClients(registry, List.of("com.baeldung.spring.mvc"));
        }
    }
    @Bean
    RestClientHttpServiceGroupConfigurer christmasJoyServiceGroupConfigurer() String baseUrl) {
        return groups -> {
            groups.filterByName("christmasJoy")
                .forEachClient((group, clientBuilder) -> {
                    clientBuilder.baseUrl("https://christmasjoy.dev/api");
                });
        };
    }
}

The ChristmasJoyClient is then available for injection into other Spring components as usual:

@RestController
@RequestMapping(path = "/hello", version = "4")
@RequiredArgsConstructor
public class HelloWorldV4Controller {
    private final ChristmasJoyClient christmasJoy;
    @GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
    public String sayHello() {
        return this.christmasJoy.getRandomGreeting();
    }
}

4.4. Resilience Annotations

Spring Retry has been part of the ecosystem for years, but it always felt like an “add-on”. In Spring Framework 7, resilience is now built-in. We can annotate Spring component methods with Spring annotations to add retry logic or concurrency limits directly:

@HttpServiceClient("christmasJoy")
public interface ChristmasJoyClient {
    @GetExchange("/greetings?random")
    @Retryable(maxAttempts = 3, delay = 100, multiplier = 2, maxDelay = 1000)
    @ConcurrencyLimit(3)
    String getRandomGreeting();
}

Those annotations are ignored by default, unless we add @EnableResilientMethods to one of our configurations.

This greatly simplifies adding resilience patterns without needing additional libraries like Resilience4j, although they still integrate nicely.

This makes it much easier to verify resilience policies at runtime, ensuring that our annotations are actually being applied.

4.5. Multiple TaskDecorator Beans

In earlier Spring versions, when we wanted to customize the execution of asynchronous tasks, we could register a single TaskDecorator on a ThreadPoolTaskExecutor. This allowed us to, for example, propagate the SecurityContext or logging MDC into asynchronous threads. However, if we had multiple concerns to apply, we had to create a composite decorator manually.

Starting with Spring Framework 7, we can now declare multiple TaskDecorator beans in the application context. Spring will automatically compose them into a chain. Each decorator is applied in turn, in the order of their bean definition or @Order annotation.

For example, we have an asynchronous event listener:

@Component
@Slf4j
public class HelloWorldEventLogger {
    @Async
    @EventListener
    void logHelloWorldEvent(HelloWorldEvent event) {
        log.info("Hello World Event: {}", event.message());
    }
}

When we want simple logging and timestamp measuring, we could simply register two TaskDecorator beans:

@Configuration
@Slf4j
public class TaskDecoratorConfiguration {
    @Bean
    @Order(2)
    TaskDecorator loggingTaskConfigurator() {
        return runnable -> () -> {
            log.info("Running Task: {}", runnable);
            try {
                runnable.run();
            } finally {
                log.info("Finished Task: {}", runnable);
            }
        };
    }
    @Bean
    @Order(1)
    TaskDecorator measuringTaskConfigurator() {
        return runnable -> () -> {
            final var ts1 = System.currentTimeMillis();
            try {
                runnable.run();
            } finally {
                final var ts2 = System.currentTimeMillis();
                log.info("Finished within {}ms (Task: {})", ts2 - ts1, runnable);
            }
        };
    }
}

The resulting logging outputs are:

Running Task: com.baeldung.spring.mvc.TaskDecoratorConfiguration$$Lambda/0x00000ff0014325f8@57e8609
Hello World Event: "Happy Christmas"
Finished within 0ms (Task: java.util.concurrent.FutureTask@bb978d6[Completed normally])
Finished Task: com.baeldung.spring.mvc.TaskDecoratorConfiguration$$Lambda/0x00000ff0014325f8@57e8609

This improvement eliminates the need for boilerplate composite decorators and makes it easier to combine multiple cross-cutting concerns in asynchronous code.

4.6. Null Safety With JSpecify

Nullability annotations have been all over the place in the Java ecosystem (@Nonnull, @Nullable, @NotNull, etc.). With Spring Framework 7, the team adopts JSpecify as the standard:

@Configuration
public class ApiConfig implements WebMvcConfigurer {
    @Override
    public void configureApiVersioning(@NonNull ApiVersionConfigurer configurer) {
        configurer.usePathSegment(1);
    }
}

This improves IDE tooling and Kotlin interop, reducing the risk of NullPointerExceptions in larger codebases.

5. Deprecations and Removals

With modernization comes cleanup:

  • javax.* packages are gone — only Jakarta EE 11 is supported.

  • Jackson 2.x support is dropped; Spring 7 expects Jackson 3.x.

  • Spring JCL (logging bridge) is removed in favor of Apache Commons Logging.

  • JUnit 4 support is deprecated — use JUnit 5 exclusively.

If we still rely on these older APIs, migration should be part of our upgrade plan.

6. Conclusion

Spring Boot 4 and Spring Framework 7 are not just incremental releases. They are a deliberate step into a modern, modular, cloud-native era of Java development:

  • API versioning and resilience annotations make applications easier to evolve and harden.

  • JSpecify null safety and Kotlin support to reduce runtime errors.

  • Declarative HTTP clients simplify service-to-service calls.

  • Native image support and observability tooling improve cloud readiness.

As always with major upgrades, the key is to start testing our applications early, especially around dependency upgrades and deprecated APIs. But the benefits in productivity, performance, and maintainability make the transition worthwhile.

As always, the code presented in this article is available over on GitHub.

The post Spring Boot 4 & Spring Framework 7 – What’s New first appeared on Baeldung.
       

 

Content mobilized by FeedBlitz RSS Services, the premium FeedBurner alternative.