Mechanically, how do the @GetMapping-style shortcuts work as composed annotations, and how would you build a custom mapping annotation like @GetJsonMapping?
answer
- Shortcut = @interface meta-annotated with @RequestMapping
- @AliasFor forwards path/params/produces; method fixed
- AnnotatedElementUtils.findMergedAnnotation / MergedAnnotations
- Custom: meta-annotate + pin attrs + alias the rest
- Raw getAnnotation() misses composed metadata
basics
~20 sThe 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 sEach 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// 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
Not expected to know the meta-annotation mechanism.
May know shortcuts are 'just @RequestMapping' without the @AliasFor detail.
Explains @AliasFor forwarding and that RequestMappingHandlerMapping reads merged metadata.
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