skip to content

Mechanically, how do the @GetMapping-style shortcuts work as composed annotations, and how would you build a custom mapping annotation like @GetJsonMapping?

level: principalimportance: nice to knowfreq 22%

answer

  1. Shortcut = @interface meta-annotated with @RequestMapping
  2. @AliasFor forwards path/params/produces; method fixed
  3. AnnotatedElementUtils.findMergedAnnotation / MergedAnnotations
  4. Custom: meta-annotate + pin attrs + alias the rest
  5. Raw getAnnotation() misses composed metadata

basics

~20 s

The shortcuts are annotations meta-annotated with @RequestMapping, using @AliasFor to forward attributes (path, params, produces) while fixing method. Spring's AnnotatedElementUtils / MergedAnnotations resolves this. You build your own by meta-annotating a new annotation with @RequestMapping and presetting attributes.

solid answer

~40 s

Each shortcut, e.g. @GetMapping, is itself annotated with @RequestMapping(method = RequestMethod.GET). Its attributes (value/path, params, headers, consumes, produces) are declared with @AliasFor(annotation = RequestMapping.class) so that setting them on @GetMapping transparently sets the corresponding @RequestMapping attribute. Spring's annotation model — AnnotatedElementUtils and MergedAnnotations — performs this meta-annotation merging, so RequestMappingHandlerMapping only ever needs to look for the merged @RequestMapping metadata; it doesn't special-case the shortcuts. To create your own, define an annotation meta-annotated with @RequestMapping, pin the attributes you want fixed (method, produces = application/json), and expose/alias the rest. This is the same technique used by @RestController (= @Controller + @ResponseBody) and lets teams codify conventions. The key insight: composed annotations are pure metadata composition, resolved centrally, not bespoke handler logic per shortcut.

code

java · 17 lines
java
// Custom composed annotation: GET + always-JSON, with versioning header
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@RequestMapping(
    method = RequestMethod.GET,
    produces = MediaType.APPLICATION_JSON_VALUE,
    headers = "X-API-Version=2")
public @interface GetJsonV2Mapping {
    @AliasFor(annotation = RequestMapping.class, attribute = "path")
    String[] value() default {};
}

@RestController
class Api {
    @GetJsonV2Mapping("/users")   // GET /users, produces JSON, requires X-API-Version=2
    List<User> users() { ... }
}

go deeper

for a junior

Not expected to know the meta-annotation mechanism.

for a middle

May know shortcuts are 'just @RequestMapping' without the @AliasFor detail.

for a senior

Explains @AliasFor forwarding and that RequestMappingHandlerMapping reads merged metadata.

for a principal

Can author a correct custom composed mapping annotation and reason about MergedAnnotations resolution, reflection pitfalls, and abstraction trade-offs.

**Composed annotations.** A *composed annotation* is an annotation that is itself annotated with one or more other annotations (its *meta-annotations*) and optionally presets some of their attributes. Spring treats meta-annotations as if they were directly present, via its annotation utilities. The mapping shortcuts are the canonical example. **Anatomy of @GetMapping.** Simplified, it looks like: ``` @Target(METHOD) @Retention(RUNTIME) @RequestMapping(method = RequestMethod.GET) public @interface GetMapping { @AliasFor(annotation = RequestMapping.class, attribute = "value") String[] value() default {}; @AliasFor(annotation = RequestMapping.class, attribute = "path") String[] path() default {}; @AliasFor(annotation = RequestMapping.class) String[] params() default {}; @AliasFor(annotation = RequestMapping.class) String[] produces() default {}; // headers, consumes, name likewise } ``` Because `method` is fixed on the meta-annotation and NOT re-exposed, callers cannot change the verb — that's why the shortcuts have no `method` attribute. **@AliasFor.** `@AliasFor` declares that an attribute on the composed annotation is an *alias* for an attribute on the meta-annotation (or a mirror of another attribute on the same annotation, as `value`↔`path` are on `@RequestMapping` itself). When you write `@GetMapping(path = "/x", produces = "application/json")`, Spring's merging logic copies those into the synthesized `@RequestMapping`. **The resolution engine.** `RequestMappingHandlerMapping.getMappingForMethod` calls `AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class)` (backed by the `MergedAnnotations` API). This walks meta-annotations, applies `@AliasFor` mirroring, and returns a single synthesized `@RequestMapping`. Consequently the handler mapping never enumerates `@GetMapping`, `@PostMapping`, etc. individually — it only asks for merged `@RequestMapping` metadata. This is why *your own* composed annotation works with zero framework changes. **Building a custom one.** ``` @Target(METHOD) @Retention(RUNTIME) @RequestMapping(method = RequestMethod.GET, produces = "application/json") public @interface GetJsonMapping { @AliasFor(annotation = RequestMapping.class, attribute = "path") String[] value() default {}; } ``` Now `@GetJsonMapping("/users")` = `GET /users` producing JSON. Teams use this to enforce conventions (audit headers, versioning, standard produces) in one place. **Why it matters / trade-offs.** - Pros: DRY, convention-encoding, discoverable intent. - Cons: over-abstraction can hide behavior from readers and tooling; IDE navigation to the actual mapping is one hop removed; too many bespoke annotations hurt onboarding. - The mechanism also underpins `@RestController` (`@Controller` + `@ResponseBody`) and Spring Security/Data composed annotations. **Gotchas.** - `@AliasFor` requires matching types and default `{}`; misuse throws `AnnotationConfigurationException` at startup. - Attributes you *don't* alias won't propagate — a common bug when authoring custom mapping annotations. - Meta-annotation attribute overrides: a fixed attribute (like `method`) that isn't re-exposed cannot be overridden by callers. - Reflection reading annotations directly (`method.getAnnotation(RequestMapping.class)`) returns null for composed usages; you must use `AnnotatedElementUtils`/`MergedAnnotations` to see the merged form. **When to use.** Framework/platform teams standardizing controller conventions; otherwise the built-in shortcuts suffice.

  • Why does @GetMapping have no method attribute?
    Because method is fixed to GET on its meta-annotation @RequestMapping(method = GET) and is not re-exposed via @AliasFor. Callers therefore cannot change the verb, which is the whole point of a verb-specific shortcut.
  • Reading a controller method with method.getAnnotation(RequestMapping.class) returns null even though it has @GetMapping. Why?
    @GetMapping is a composed (meta) annotation; plain reflection only sees directly-present annotations. You must use AnnotatedElementUtils.findMergedAnnotation or the MergedAnnotations API to resolve the synthesized @RequestMapping.
  • What breaks if you add produces to a custom mapping annotation but forget the @AliasFor?
    If you set produces on the meta-annotation it's fixed and fine, but a locally-declared attribute without @AliasFor won't propagate to @RequestMapping, so callers setting it have no effect on the actual mapping.

saying these in an interview costs you the question

  • Thinking each shortcut has bespoke handler-mapping code
  • Believing plain reflection getAnnotation sees composed annotations
  • Assuming attributes propagate without @AliasFor
  • Claiming you can override method on @GetMapping

context