Skip to content

fix: Read class-level prefixes of composed controller annotations as Spring merges them (#672) - #690

Merged
imuromtsev merged 4 commits into
mainfrom
imuromtsev/composed-controller-prefix
Oct 14, 2026
Merged

imuromtsev merged 4 commits into
mainfrom
imuromtsev/composed-controller-prefix

Conversation

@imuromtsev

Copy link
Copy Markdown
Collaborator

Purpose

A controller annotated with a composed annotation such as

@Target(TYPE) @Retention(RUNTIME)
@RestController
@RequestMapping
@interface ApiController {
    @AliasFor(annotation = RequestMapping.class, attribute = "path")
    String[] path() default "/api";
}

is served by Spring under /api/..., but the plugin read the class-level prefix from the values written at the use site only, so @ApiController class ItemsController lost its prefix in the endpoint model and in every consumer of the class prefix. #673 added the merged-values resolver; this PR switches the class-level prefix to it. First caller group of #672.

Change

  • HandlerMethods.requestMappingPrefixes reads the class mapping through the merged UAST API (MetaAnnotationsHolder / MergedAnnotationAttributes from fix: Read composed annotations as Spring merges them (#651) #673): @AliasFor defaults and use-site values, an alias to value, a second composed level, a meta-fixed path. An unaliased attribute named like a mapping path attribute (convention mapping) sets the prefix only when the Spring major is known and below 7, as Spring does; an unknown version applies no convention.
  • Spring uses only the first @RequestMapping descriptor of an element, ordered by meta-distance (RequestMappingHandlerMapping.createRequestMappingInfo, "only the first will be used"). The prefix is read from exactly that annotation: a direct @RequestMapping wins over a composed one in any source order; ties go to declaration order. New MetaAnnotationsHolder.nearestAnnotation (+ MergedAnnotationAttributes.metaDistanceOf) implements the rule.
  • SpringWebControllerLoader reuses requestMappingPrefixes instead of its own declared-only copy. When a class cannot be converted to UAST, the declared PSI values of the nearest annotation are read as before.
  • Consumers that now get the merged prefix: the Endpoints tool window and Spring MCP endpoint tools (loader), the controller/endpoint run gutters and "Generate OpenApi Specification".

Framework evidence

Probe on real jars (spring-webmvc / spring-webflux 6.2.19 and 7.0.9), prefix registered for each case:

Case 6.2.19 7.0.9
alias to path, default /api, bare /api /api
alias, use-site /v2 /v2 /v2
two values /x, /y /x, /y
alias to value, default /dval /dval /dval
meta-fixed @RequestMapping("/fixed") /fixed /fixed
two composed levels /api /api
unaliased same-named path (convention) /conv no prefix

Not in this PR (remaining #672 groups)

  • Method-level @RequestMapping readers still on the declared PSI overload (gutters, OpenAPI endpoint info, duplicate mapping inspection), and SpringWebUtil.requestMappingOf, which merges values of every mapping annotation of a handler method instead of the nearest one.
  • @HttpExchange loader, @ConditionalOnBean/@ConditionalOnMissingBean, PropertyConditionSpecReader, OnWebApplicationCondition.

Verification

  • Red commits 721dc27 (9 reds: alias default, alias to value, two levels, convention on Spring 6, HandlerMethods; Java and Kotlin) and e716b94 (8 reds: a direct @RequestMapping beside a composed one, both source orders, loader and HandlerMethods, Java and Kotlin).
  • After the fixes: ComposedMappingDefaultPathTest 15/15, ComposedControllerPrefixSpring7Test 2/2, Kotlin ComposedControllerPrefixTest 10/10; gutter, OpenAPI and loader suites 184/184; MetaAnnotationsHolderMergedValues* 45/45.
  • All modules (:spring-bootstrap:test --rerun-tasks on 7d44c43, fresh JUnit XML): spring-core 2387, spring-web 930 (6 skipped), spring-ai 776, jpa 138, spring-data 140, base 35, aop 6, security 6, initializr 5 — 0 failures except one spring-ai ThreadLeakTracker leak assertion in SpringBootApplicationMcpToolsetOutsideProjectRootTest, which passed 4/4 when rerun alone (known flake).

Main only: MergedAnnotationAttributes (#673) is not on the release branches.

Refs #672

@github-actions

Copy link
Copy Markdown
TestsPassed ✅Skipped ⚠️Failed
JUnit Test Report4423 ran4417 passed6 skipped0 failed

@imuromtsev
imuromtsev merged commit 4a7ecf7 into main Oct 14, 2026
2 checks passed
@imuromtsev
imuromtsev deleted the imuromtsev/composed-controller-prefix branch October 14, 2026 06:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant