Support Spring Framework 7 (Spring Boot 4) and Spring Cloud Gateway 5.x - #826
Merged
Conversation
Spring Framework 7.0 dropped the MultiValueMap contract from HttpHeaders (spring-projects/spring-framework gh-33913), removing List get(Object). The webflux-6.x plugin was compiled against that overload, so every request through DispatcherHandler#handle threw NoSuchMethodError before the entry span was created: the service kept serving traffic but produced no traces at all. spring-cloud-gateway-4.x shades a byte-identical copy of that interceptor and its witness class still resolves on Spring Cloud Gateway 5.x, so gateway users hit it through the documented plugin as well. A bytecode sweep of every built plugin jar, resolving all org/springframework constant-pool refs against spring-web/webflux 5.3.39, 6.2.0 and 7.0.5, found only two Spring 7 removals reaching the agent, across three call sites. - spring-webflux-6.x / -5.x: read the header via HttpHeaders#getFirst(String), which is declared on HttpHeaders itself in Spring 5, 6 and 7 with the same descriptor. Note HttpHeaders#get(String) does NOT work: compiled against Spring 6 javac still emits the (Object) descriptor. getOrEmpty is not cross-version safe either, its descriptor changes between 6 and 7. - mvc-annotation-commons RequestUtil: use getValuesAsList(String). getFirst is wrong here because this site consumes every value. Note it additionally splits comma-delimited values. - mvc-annotation-commons AbstractMethodInterceptor: drop the premature reactive status read. ServerHttpResponse#getRawStatusCode() was removed in Spring 7, and it ran before the response was written so it normally read null anyway. The version-specific InvokeInterceptor already sets http.status_code in Mono#doFinally, once the status is actually known. Add Spring generation witnesses to both webflux plugins. The only guard was WitnessMethod(Mono, contextWrite), which bounds Reactor and not Spring, which is why the plugin attached to Spring 7 and then failed on every request rather than cleanly not attaching. On Reactor 3.4 both Mono#subscriberContext and Mono#contextWrite exist, so the 5.x and 6.x plugins would both match DispatcherHandler#handle on Spring Boot 2.4-2.7. Split the WebFlux application toolkit. Reactor removed Signal#getContext() in 3.5.0 and ContextView does not exist before 3.4.0, so no single artifact can serve both; continueTracing has been broken on all of Spring Boot 3 and 4 since Nov 2022. apm-toolkit-webflux becomes apm-toolkit-webflux-5.x (package ...toolkit.webflux.v5, unchanged behaviour) and a new apm-toolkit-webflux-6.x (package ...toolkit.webflux.v6) compiles against Reactor 3.5 and uses Signal#getContextView(). Compiling forward is deliberate: the module rotted silently for three years because it compiled against the oldest Reactor, so javac never flagged the removal. Distinct packages make picking the wrong artifact a compile error rather than a runtime NoSuchMethodError. The activation keeps matching the legacy un-versioned names, so applications that never upgrade their toolkit still work against a newer agent. No plugin, framework version or Reactor version loses support. Add webflux-6.x-7.x-scenario (Spring Boot 3.0 -> 4.1, one version per minor) and gateway-5.x-scenario (Spring Cloud Gateway 5.0.3 on Spring Boot 4). Both drive a full round-trip so the cross-process ref is asserted, which is the only thing that proves the sw8 header is read back correctly. Closes apache/skywalking#14047
…n docs Only stop the span this invocation created. WebFluxSkyWalkingOperatorsInterceptor created a local span conditionally — a snapshot has to be present, which it is not for an empty Reactor context or a ServerWebExchange that was never enhanced because the optional WebFlux plugin is absent — but afterMethod called ContextManager.stopSpan() and handleMethodException called activeSpan().log() unconditionally. That could stop or log against an unrelated active span. The flaw predates this PR; widening the activation to the v5/v6 packages exposes both replacement artifacts to it, so fix it here. Migrated only this interceptor to the V2 API so MethodInvocationContext can carry the "did I create a span" decision per invocation, which also handles nesting. The shared WebFluxSkyWalkingStaticMethodsAroundInterceptor base is left on V1 — its five other subclasses are pure trace-id/correlation getters that never create or stop spans — with its two stateless helpers made static so the V2 interceptor can still use them. Verified without the optional WebFlux plugin installed, so no snapshot ever reaches the Reactor context: no intercept failure, no span-lifecycle error. With the plugin installed the WebFluxOperators/onNext span and the cross-process ref are unchanged and the scenario still validates. Docs: - The migration note claimed there is no silent runtime failure. That only holds for old-toolkit/new-agent. A 9.8.0+ toolkit against a <= 9.7.0 agent compiles and runs but is silently untraced, because the older agent does not know the .v5/.v6 class names. Documented as a compatibility table, with the instruction to upgrade the agent first or together. - The continueTracing examples mixed Reactor generations: one uses Mono#subscriberContext, absent in 3.5+, so it cannot compile for the Boot 3/4 users the page now documents; another uses Mono#deferContextual and Context#of(ContextView), absent before 3.4. Labelled each by generation, and noted that the ServerWebExchange overload is generation-agnostic. - Corrected the v5 lower bound to Reactor 3.1.3. Signal#getContext() is absent in 3.1.0-3.1.2 and first appears in 3.1.3, verified against the published jars.
mrproliu
approved these changes
Aug 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix apache/skywalking#14047 —
NoSuchMethodErroron Spring Framework 7, and extend the Spring plugin family to Spring Boot 4 / Spring Cloud Gateway 5.xCHANGESlog.Closes apache/skywalking#14047.
Why the bug exists
Spring Framework 7.0 dropped the
MultiValueMapcontract fromHttpHeaders(gh-33913), which removedList get(Object):spring-webflux-6.x-pluginwas compiled against the removed overload, emittinginvokevirtual org/springframework/http/HttpHeaders.get:(Ljava/lang/Object;)Ljava/util/List;.InstMethodsInterswallows the throwable and calls through, so the application keeps serving traffic while the entry span — created four lines later — never exists. The result is exactly what the reporter saw: one ERROR per request and no traces at all.It is not limited to users who enabled the WebFlux plugin.
spring-cloud-gateway-4.x-pluginshades a byte-identical copy of that interceptor, and its witness classLocalResponseCachePropertiesstill resolves onspring-cloud-gateway-server-webflux5.0.3, so a gateway user who copies only the documented gateway plugin hits the same failure.The boundary is exact: all of spring-web 6.2.x is fine, every 7.x breaks. Spring Boot 4.0.3 pins Spring Framework 7.0.5.
How it is fixed
A bytecode sweep of every built plugin jar — resolving all
org/springframeworkconstant-pool refs against spring-web/webflux 5.3.39, 6.2.0 and 7.0.5 — found only two Spring 7 removals reaching the agent, across three call sites.spring-webflux-6.x/-5.xDispatcherHandlerHandleMethodInterceptorHttpHeaders#getFirst(String), declared onHttpHeadersitself in Spring 5, 6 and 7 with an identical descriptormvc-annotation-commonsRequestUtilgetValuesAsList(String)— this site consumes every value, sogetFirstwould be wrongmvc-annotation-commonsAbstractMethodInterceptorTwo traps worth recording:
HttpHeaders#get(String)does not fix it (compiled against Spring 6, javac still emits the(Object)descriptor), andgetOrEmptyis not cross-version safe ((Object)on 6.2.0,(String)on 7.0.5).For
getRawStatusCode(): it was removed in Spring 7, but it also ran when the controller returned itsMono, before the response was written, so it normally readnull. The version-specificInvokeInterceptoralready setshttp.status_codeinMono#doFinallyonce the status is known, so the shared module simply stops reading it — no version-specific types leak intomvc-annotation-commons.Version witnesses
The 6.x plugin's only guard was
WitnessMethod(Mono, contextWrite), which bounds Reactor, not Spring — present from 3.4.1 through 3.8.x. That is why it attached to Spring 7 and then failed per request instead of cleanly not attaching. Added:spring-webflux-5.xweb.reactive.resource.GzipResourceResolverspring-webflux-6.xweb.reactive.DispatchExceptionHandlerThis also closes a pre-existing hazard: on Reactor 3.4 both
Mono#subscriberContextandMono#contextWriteexist, so on Spring Boot 2.4–2.7 the 5.x and 6.x plugins would both matchDispatcherHandler#handle.Reactor removed
Signal#getContext()in 3.5.0, andContextViewdoes not exist before 3.4.0 — no single artifact can span both.WebFluxSkyWalkingOperators#continueTracinghas therefore been broken on all of Spring Boot 3 and 4 since Nov 2022.apm-toolkit-webflux-5.x(renamed)...toolkit.webflux.v5apm-toolkit-webflux-6.x(new)...toolkit.webflux.v6The 5.x module keeps its code and behaviour byte-for-byte; only the coordinate and package change. Distinct packages are deliberate: the two artifacts have identical public signatures, so a shared FQCN would let a wrong dependency compile and fail only at runtime. This way it is an unresolvable import. The activation still matches the legacy un-versioned names, so applications that never upgrade their toolkit keep working against a newer agent.
The 6.x module compiles against Reactor 3.5 on purpose — the root cause of the three-year silent rot was that this module compiled against the oldest Reactor (3.2, via spring-webflux 5.1), so javac never flagged the removal. Reactor 3.5–3.8 are still Java 8 bytecode, so
maven.compiler.release=8is preserved.Nothing loses support. Every Spring 3/4/5 plugin, Boot 1.x/2.x plugin and Reactor 3.1–3.4 user keeps working exactly as before.
Tests
Two new scenarios, both driving a full round-trip so the cross-process ref is asserted — the only thing that proves the
sw8header is read back correctly:webflux-6.x-7.x-scenario—GET /case/webflux(Entry,componentId 67) → WebClient (Exit,99) →/case/receive(Entry,67,refType: CrossProcess) →WebFluxOperators/onNext(Local, through the newv6toolkit package). Spring Boot 3.0 → 4.1, one version per minor.gateway-5.x-scenario— Spring Cloud Gateway 5.0.3 on Spring Boot 4, gateway → backend, asserting thecomponentId 67entry span,SpringCloudGateway/RoutingFilter/sendlocals,sendRequestexit, and the downstreamCrossProcessref.Verified locally against the mock collector and validator on Spring Boot 3.5.16 and 4.0.8 (
segment items assert successful, zerointercept failure), and Spring Cloud Gateway 5.0.3 on Boot 4.0.8.The scenario was also negative-tested: reverting the one-line fix reproduces
NoSuchMethodError: 'java.util.List org.springframework.http.HttpHeaders.get(java.lang.Object)', drops CrossProcess refs to 0, and fails the validator — so it genuinely guards this regression.Documentation
Supported-list.md(WebFlux / WebClient / Spring MVC / RestTemplate → 7.x, Spring Cloud Gateway → 5.0.x),Optional-plugins.md(spring-webflux-6.xwas never named there),agent-optional-plugins/spring-gateway.md, andApplication-toolkit-webflux.mdrewritten with both toolkit usages plus a migration note.Supported-list.mdline 10 saidSpring Boot Web 4.x, added in 2017 (e327790c36) when it meant the spring-web module 4.x. In a Spring Boot 4 PR that reads as a support claim, so it is corrected toSpring Web 4.x.