I have spent the last few months taking a mid-sized Spring service from Spring Framework 6.2 to Spring Framework 7.0, and the thing nobody tells you up front is this: most of the migration content you find is actually about Spring Boot. Boot 4 gets all the headlines. But Boot 4 sits on top of Framework 7, and the changes that broke my build, my tests, and (twice) my production behaviour came from the core framework — bean lifecycle, AOT metadata, the Jakarta cutover, and a pile of quietly removed APIs.
This guide is about the layer underneath Boot. If you maintain a library, a non-Boot Spring application, or you just want to understand why Boot 4 forces certain changes, this is the migration you need to read. Framework 7.0 went GA on 13 November 2025, and everything below is verified against the official 7.0 release notes — not guessed from a beta.
First, the mental model: Framework 7 is not Boot 4
Spring Boot 4.0 is the product most teams upgrade. But Boot 4 depends on Spring Framework 7.0 the same way Boot 3 depended on Framework 6. Almost every “Boot 4 broke my code” story traces back to a Framework 7 decision: the Jakarta EE 11 baseline, the removal of javax annotation support, the JSpecify null-safety migration, the AOT reachability-metadata change. If you understand Framework 7, Boot 4 stops being mysterious.
A useful rule while migrating: when a class lives in org.springframework.*, it is a Framework 7 concern and this guide covers it. When it lives in org.springframework.boot.*, check the Spring Boot 3 to 4 migration guide instead. The two upgrades happen together, but they fail for different reasons.
Baseline upgrades: what you need before you start
Framework 7.0 keeps a JDK 17 baseline — you are not forced onto Java 25 — but it recommends JDK 25 (the current LTS) and adopts Jakarta EE 11. The minimum versions moved up across the board:
| Component | Spring Framework 6.2 | Spring Framework 7.0 |
|---|---|---|
| JDK | 17 (baseline), 21 recommended | 17 (baseline), 25 recommended |
| Jakarta EE | 9/10 | 11 |
| Servlet | 6.0 (Tomcat 10.1, Jetty 12) | 6.1 (Tomcat 11, Jetty 12.1) |
| JPA | 3.1 | 3.2 (Hibernate ORM 7.1/7.2) |
| Bean Validation | 3.0 | 3.1 (Hibernate Validator 9.0/9.1) |
| Kotlin | 1.x/2.0 | 2.2 |
| GraalVM | 22.3+ | 25 (new metadata format) |
| JUnit | 5 (Jupiter) | 6 |
| Jackson | 2.x | 3.x default, 2.x deprecated |
The one that surprised me: Undertow support is gone. Undertow does not yet support Servlet 6.1, so Spring dropped its Undertow-specific WebSocket and WebFlux HTTP classes. If you run Undertow, you are blocked on the migration until Undertow ships a Servlet 6.1 release — plan around that early, because there is no flag to work past it.
The Jakarta cutover that actually bites: javax annotations
Most teams finished the javax.* → jakarta.* move during the Boot 2→3 era. But Framework 6 quietly kept a compatibility bridge for two annotation packages. Framework 7 removes it. Support for javax.annotation and javax.inject annotations is completely gone:
// These no longer do anything in Spring 7 — silently ignored:
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.inject.Inject;
// Replace with the Jakarta equivalents:
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.inject.Inject;
This is the most dangerous change in the whole release, because it fails silently. Code using @javax.annotation.PostConstruct still compiles (the annotation is on the classpath via some transitive dependency), but Spring 7 no longer recognises it, so your init method just never runs. No exception, no log line. Grep your entire codebase for javax.annotation and javax.inject before you trust your test suite.
Removed APIs (these are hard compile errors — the easy part)
Compile errors are the friendly failures: the compiler tells you exactly where to look. Here is what Framework 7 deleted outright.
| Removed | Replacement |
|---|---|
spring-jcl module | Apache Commons Logging 1.3+ (transitive, usually transparent) |
javax.annotation / javax.inject support | jakarta.annotation / jakarta.inject |
ListenableFuture | CompletableFuture |
suffixPatternMatch, trailingSlashMatch, favorPathExtension | PathPattern-based matching (the default) |
PathExtensionContentNegotiationStrategy | Explicit content negotiation config |
| Undertow WebSocket / WebFlux support | Tomcat 11+ or Jetty 12.1+ (Servlet 6.1) |
Theme support | External theming / i18n libraries |
| OkHttp3 client support | JdkClientHttpRequestFactory or Reactor Netty |
webjars-locator-core | webjars-locator-lite |
The ListenableFuture removal is the one I hit most. Any async method that returned ListenableFuture<T> — common in older @Async code and in Kafka/AMQP callback chains — now has to return CompletableFuture<T>. The good news is CompletableFuture has a richer API, so the rewrite usually simplifies the code.
Deprecated APIs: what still works today but won’t in 7.1 / 7.2
Deprecations are your migration runway. Nothing here breaks on the day you upgrade to 7.0, but each one has a removal timeline, and ignoring them just defers the pain. The ones that matter for application code:
| Deprecated in 7.0 | Migrate to | Notes |
|---|---|---|
RestTemplate (docs-level; @Deprecated in 7.1) | RestClient / HTTP interface clients | See “the state of HTTP clients” from the Spring team |
| Jackson 2.x support | Jackson 3.x (tools.jackson package) | Auto-detection off in 7.1, removed in 7.2 |
AntPathMatcher for request mappings | PathPattern | Now supports leading /**/ segments |
PathMatcher in Spring MVC | PathPattern | Affects Spring Security path alignment |
| JUnit 4 support in TestContext | SpringExtension + JUnit Jupiter | SpringRunner, SpringClassRule etc. deprecated |
<mvc:*> XML namespace | Java config | <bean> namespace is NOT deprecated |
document / feed view classes (PDF, RSS, XLS) | Render with external libraries in handlers | Will be removed in a future version |
| Spring nullness annotations (JSR 305) | JSpecify annotations | See Null Safety section below |
My advice: treat the Jackson 2.x and RestTemplate deprecations as the highest-priority follow-ups. Both have a hard removal date (Jackson 2.x in 7.2), and both touch a lot of surface area. I cover the Jackson side in detail in the Jackson 2 to Jackson 3 migration guide, and the client side in Spring Boot 4 HTTP Service Clients.
Bean lifecycle and registration changes
This is the section I wish I had read first, because the changes are subtle and they interact with AOT.
Programmatic registration gets a first-class API: BeanRegistrar
Before 7.0, registering beans programmatically meant a BeanDefinitionRegistryPostProcessor and hand-built RootBeanDefinition objects — verbose, and invisible to the AOT engine. Framework 7 introduces the BeanRegistrar contract, which is both cleaner and AOT-analysable at build time (so it works in GraalVM native images):
class MyBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean("foo", Foo.class);
registry.registerBean("bar", Bar.class, spec -> spec
.prototype()
.lazyInit()
.description("Custom description")
.supplier(context -> new Bar(context.bean(Foo.class))));
if (env.matchesProfiles("baz")) {
registry.registerBean(Baz.class, spec -> spec
.supplier(context -> new Baz("Hello World!")));
}
}
}
You import it with @Import(MyBeanRegistrar.class) on a configuration class. If you have ever written conditional or loop-driven bean registration, this replaces a lot of brittle low-level code — and unlike the old approach, it does not silently break native-image builds.
Proxy defaulting is now consistently CGLIB — and @Proxyable lets you opt out
In 6.x, proxy-type defaulting was inconsistent: Boot defaulted to CGLIB, but some processors (like @Async) did their own thing. As of 7.0, CGLIB defaulting is applied consistently to all proxy processors. If you relied on a JDK dynamic proxy being created for an @Async bean, that assumption may no longer hold. The new @Proxyable annotation gives you per-bean control:
@Service
@Proxyable(INTERFACES) // force a JDK interface proxy even under a CGLIB default
public class OrderService implements OrderOperations { /* ... */ }
// Or suggest specific proxy interfaces:
@Proxyable(interfaces = OrderOperations.class)
public class OrderService implements OrderOperations, AuditAware { /* ... */ }
The rule that AOT now enforces
Framework 7 formalises two long-standing recommendations: never register several beans inside a single @Bean method, and always declare the most concrete type as a @Bean method’s return type. These were always “best practice” — under AOT they become close to mandatory, because the build-time engine reads your declared return type to generate bean metadata. A method declared as returning an interface can leave AOT unable to see the concrete type.
AOT and GraalVM native image changes
If you build native images — or you use Spring’s AOT processing to speed up JVM startup — this is the section with the most teeth. Framework 7 switches to GraalVM’s unified “exact reachability metadata” format, and two changes will affect any code that contributes RuntimeHints.
Resource hints moved from regex to glob patterns
The resource-hint syntax changed from java.util.regex.Pattern to glob patterns, and the matching semantics changed with it. Previously "/files/*.ext" matched both /files/a.ext and /files/folder/b.ext. Now * matches a single path segment only:
// Spring 6 (regex) — matched nested folders too:
hints.resources().registerPattern("/files/*.ext");
// Spring 7 (glob) — to match nested paths you must be explicit:
hints.resources().registerPattern("/files/**/*.ext");
Registration of “excludes” has been removed entirely. If your native build suddenly cannot find a resource it used to bundle, a too-narrow glob is the first place to look.
A reflection type hint now implies its members
Registering a reflection hint for a type now automatically implies methods, constructors, and field introspection. As a result, ExecutableMode.INTROSPECT and every MemberCategory except the INVOKE_* values are deprecated. Most hint code gets shorter:
// Spring 6:
hints.reflection().registerType(MyType.class, MemberCategory.DECLARED_FIELDS);
// Spring 7 — the type hint alone is enough:
hints.reflection().registerType(MyType.class);
Pairing this with BeanRegistrar (above) is the cleanest way to keep a 7.0 app native-image friendly. If you want background on why AOT and startup caching matter at all, I wrote up the JVM side in Project Leyden: AOT compilation and smart caching.
Null safety: JSpecify replaces the Spring JSR 305 annotations
Spring’s own nullness annotations (org.springframework.lang.Nullable / @NonNull, which carried JSR 305 semantics) are deprecated in favour of JSpecify. The whole framework codebase has been re-annotated with JSpecify, which can specify nullness for generic types, arrays, and vararg elements — things JSR 305 could not express.
// Old (deprecated):
import org.springframework.lang.Nullable;
// New:
import org.jspecify.annotations.Nullable;
For Java code this is mostly a find-and-replace plus a dependency. Kotlin teams should pay close attention: because Kotlin maps these annotations to platform vs. nullable types, the refined JSpecify nullness on Spring’s APIs can turn previously-compiling Kotlin into a compile error (or vice versa). Expect to revisit some !! and ? usages after the upgrade.
The HttpHeaders breaking change (web apps, read this)
In 7.0, HttpHeaders no longer implements MultiValueMap. HTTP headers are case-insensitive and behave more like a collection of pairs than a map, so the map contract was a poor fit. Several map-style methods were removed; a few fallbacks like HttpHeaders#asMultiValueMap() exist but are immediately @Deprecated.
// Spring 6: treated headers as a Map
MultiValueMap<String, String> map = headers; // worked
headers.put("X-Trace", List.of(traceId)); // map-style
// Spring 7: use the header-specific API
headers.add("X-Trace", traceId);
headers.set("X-Trace", traceId);
List<String> values = headers.get("X-Trace");
// asMultiValueMap() exists but is @Deprecated — do not lean on it
If you have utility code, filters, or tests that cast HttpHeaders to MultiValueMap, those will not compile. Search for that cast specifically.
Testing changes
The TestContext framework got several useful upgrades, and one genuine breaking change.
- JUnit 6 baseline. Framework 7 targets JUnit Jupiter on the JUnit 6 platform, and JUnit 4 support in the TestContext framework (
SpringRunner,SpringClassRule,AbstractJUnit4SpringContextTests) is deprecated. If you are still on JUnit 4, migrate now — I have a prompt-driven approach in 10 AI prompts to optimise and update JUnit 6 tests. - SpringExtension context scope changed (breaking).
SpringExtensionnow uses a test-method-scopedExtensionContext. This fixes DI consistency in@Nestedhierarchies, but a customTestExecutionListeneroverridingprepareTestInstancemay break — look up the current test class viatestContext.getTestInstance().getClass()rather thantestContext.getTestClass(). If@Nestedtests start failing, annotate the top-level class with@SpringExtensionConfig(useTestClassScopedExtensionContext = true)or setspring.test.extension.context.scope=test_class. - Application context pausing. Cached test contexts can now be paused when idle and restarted on demand, so background threads do not run between tests. Control it with
spring.test.context.cache.pause(always/never). - Bean overrides on non-singletons.
@MockitoBean,@MockitoSpyBean, and@TestBeannow work on prototype and custom-scoped beans. - New
RestTestClient. A non-reactive sibling ofWebTestClient— the fluent assertion API without dragging in the reactive stack. Bind it to a live server, a single@Controller, or the context.
Security implications
Framework 7 does not contain Spring Security — that ships separately as Spring Security 7 — but several Framework 7 changes directly affect how security behaves, which is exactly where silent issues hide.
- Path matching alignment. The deprecation of
PathMatcher/AntPathMatcherand theHandlerMappingIntrospectorSPI changes how Spring MVC and Spring Security agree on which path a request maps to. If your security rules use Ant-style patterns, verify they still match the same URLs underPathPattern— a mismatch here means a rule silently stops protecting an endpoint. - The javax removal touches security too. Any security config or filter relying on
javax.annotationlifecycle hooks will silently skip initialisation, exactly as described earlier. - CORS pre-flight behaviour changed. As of 7.0, CORS pre-flight (
OPTIONS) requests are no longer rejected when the CORS configuration is empty. Re-check that you are not relying on the old rejection as an implicit guard.
Because “it compiles and starts” tells you almost nothing about whether your app is still protected, I treat the security layer as its own migration pass with its own tests. I walk through the Security 5→6→7 specifics — SecurityFilterChain, the lambda DSL, the silent authorization changes — in the Spring Security 5 to 6 to 7 migration guide.
Worth adopting while you are in there
A migration is the cheapest time to pick up the new APIs, because you are already touching and re-testing the code.
- API versioning. First-class support in MVC and WebFlux for mapping requests by API version, marking versions deprecated, and setting the version on the client side (
RestClient,WebClient, HTTP interfaces) and in tests (MockMvc,WebTestClient). - Core resilience. The old
spring-retryproject is merged intospring-core(org.springframework.core.retry):RetryTemplate,RetryPolicy, plus@Retryableand@ConcurrencyLimitinspring-context, enabled with@EnableResilientMethods.@Retryableeven adapts automatically to reactive return types. JmsClient. A fluent client for JMS in the spirit ofJdbcClientandRestClient, andJdbcClientitself gains statement-level settings (fetch size, max rows, query timeout).- HTTP interface client groups.
@ImportHttpServicesturns a pile of@HttpExchangeinterfaces into auto-registered client proxies grouped by host — covered in depth in Spring Boot 4 HTTP Service Clients.
The upgrade checklist (in the order I’d do it)
- Get to Spring Framework 6.2 first and clear every deprecation warning. Migrating from the latest 6.x is far easier than jumping from an older 6.0/6.1.
- Confirm your servlet container supports Servlet 6.1 (Tomcat 11+, Jetty 12.1+). If you run Undertow, stop here — it is not yet supported.
- Bump the Jakarta EE 11 dependencies: Servlet 6.1, JPA 3.2 (Hibernate ORM 7.1/7.2), Bean Validation 3.1.
- Grep for
javax.annotationandjavax.injectacross the whole codebase and replace withjakarta.*. This is the silent-failure risk — do it before you trust any test run. - Fix hard compile errors from removed APIs:
ListenableFuture→CompletableFuture, removed path-mapping options,HttpHeadersmap casts. - Migrate nullness annotations to JSpecify; Kotlin teams budget extra time for nullability fallout.
- If you use AOT / native images: update
RuntimeHints(glob resource patterns, simplified reflection hints) and consider moving programmatic registration toBeanRegistrar. - Review proxy assumptions (consistent CGLIB default) and add
@Proxyablewhere you need a specific proxy type. - Run the test suite; fix
SpringExtensionscope and@Nestedissues. Migrate any remaining JUnit 4 tests. - Re-test the security layer end to end — path-matching alignment and CORS pre-flight especially.
- Schedule the deprecation follow-ups with hard deadlines: Jackson 2.x (removed in 7.2) and RestTemplate (
@Deprecatedin 7.1).
An AI prompt to scan your project for Framework 7 blockers
Paste this into Claude, ChatGPT, Gemini, or Cursor with your repository in context. It is tuned to the specific Framework 7 changes above, not generic “upgrade my Spring” advice.
You are auditing a Java/Kotlin codebase for a Spring Framework 6.2 -> 7.0 migration.
Scan the project and produce a prioritised report with file paths and line numbers.
Flag, in this order of severity:
1. SILENT FAILURES: any use of javax.annotation.* or javax.inject.* (e.g.
@PostConstruct, @PreDestroy, @Resource, @Inject) — these compile but are
ignored in Spring 7. List each and give the jakarta.* replacement.
2. HARD COMPILE ERRORS: ListenableFuture usage, HttpHeaders cast to
MultiValueMap, removed path-mapping options (suffixPatternMatch,
trailingSlashMatch, favorPathExtension), Undertow-specific config.
3. DEPRECATIONS WITH DEADLINES: Jackson 2.x (com.fasterxml.jackson) usage,
RestTemplate usage, AntPathMatcher/PathMatcher in MVC or Security config,
JUnit 4 + SpringRunner/SpringClassRule, <mvc:*> XML config.
4. NULL SAFETY: org.springframework.lang.Nullable/@NonNull to migrate to
org.jspecify.annotations.*; for Kotlin, flag APIs whose nullability may shift.
5. AOT/NATIVE: RuntimeHints using regex resource patterns or MemberCategory
values other than INVOKE_*; BeanDefinitionRegistryPostProcessor that could
become a BeanRegistrar.
6. SECURITY RISK: Ant-style path patterns in security rules that may match
different URLs under PathPattern; reliance on empty-CORS pre-flight rejection.
For each finding give: file:line, the risk, and the exact replacement code.
End with an ordered migration plan grouped by severity.
For a broader set of Spring automation prompts, see 10 AI prompts for Spring Boot development.
Frequently asked questions
Does Spring Framework 7 require Java 21 or 25?
No. Framework 7.0 keeps a JDK 17 baseline. JDK 25 (the current LTS) is recommended and unlocks features like the Class-File API path, but 17 is the minimum. This is different from Spring Boot 4, which raises its own baseline — check the Boot 3 to 4 guide for that.
Do I migrate Spring Framework 7 and Spring Boot 4 separately?
In practice they move together — Boot 4 pulls in Framework 7. But the failures have different root causes. Framework 7 owns the Jakarta cutover, bean lifecycle, AOT, and null-safety changes; Boot 4 owns autoconfiguration, properties, and starter changes. Knowing which layer a class belongs to tells you which guide to read.
What is the most dangerous change to miss?
The removal of javax.annotation / javax.inject support. It fails silently: @PostConstruct methods simply never run, with no error. Grep for those packages before trusting your tests.
Is RestTemplate removed in Spring 7?
Not yet. In 7.0 it is deprecated at the documentation level; it becomes @Deprecated in code in 7.1. You can keep using it for now, but new code should use RestClient or HTTP interface clients.
Can I still use Jackson 2 with Spring 7?
Yes, for now — Spring 7 defaults to Jackson 3.x but falls back to 2.x. However, 2.x auto-detection is disabled in 7.1 and support is removed entirely in 7.2, so plan the move. See the Jackson 3 migration guide.
Does Undertow work with Spring 7?
No. Framework 7 requires Servlet 6.1, which Undertow does not yet support, so Spring removed its Undertow-specific classes. You must move to Tomcat 11+ or Jetty 12.1+, or wait for an Undertow release compatible with Servlet 6.1.
See also
- Spring Boot 3 to 4 Migration Guide
- Spring Security 5 to 6 to 7 Migration Guide
- Hibernate 5 to 6 to 7 Migration Guide
- Jackson 2 to Jackson 3 Migration Guide
- Spring Boot 4 HTTP Service Clients
- Project Leyden: AOT Compilation and Smart Caching
- Java 25 LTS: Every JEP That Matters
- 10 AI Prompts for Spring Boot Development
Conclusion
The Spring Framework 6→7 migration follows the same pattern I have seen on every major Spring upgrade: the compile errors are the easy part, and the silent behaviour changes are what reach production. The deleted javax annotation support, the HttpHeaders contract change, the consistent CGLIB defaulting, and the security path-matching alignment are the ones that pass your build and then surprise you at runtime. Work the checklist top to bottom, lean on the AI prompt to find the silent failures fast, and treat the security layer as its own pass. Do that, and Framework 7 — with its AOT-friendly bean registration, JSpecify null safety, and built-in resilience — is a genuinely better foundation than 6.2.