Spring Framework 6 to 7 Migration Guide: Breaking Changes, Deprecated APIs, and Upgrade Checklist

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:

ComponentSpring Framework 6.2Spring Framework 7.0
JDK17 (baseline), 21 recommended17 (baseline), 25 recommended
Jakarta EE9/1011
Servlet6.0 (Tomcat 10.1, Jetty 12)6.1 (Tomcat 11, Jetty 12.1)
JPA3.13.2 (Hibernate ORM 7.1/7.2)
Bean Validation3.03.1 (Hibernate Validator 9.0/9.1)
Kotlin1.x/2.02.2
GraalVM22.3+25 (new metadata format)
JUnit5 (Jupiter)6
Jackson2.x3.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.

RemovedReplacement
spring-jcl moduleApache Commons Logging 1.3+ (transitive, usually transparent)
javax.annotation / javax.inject supportjakarta.annotation / jakarta.inject
ListenableFutureCompletableFuture
suffixPatternMatch, trailingSlashMatch, favorPathExtensionPathPattern-based matching (the default)
PathExtensionContentNegotiationStrategyExplicit content negotiation config
Undertow WebSocket / WebFlux supportTomcat 11+ or Jetty 12.1+ (Servlet 6.1)
Theme supportExternal theming / i18n libraries
OkHttp3 client supportJdkClientHttpRequestFactory or Reactor Netty
webjars-locator-corewebjars-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.0Migrate toNotes
RestTemplate (docs-level; @Deprecated in 7.1)RestClient / HTTP interface clientsSee “the state of HTTP clients” from the Spring team
Jackson 2.x supportJackson 3.x (tools.jackson package)Auto-detection off in 7.1, removed in 7.2
AntPathMatcher for request mappingsPathPatternNow supports leading /**/ segments
PathMatcher in Spring MVCPathPatternAffects Spring Security path alignment
JUnit 4 support in TestContextSpringExtension + JUnit JupiterSpringRunner, SpringClassRule etc. deprecated
<mvc:*> XML namespaceJava config<bean> namespace is NOT deprecated
document / feed view classes (PDF, RSS, XLS)Render with external libraries in handlersWill be removed in a future version
Spring nullness annotations (JSR 305)JSpecify annotationsSee 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). SpringExtension now uses a test-method-scoped ExtensionContext. This fixes DI consistency in @Nested hierarchies, but a custom TestExecutionListener overriding prepareTestInstance may break — look up the current test class via testContext.getTestInstance().getClass() rather than testContext.getTestClass(). If @Nested tests start failing, annotate the top-level class with @SpringExtensionConfig(useTestClassScopedExtensionContext = true) or set spring.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 @TestBean now work on prototype and custom-scoped beans.
  • New RestTestClient. A non-reactive sibling of WebTestClient — 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/AntPathMatcher and the HandlerMappingIntrospector SPI 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 under PathPattern — 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.annotation lifecycle 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-retry project is merged into spring-core (org.springframework.core.retry): RetryTemplate, RetryPolicy, plus @Retryable and @ConcurrencyLimit in spring-context, enabled with @EnableResilientMethods. @Retryable even adapts automatically to reactive return types.
  • JmsClient. A fluent client for JMS in the spirit of JdbcClient and RestClient, and JdbcClient itself gains statement-level settings (fetch size, max rows, query timeout).
  • HTTP interface client groups. @ImportHttpServices turns a pile of @HttpExchange interfaces 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)

  1. 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.
  2. Confirm your servlet container supports Servlet 6.1 (Tomcat 11+, Jetty 12.1+). If you run Undertow, stop here — it is not yet supported.
  3. Bump the Jakarta EE 11 dependencies: Servlet 6.1, JPA 3.2 (Hibernate ORM 7.1/7.2), Bean Validation 3.1.
  4. Grep for javax.annotation and javax.inject across the whole codebase and replace with jakarta.*. This is the silent-failure risk — do it before you trust any test run.
  5. Fix hard compile errors from removed APIs: ListenableFutureCompletableFuture, removed path-mapping options, HttpHeaders map casts.
  6. Migrate nullness annotations to JSpecify; Kotlin teams budget extra time for nullability fallout.
  7. If you use AOT / native images: update RuntimeHints (glob resource patterns, simplified reflection hints) and consider moving programmatic registration to BeanRegistrar.
  8. Review proxy assumptions (consistent CGLIB default) and add @Proxyable where you need a specific proxy type.
  9. Run the test suite; fix SpringExtension scope and @Nested issues. Migrate any remaining JUnit 4 tests.
  10. Re-test the security layer end to end — path-matching alignment and CORS pre-flight especially.
  11. Schedule the deprecation follow-ups with hard deadlines: Jackson 2.x (removed in 7.2) and RestTemplate (@Deprecated in 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

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.