Spring Framework 7 is the seventh major version of the Spring core framework, with new features such as built-in retry and API versioning. Version 7.0 was released in November 2025. It still runs on Java 17, and Java 25 is the recommended version.
We upgrade to Spring Framework 7 when we move a Spring Boot application to Spring Boot 4, because Spring Boot 4.0 and 4.1 are built on it. Spring applications without Spring Boot move to it from Spring Framework 6.2, whose open source support ended in June 2026.
The following example uses Spring Framework 7.0.9 (the version Spring Boot 4.1.1 brings) on Java 25, without Spring Boot. A BeanRegistrar registers the beans in Java code, and @Retryable calls a failing method again. The complete project with tests for every snippet is on GitHub.
@Configuration
@EnableResilientMethods // enables @Retryable and @ConcurrencyLimit
@Import(LibraryRegistrar.class) // a BeanRegistrar registers the beans
public class LibraryConfig {
}
try (var context = new AnnotationConfigApplicationContext(LibraryConfig.class)) {
ShelfClient shelf = context.getBean(ShelfClient.class);
int copies = shelf.copies("dune"); // 3, after 2 failed calls retried by @Retryable
}
Notice that we add no Spring Retry or Resilience4j dependency, because retry is built into Spring 7.
The summary table lists every notable change, and the sections after it show each change with a short example.
1. Spring Framework 7 Changes at a Glance
Spring Framework 7 needs newer library versions, adds new APIs and removes most code that 6.x deprecated. The type column shows whether a change needs upgrade work (Baseline, Changed, Removed) or is optional (New). Baseline means a new minimum version.
| Change | Type | What it does | More |
|---|---|---|---|
| Java 17 minimum, Java 25 recommended | Baseline | Same minimum as 6.x | section 2 |
| Jakarta EE 11, Kotlin 2.2, JUnit 6 | Baseline | Servlet 6.1 and JPA 3.2; Tomcat 11.0 or Jetty 12.1 | section 2 |
| JSpecify null-safety | New | Spring APIs say which values can be null | section 3.1 |
| API versioning | New | Picks the controller method by request version | section 3.2 |
| HTTP service groups | New | @ImportHttpServices creates HTTP clients by group | section 3.3 |
| @Retryable, @ConcurrencyLimit | New | Retry and parallel-call limits without Spring Retry | section 3.4 |
| BeanRegistrar | New | Registers beans in code with loops and if checks | section 3.5 |
| Optional in SpEL | New | ?. and ?: treat an empty Optional like null | section 3.6 |
| JmsClient | New | Sends and receives JMS messages, like JdbcClient | section 3.7 |
| Jackson 3, HttpMessageConverters | Changed | Jackson 3 by default; converters set up in one place | section 3.8 |
| CGLIB proxies by default, @Proxyable | Changed | Same proxy type for @Async, @Retryable and others | Proxying |
| RestTestClient, context pausing | New | WebTestClient without reactive code; paused test contexts | section 4 |
| HttpHeaders | Changed | No longer extends MultiValueMap | section 5 |
| javax.annotation, javax.inject, spring-jcl, Undertow | Removed | Use the jakarta packages and Apache Commons Logging | section 5 |
| RestTemplate, JUnit 4 support, Jackson 2.x | Deprecated | Still work in 7.0, removal is planned | section 5 |
2. Java 17, Jakarta EE 11 and Kotlin 2.2 Baselines
Spring Framework 7 still needs only Java 17, like 6.x, so an app on Java 17 or 21 can upgrade without changing the JDK. Java 25 is the recommended version for production. The bigger change is on the Jakarta side. Spring Framework 7 requires Jakarta EE 11, so the servlet container (Tomcat or Jetty) and the JPA provider (Hibernate) must move to their EE 11 versions together with Spring.

For a web app, the upgrade means Tomcat 11.0 (or Jetty 12.1), Hibernate ORM 7.1+ and Hibernate Validator 9. Undertow has no release for Servlet 6.1 yet, so Spring 7 does not support it. Kotlin projects need Kotlin 2.2, and tests move to JUnit 6.
3. New Features in Spring Framework 7
All features in this part work without Spring Boot, and the Spring Boot 4 tutorial shows them with the starters. The examples use a small library app.
3.1. JSpecify Null-Safety for Spring APIs and Our Code
Every Spring 7 API says whether a value can be null. It uses JSpecify annotations, a shared standard for null annotations in Java, instead of Spring’s older @Nullable and @NonNullApi. Kotlin 2 reads them as nullable or non-null types.
In our own code, @NullMarked on a package makes every type non-null by default, and @Nullable marks the values that can be null.
@NullMarked // everything in the package is non-null by default
package com.howtodoinjava.library.web;
public @Nullable Book find(String title) { // null for an unknown title
return books.get(title);
}
The annotations change nothing at runtime. A build-time checker such as NullAway (a compiler plugin) reports a call like find(title).copies() without a null check.
3.2. API Versioning in Spring MVC and WebFlux
Spring 7 adds API versioning to Spring MVC and WebFlux. A mapping declares its version with the new version attribute, such as @GetMapping(path = “/books/{title}”, version = “2”), and Spring calls the method that matches the version in the request. In WebMvcConfigurer, we read the version from a header, a query parameter, a path segment or a media type parameter.
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useQueryParam("version").setDefaultVersion("1"); // ?version=2, or 1 when missing
}
With methods for version 1 and version 2, the same URL returns different responses, and an unknown version gets a 400 response.
| Request | Handler | Response |
|---|---|---|
| GET /books/dune | version 1 (default) | 200 {“title”:”dune”,”copies”:3} |
| GET /books/dune?version=2 | version 2 | 200 {“title”:”dune”,”copies”:3,”shelf”:”A3″} |
| GET /books/dune?version=3 | none | 400 Bad Request, InvalidApiVersionException |
3.3. HTTP Service Groups With @ImportHttpServices
An HTTP interface client is an interface with @HttpExchange methods, and in 6.x each one needed its own @Bean method. In Spring 7, @ImportHttpServices lists the interfaces in groups, typically one group per remote server. Spring creates one HTTP client per group and a bean for every interface.
Say our library API reads stock counts from a warehouse service. We put the StockClient interface in the “stock” group and configure the RestClient of that group once.
@Configuration(proxyBeanMethods = false)
@ImportHttpServices(group = "stock", types = StockClient.class)
public class HttpClientsConfig {
@Bean
RestClientHttpServiceGroupConfigurer stockGroup(Environment env) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setReadTimeout(Duration.ofSeconds(2));
return groups -> groups.filterByName("stock").forEachClient((group, builder) -> builder
.baseUrl(env.getRequiredProperty("stock.base-url"))
.requestFactory(requestFactory));
}
}
We inject StockClient like any bean, and stockClient.copies(“dune”) returns 3 from the warehouse service.
3.4. @Retryable and @ConcurrencyLimit in the Core Framework
Spring 7 moves a trimmed version of Spring Retry into the framework. We turn on @Retryable and @ConcurrencyLimit from org.springframework.resilience.annotation with @EnableResilientMethods, as the intro example does.
By default, @Retryable retries any exception up to 3 times with a 1 second delay, and when all attempts fail, the caller gets the last exception. The name is the same as in Spring Retry, so only the imports change. The @ConcurrencyLimit annotation limits how many threads run a method at the same time, and the other callers wait for a free slot.
@Retryable(includes = ShelfBusyException.class, maxRetries = 2, delay = 100, multiplier = 2)
public int copies(String title) {
return scanner.count(title); // ShelfBusyException twice, then 3
}
@ConcurrencyLimit(2)
public String reserve(String title) {
return scanner.reserve(title); // 5 callers, at most 2 run at the same time
}
To retry in code, we use RetryTemplate from org.springframework.core.retry. Spring Framework 7 has no circuit breaker or rate limiter, so we keep Resilience4j for those.
3.5. Programmatic Bean Registration With BeanRegistrar
A @Bean method registers one bean. So in 6.x, one bean per configured value needed a low-level BeanDefinitionRegistryPostProcessor. The new BeanRegistrar interface has one method that gets a BeanRegistry and the Environment. Inside it, we register beans with plain Java loops and if checks.
For example, each library branch needs its own HelpDesk bean, so we register one bean per name in the library.branches property.
@Override
public void register(BeanRegistry registry, Environment env) {
String[] branches = env.getProperty("library.branches", String[].class, new String[0]);
for (String branch : branches) { // library.branches=north,south
registry.registerBean(branch + "Desk", HelpDesk.class, // northDesk, southDesk
spec -> spec.supplier(ctx -> new HelpDesk(branch)));
}
}
We activate a registrar with @Import on a @Configuration class, as the intro example does.
3.6. Optional Support in SpEL Expressions
In SpEL (the expression language of @Value), the operators ?. and ?: treat an empty Optional like null and use the value inside a present one. For example, #shelf ?: ‘none’ returns “none” for Optional.empty() and “a3” for Optional.of(“a3”). The expression #shelf?.toUpperCase() returns null or “A3”.
3.7. JmsClient for JMS Messaging
After RestClient and JdbcClient in 6.1, Spring 7 adds JmsClient, a fluent client to send and receive messages on a JMS queue or topic. It works on top of JmsTemplate, so our existing JMS connection setup keeps working.
3.8. Jackson 3 as the Default JSON Library
Spring 7 uses Jackson 3 (package tools.jackson) everywhere and falls back to the deprecated Jackson 2 support only when Jackson 3 is missing. The new HttpMessageConverters API sets up the converters (classes that turn objects into JSON and back) for Spring MVC and RestClient together, e.g. a JacksonJsonHttpMessageConverter with our own JsonMapper. The Jackson 3 migration guide covers the new imports and the changed defaults.
4. Testing Changes in Spring Framework 7
The main new test API is RestTestClient, a version of WebTestClient for apps without reactive code. We bind it to a controller, an application context, a MockMvc instance or a running server, and the test code stays the same.
RestTestClient client = RestTestClient.bindToApplicationContext(context).build();
client.get().uri("/books/dune?version=2").exchange()
.expectStatus().isOk()
.expectBody().jsonPath("$.shelf").isEqualTo("A3");
Existing tests see two changes. The Spring TestContext Framework supports JUnit 6 and deprecates its JUnit 4 support (SpringRunner). Also, a cached test context is paused while tests use another context, so its scheduled tasks do not run in between.
5. Removed and Deprecated APIs in Spring Framework 7
Removed APIs break the build or the startup, so we check them first. Deprecated APIs still work in 7.0.
| API or feature | Status in 7.0 | Replacement |
|---|---|---|
| javax.annotation (@PostConstruct, @Resource) and javax.inject (@Inject) | Removed | jakarta.annotation and jakarta.inject |
| spring-jcl module | Removed | Apache Commons Logging 1.3 |
| ListenableFuture | Removed | CompletableFuture |
| HttpHeaders as a MultiValueMap | Changed | HttpHeaders methods |
| RestTemplate | Deprecation announced, @Deprecated in 7.1, removal in 8.0 | RestClient |
| Jackson 2.x support | Deprecated, removal planned in 7.2 | Jackson 3.x |
RestTemplate still works in 7.0, but new HTTP client code should use RestClient or HTTP interface clients. Existing RestTemplate code keeps working until 8.0, so we can migrate one client at a time.
6. Upgrading From Spring Framework 6.2 to 7
Spring Boot users get Spring Framework 7 with Spring Boot 4. Apps without Spring Boot change the spring-framework-bom version, which sets the version of every Spring module. We run the tests after each step, and apps on 5.3 first follow the javax to jakarta move from What’s New in Spring 6.
- Upgrade to the latest 6.2.x and fix every deprecation warning, because most 7.0 removals were deprecated in 6.x.
- Replace javax.annotation and javax.inject imports with their jakarta versions.
- Upgrade the server and Hibernate as section 2 lists, and move JSON code to Jackson 3.
- Replace ListenableFuture with CompletableFuture and remove any explicit spring-jcl dependency.
7. Conclusion
Spring Framework 7 keeps Java 17 but moves to Jakarta EE 11, JUnit 6 and Jackson 3. Most upgrade work comes from these versions and the removed javax annotations, not from our Spring code.
The new features are optional and work without Spring Boot. For new code, we use JSpecify annotations, @Retryable, RestClient and RestTestClient, because Spring 7.1 and 8.0 build on these APIs.
8. References
- Spring Framework 7.0 Release Notes
- Spring Framework 7.0 General Availability
- Spring Framework Versions
- Resilience Features
- Introducing Jackson 3 support in Spring
Happy Learning !!