Skip to content

Merged composed-annotation values for the callers that still read declared values #672

Open

Description

@imuromtsev

Context

#651 added one resolver, MergedAnnotationAttributes, that reads composed annotations the way Spring merges them: @AliasFor defaults, convention-named overrides before Spring 7, and the nearest meta-annotation chain. Its PR (#673) switched only the UAST API MetaAnnotationsHolder.getAnnotationMemberValues(UAnnotation, …) to merged values. The deprecated PSI overloads still return the values written at the use site, so these callers still ignore alias defaults of composed annotations.

Callers that still read declared values only

  • Class-level controller prefixes: SpringWebControllerLoader, HandlerMethods. A composed @ApiController whose path alias defaults to /api gives the prefix "", while the method-level mapping under it is already merged.
  • Client and JAX-RS loaders: SpringHttpExchangeLoader, SpringWebFeignClientLoader, JaxRsExchangeLoader.
  • Request parameter requiredness in SpringWebUtil.
  • Conditions: @ConditionalOnBean / @ConditionalOnMissingBean strategies, PropertyConditionSpecReader (a composed @ConditionalOnProperty with an alias default, the case that motivated MetaAnnotationsHolder ignores @AliasFor attribute defaults and convention overrides of composed annotations #651), OnWebApplicationConditionStrategy.

Rule: follow how Spring reads each annotation

Spring does not merge every annotation the same way, so each caller needs a probe before it is switched:

  • OnPropertyCondition reads merged attributes, so a composed @ConditionalOnProperty's alias default applies.
  • OnClassCondition reads non-merged attributes (getAllAnnotationAttributes(…, true) → withNonMergedAttributes), so a composed @ConditionalOnClass's alias default and even a use-site alias value are ignored. MetaAnnotationsHolder ignores @AliasFor attribute defaults and convention overrides of composed annotations #651 already reads @ConditionalOnClass that way on Java and Kotlin.
  • Request mappings on classes and methods are merged (AnnotatedElementUtils.findMergedAnnotation in RequestMappingHandlerMapping).

Also seen in review

  • Spring 6 AnnotationTypeMapping.addConventionAnnotationValues: an intermediate meta-annotation's same-named, unaliased attribute overrides a deeper one (@O → @C2(mode = "M") → @Tgt(mode = "T")). The resolver answers T; probe it on 6.2.x and 7.0.x.
  • Each composed use site rebuilds the meta-path and alias sets; cache the per-type data in the module-cached holder.
  • SpringWebUtil.requestMappingOf answers path "" and all verbs when the UAST conversion fails; fall back to the declared PSI values instead.

Activity

  1. imuromtsev commented on Oct 13, 2026

    @imuromtsev
    CollaboratorAuthor

    PR #690 covers the first caller group: class-level @RequestMapping prefixes of composed controller annotations.

    • HandlerMethods.requestMappingPrefixes reads the class mapping merged (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 same-named attribute only when the Spring major is known and below 7.
    • Spring keeps only the first @RequestMapping descriptor ordered by meta-distance (RequestMappingHandlerMapping.createRequestMappingInfo, spring-webmvc 6.2.19 :352-361, "only the first will be used"). The prefix now comes from exactly that annotation: a direct @RequestMapping wins over a composed one in any source order.
    • SpringWebControllerLoader reuses requestMappingPrefixes, so the Endpoints tool window, the MCP endpoint tools, the run gutters and "Generate OpenApi Specification" get the same prefix.

    Remaining groups, in the order I plan them (line refs on main 7d9574f):

    1. Method-level @RequestMapping readers on the declared PSI overload: ControllerEndpointActionsLineMarkerProvider.kt:56-71, RequestMappingDuplicateInspection.kt:80,108,147,179, plus SpringWebUtil.requestMappingOf (SpringWebUtil.kt:390-394), which merges values of every mapping annotation of a handler method, while Spring keeps only the nearest one (same createRequestMappingInfo rule).
    2. @HttpExchange: SpringHttpExchangeLoader.kt:71,80,89. Spring reads it merged (HttpServiceMethod, spring-web 6.2.19 :329, MergedAnnotations.from(element, TYPE_HIERARCHY, …)).
    3. Conditions: ConditionalOnBeanStrategy.kt:29-40, ConditionalOnMissingBeanStrategy.kt:29-40, PropertyConditionSpecReader.kt:52-55. Boot reads @ConditionalOnProperty through metadata.getAnnotations() (merged, OnPropertyCondition 3.5.15 :55) and OnBeanCondition works on MergedAnnotations; each one gets a probe on real jars before it is switched.
    4. OnWebApplicationConditionStrategy.kt:34: Boot reads metadata.getAnnotationAttributes(ConditionalOnWebApplication) (3.5.15 :160), merged; probe first.
    5. OpenFeign (SpringWebFeignClientLoader.kt:68-118): not verified yet; the OpenFeign sources are not at hand.

    Dropped from scope:

    • Request parameter requiredness: @RequestParam, @RequestHeader, @PathVariable, @RequestBody and @CookieValue are @Target(ElementType.PARAMETER) only (spring-web 6.2.19), so they cannot sit on an annotation type and cannot be composed; declared values are the merged ones.
    • JAX-RS @Path (JaxRsExchangeLoader.kt:113-126): a JAX-RS runtime reads the annotation as declared on the resource class or method, without Spring's merged-annotation semantics, so there is nothing to merge.
  2. imuromtsev commented on Oct 15, 2026

    @imuromtsev
    CollaboratorAuthor

    Group 2 (method-level `@RequestMapping` readers) is merged as `e73a5a7fc` (PR #707).

    `HandlerMethods.methodMapping` is now the single reader for a method's mapping: it picks the one annotation Spring registers (a direct `@RequestMapping` first, else the nearest composed one, ties by source order) and takes path, verbs, `produces` and `consumes` from that winner only, with merged values and a declared-PSI fallback. `SpringWebUtil.requestMappingOf` and `getEndpointInfo`, both endpoint gutters, `RequestMappingDuplicateInspection` and the MCP `SpringMcpProvider.mediaTypesOf` all delegate to it, so the model, the gutters, OpenAPI generation and the MCP tools agree by construction.

    Probe behind it (Spring 6.2.19 and 7.0.9, MVC and WebFlux identical): a direct `@RequestMapping` beats a composed one in both source orders; two composed annotations at the same meta-distance resolve to the first in source order; a convention-named unaliased `path` applies on 6.x and is dropped on 7.x.

    Two things rode along: a declared but unreadable path (`@GetMapping(SOME_UNRESOLVED_CONST)`) no longer becomes a phantom row at the class prefix, and the private `RequestMappingVisitor` in `RequestMappingDuplicateInspection` was deleted — it had no caller and never had one.

    Still open under this issue: method-level `@HttpExchange`, `@ConditionalOnBean`/`@ConditionalOnMissingBean` together with `PropertyConditionSpecReader` (probe first — each Spring condition decides merged vs declared on its own), and `OnWebApplicationCondition`.

    Known gaps worth their own issues, named in the PR: the PSI fallback reads declared values, so a compiled library mapping source still loses an `@AliasFor` default; a verb given through a constant (`method = M`) is named after the constant; and the gutters still fall back to the class prefix for an unreadable path, as they did before. An inspection for "several mapping annotations on one element, only the first is used" is also worth filing — Spring logs exactly that warning.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    in:spring-coreSpring Core or Spring Boot Core issues

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions