Skip to content

fix: Read method request mappings from the single annotation Spring registers (#672) - #707

Merged
imuromtsev merged 4 commits into
mainfrom
imuromtsev/composed-method-mappings
Oct 15, 2026
Merged

imuromtsev merged 4 commits into
mainfrom
imuromtsev/composed-method-mappings

Conversation

@imuromtsev

Copy link
Copy Markdown
Collaborator

Refs #672

Method-level request mappings were read from declared annotation values, and from every mapping annotation at once.
Spring does neither.

What Spring does

RequestMappingHandlerMapping.createRequestMappingInfo (spring-webmvc 6.2.19 :347-361, WebFlux :195-204) takes
requestMappings.get(0) and logs "Multiple @RequestMapping annotations found on …, but only the first will be used".
Probe on 6.2.19 and 7.0.9 (MVC and WebFlux gave identical results):

Method annotations registered
bare composed @ApiGet (alias path default /items), class /api GET /api/items
@GetMapping("/a") @PostMapping("/b") GET /api/a only — reversed order: POST /api/b only
direct @RequestMapping("/d") + @ApiGet, both source orders one any-verb /d; the composed GET is ignored
convention-named unaliased path applies on 6.2.19, dropped on 7.0.9

So: one annotation wins (a direct @RequestMapping first, else the nearest composed one, ties by source order), and every
attribute — path, verbs, produces, consumes — comes from that winner. Nothing is folded in from the losers.

What changed

HandlerMethods.methodMapping(member, mah) is now the single reader: it picks the winner with
MetaAnnotationsHolder.nearestAnnotation, reads merged values, and falls back to declared PSI values when there is no UAST
element, exactly as requestMappingPrefixes does for the class level (#690). Every caller delegates to it:
SpringWebUtil.requestMappingOf and getEndpointInfo, ControllerEndpointActionsLineMarkerProvider,
EndpointRunLineMarkerProvider, RequestMappingDuplicateInspection and the MCP SpringMcpProvider.mediaTypesOf, which had the
same union defect for produces/consumes.

User-visible removal: where a method carried several mapping annotations, the Endpoints tool window and the MCP tools listed
a row per combination. They now list the one route Spring serves. A follow-up inspection ("several mappings on one element, only
the first is used") is worth filing separately.

Two smaller things in the same area:

  • A declared but unreadable path (@GetMapping(SOME_UNRESOLVED_CONST)) no longer becomes a phantom row at the class prefix.
    "" now means "this mapping declares no path", which is what it meant before this PR; an unreadable one yields no row.
  • The private RequestMappingVisitor in RequestMappingDuplicateInspection is deleted. It had no caller and never had one
    (git log -S over the constructor call is empty; only checkClass is overridden), so it was dead code that this PR would
    otherwise have carried forward in rewritten form.

Known gaps (unchanged by this PR, worth their own issues)

  • The PSI fallback reads declared values, so a mapping source without UAST (a compiled library interface) still loses an
    @AliasFor default. This is the agreed shape of the class-level reader too.
  • A verb given through a constant (method = M where val M = RequestMethod.GET) is named after the constant. Pre-existing in
    every reader.
  • The gutters and getEndpointInfo still fall back to the class prefix for a declared-but-unreadable path; they did so before
    this PR as well (…mapNotNull{…}.firstOrNull() ?: "" on main).

Tests

Red first: loader/{java,kotlin}/ComposedMethodMappingTest.kt + ComposedMethodMappingFixture.kt and two cases in
inspections/{java,kotlin}/RequestMappingDuplicateInspectionTest.kt — 30 of 48 failing on the pre-fix head, with the Spring 7
classes and the use-site-alias cases green as controls. The fixtures mirror the probe sources, including both source orders.

Verification on the final head: target classes 54 tests, 0 failures; spring-web loader.* + providers.* + inspections.*
708 tests, 0 failures; spring-ai mcp.* 793 tests, 0 failures (one Exodus thread-leak flake in the batch run passed
when the class was rerun alone).

Backport

Main-only: HandlerMethods, MergedAnnotationAttributes and nearestAnnotation exist on no release/* branch.

@github-actions

Copy link
Copy Markdown
TestsPassed ✅Skipped ⚠️Failed
JUnit Test Report4630 ran4624 passed6 skipped0 failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant