skip to content

Explain how @RestController is composed from stereotype meta-annotations, and how component scanning discovers it as a bean.

level: seniorimportance: should knowfreq 45%

answer

  1. @Component <- @Controller <- @RestController
  2. @RestController also carries @ResponseBody
  3. @AliasFor bridges value to @Component name
  4. AnnotationTypeFilter(Component) matches transitively
  5. isHandler() = @Controller OR @RequestMapping (meta-aware)

basics

~20 s

@RestController is a composed annotation carrying @Controller (which itself carries @Component) plus @ResponseBody. Spring's component scanning finds @Component transitively through these meta-annotations, so a @RestController class is auto-registered as a bean and recognized as a web handler.

solid answer

~40 s

Spring stereotypes are built by meta-annotation composition. @Component is the root; @Controller is meta-annotated with @Component; @RestController is meta-annotated with @Controller and @ResponseBody. During classpath scanning (@ComponentScan / Spring Boot's @SpringBootApplication), the ClassPathScanningCandidateComponentProvider uses an AnnotationTypeFilter for @Component that matches transitively — Spring's MergedAnnotations/AnnotationMetadata walks the meta-annotation graph, so @Controller and @RestController are treated as @Component and their classes become bean definitions. Separately, RequestMappingHandlerMapping detects handler beans by looking for the @Controller stereotype (or @RequestMapping) via AnnotatedElementUtils, which also traverses meta-annotations — that's why @RestController counts as a controller. The @ResponseBody carried by @RestController is likewise discovered by RequestMappingHandlerAdapter through the same merged-annotation lookup. You can compose your own stereotype the same way and scanning will pick it up.

code

java · 13 lines
java
// A custom composed stereotype — scanning discovers it like any @Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@RestController               // -> @Controller -> @Component, + @ResponseBody
@RequestMapping("/api/v1")   // shared base path
public @interface ApiV1Controller {}

@ApiV1Controller
class UserEndpoint {
    @GetMapping("/users/{id}")
    User get(@PathVariable Long id) { return service.find(id); }
    // Registered as a bean, mapped to GET /api/v1/users/{id}, returns JSON
}

go deeper

for a junior

Not expected to know the meta-annotation machinery.

for a middle

Should know @RestController = @Controller + @ResponseBody and that scanning registers it as a bean.

for a senior

Should explain transitive @Component matching, isHandler detection, and @AliasFor.

for a principal

Discusses MergedAnnotations internals, custom composed stereotypes, and separation of bean-scanning vs handler-mapping phases.

## Meta-annotation composition Spring stereotypes form a **hierarchy of composed annotations**: ``` @Component (root stereotype) ^ @Controller (@Component + "web controller" semantics) ^ @RestController (@Controller + @ResponseBody) ``` Concretely: ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Component // <-- @Controller IS a @Component public @interface Controller { @AliasFor(annotation = Component.class) String value() default ""; } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Controller // <-- @RestController IS a @Controller @ResponseBody // <-- ...and carries @ResponseBody public @interface RestController { @AliasFor(annotation = Controller.class) String value() default ""; } ``` Note `@AliasFor`: the `value` on `@Controller`/`@RestController` is aliased to `@Component`'s `value`, so a name you give a `@RestController("myBean")` flows through to the bean name. ## How component scanning finds it 1. `@ComponentScan` (implied by `@SpringBootApplication`) creates a `ClassPathScanningCandidateComponentProvider`. 2. By default it registers an **include filter**: `AnnotationTypeFilter(Component.class)`. 3. That filter matches **not just directly-annotated classes but any class whose annotations are (transitively) meta-annotated with `@Component`**. It uses `MergedAnnotations` / `AnnotationMetadata` to walk the annotation graph. 4. So a `@RestController` class → is `@Controller` → is `@Component` → matches → becomes a `BeanDefinition` → instantiated as a singleton bean. ## How it's recognized as a web handler Bean-ness alone doesn't make it handle requests. `RequestMappingHandlerMapping.isHandler()` returns true for a bean type that is annotated (again, meta-annotation-aware via `AnnotatedElementUtils`) with **`@Controller` OR `@RequestMapping`**. Because `@RestController` is meta-`@Controller`, it qualifies. The mapping then registers each `@RequestMapping`/`@GetMapping` method. ## How @ResponseBody is honored When a handler method returns, `RequestMappingHandlerAdapter` runs its `HandlerMethodReturnValueHandler`s. `RequestResponseBodyMethodProcessor.supportsReturnType()` checks for `@ResponseBody` on the **method or its declaring class**, using merged-annotation lookup — so the class-level `@ResponseBody` inherited from `@RestController` triggers body serialization for every method. ## Rolling your own stereotype Because matching is transitive, you can define a custom composed stereotype and scanning will pick it up: ```java @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @RestController @RequestMapping("/api") public @interface ApiController {} ``` A class annotated `@ApiController` is a bean, a controller, all-body, and base-pathed at `/api`. ## Gotchas - Meta-annotation traversal is why `AnnotatedElementUtils.findMergedAnnotation` (not plain `getAnnotation`) is used internally — a naive `Class.getAnnotation(Controller.class)` on a `@RestController` class returns **null** because it's meta-, not directly, present. - If you write a custom `@ComponentScan` with `useDefaultFilters = false`, you drop the `@Component` include filter and your controllers won't be found unless you re-add filters. - Component scanning only registers classes under the scanned base packages; a `@RestController` outside them is silently ignored.

  • Why does Class.getAnnotation(Controller.class) return null on a @RestController class, yet Spring still treats it as a controller?
    Because @Controller is a *meta*-annotation on @RestController, not directly present. java.lang.reflect's getAnnotation only sees directly-declared annotations. Spring uses AnnotatedElementUtils/MergedAnnotations, which walk the meta-annotation graph, so it finds @Controller transitively.
  • What does @AliasFor accomplish on @RestController's value attribute?
    It aliases the controller's value to @Component's value, so an explicit bean name given via @RestController("foo") propagates to the underlying @Component bean name instead of being ignored.

saying these in an interview costs you the question

  • Saying component scanning matches only classes *directly* annotated with @Component
  • Claiming @RestController's controller-ness requires an explicit @Controller in addition
  • Confusing bean registration (scanning) with handler registration (RequestMappingHandlerMapping) — they're separate steps

context