0% completed
Annotations and Reflection
On This Page
Annotations Are Data in the Class File
Reflection: Reading Classes at Run Time
Meta-Annotations and Stereotypes
How Spring Finds Your Classes
What Reflection Costs
Key Takeaways
Practice Questions
How did Spring know that BookingController exists? No line of the booking service calls new BookingController(), and no list of controllers names it. Yet when the application started, a POST to /bookings reached the method book. Only two annotations told Spring what to do: @RestController on the class and @PostMapping("/bookings") on the method.
The answer has two parts. Annotations store information inside the compiled class, and reflection lets a running program read that information and act on it. The sections below declare annotations and read them with reflection. They then build a tiny scanner that finds and calls classes, and run Spring's own scanner for comparison.
Annotations Are Data in the Class File
An annotation is a label, with optional values, attached to a class, method, field, or parameter. It does nothing by itself until some other code reads it. You declare one with the keyword @interface. This one maps a method to a path, like a much smaller @GetMapping:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Get { String value(); }
The method value is an attribute, the value written in parentheses where the annotation is used: @Get("/events"). Two annotations on the declaration control where it may appear and how long it is kept:
- @Target lists the places it may appear. Here only methods, so putting @Get on a class is a compile error.
- @Retention sets how long the annotation is kept, with three choices in the table below.
| Retention | Kept until | Readable while the program runs? |
|---|---|---|
| SOURCE | Compilation; the compiler drops it | No |
| CLASS (the default) | The class file | No |
| RUNTIME | The running program | Yes, through reflection |
The Java documentation says that RUNTIME annotations are retained by the VM at run time, so they may be read reflectively. CLASS is the default, so an annotation declared without @Retention is invisible to a framework at run time. Every annotation that Spring reads at run time therefore has RUNTIME retention.
Reflection: Reading Classes at Run Time
Reflection is the part of the JDK that lets a running program inspect classes, methods, and annotations. It can also create objects and call methods that the source code never names. A handful of methods do most of the work:
| Call | What it does |
|---|---|
type.isAnnotationPresent(Managed.class) | Asks whether a class carries an annotation |
method.getAnnotation(Get.class) | Returns the annotation with its values, or null |
type.getDeclaredMethods() | Lists the methods a class declares |
type.getDeclaredConstructor().newInstance() | Creates an object through its no-argument constructor |
method.invoke(target) | Calls a method on an object |
The program below combines them into a tiny scanner. It looks at three classes, keeps those that carry @Managed directly or through another annotation, and creates one object of each. It reads @Get from their methods into a route table and answers two requests from that table. The third class, PriceFormatter, carries @Reviewed, which has CLASS retention.
It prints the following:
1. Reading class annotations EventController: managed true, annotations seen 1 BookingService: managed true, annotations seen 1 PriceFormatter: managed false, annotations seen 0 instances created: 2 2. Reading method annotations and their values route GET /events -> EventController.listEvents route GET /events/7/seats -> EventController.listSeats 3. Calling a method found by reflection GET /events/7/seats -> A12 taken, B12 free GET /bookings -> 404, no route
Part 1 shows retention at work. PriceFormatter has an annotation in its source, yet reflection sees 0 annotations on it, because @Reviewed was not kept for run time. Part 2 sorts the methods before reading them, because the documentation of getDeclaredMethods says they are not in any particular order. Part 3 calls listSeats through invoke, and the source never contains the call listSeats().
Notice that all the reading happens once, before any request arrives. Each request then needs only one lookup in the route table and one call.
Meta-Annotations and Stereotypes
A meta-annotation is an annotation that can be applied to another annotation, and Spring uses meta-annotations so that many annotations count as @Component. The Spring documentation calls @Component the generic stereotype for any Spring-managed component, and it calls @Repository, @Service, and @Controller specializations of @Component. A stereotype here means an annotation that marks the role a class plays.
The scanner above does the same with @WebController, which carries @Managed. To check Spring's own annotations, we read them with reflection in a test of the booking service:
RESULT @Component retention: RUNTIME RESULT @Service carries @Component: true RESULT @RestController carries @Component: false RESULT @RestController carries @Controller: true RESULT @Controller carries @Component: true
The third line is the interesting one. The annotation @RestController does not carry @Component directly. It carries @Controller, which carries @Component. Spring therefore searches the whole chain of meta-annotations, not only the first level. The tiny scanner checks one level, so it would miss an annotation two steps away.
Notice that the chain ends at @Component every time. A class becomes a candidate if any path of meta-annotations from its own annotations reaches @Component.
How Spring Finds Your Classes
The Spring documentation states that Spring can automatically detect stereotyped classes and register a bean definition for each one. The part that does this is a classpath scanner: it lists the class files below a package, reads their annotations, and keeps the classes that reach @Component.
Spring's scanner is a public class, ClassPathScanningCandidateComponentProvider, so we ran it directly on the package com.example.booking. It found exactly four classes:
RESULT candidate: com.example.booking.BookingApplication RESULT candidate: com.example.booking.HelloController RESULT candidate: com.example.booking.bookings.BookingController RESULT candidate: com.example.booking.config.StartupConfig
The records, the enums, and the Customer class were left out, because none of them carries a stereotype. BookingApplication was found through a chain of three steps. Its annotation @SpringBootApplication carries @SpringBootConfiguration, which carries @Configuration, which carries @Component. The CustomerRepository interface is also absent. It carries no stereotype, and the Spring documentation says this scanner keeps concrete classes and rejects interfaces. Spring Data finds repository interfaces through its own mechanism.
What Reflection Costs
Reflection makes these frameworks possible, but it moves work and errors to run time:
- Errors appear at startup, not at compile time. If a managed class has no no-argument constructor, getDeclaredConstructor fails when the program runs, not when it compiles.
- The flow is harder to follow. No source line calls listSeats, so searching the code for callers finds nothing.
- Reading has a price. Frameworks do the reading once at startup and keep the results, as the route table does, so requests do not repeat it.
Key Takeaways
- An annotation stores information in a class and does nothing until other code reads it.
- Only RUNTIME annotations are visible to reflection, and CLASS retention is the default.
- Reflection lets a program find annotated classes, create objects, read annotation values, and call methods by name.
- @Service, @Controller, and @RestController reach @Component through meta-annotations, and Spring follows the whole chain.
- Spring's classpath scanner keeps only classes whose annotations reach @Component, which is why a class without a stereotype never becomes a bean.
The scanner looked for classes on the classpath, and the build tool decides what that classpath contains. Build Tools: Maven and Gradle explains how a build resolves dependencies, compiles, tests, and packages the service.
Practice Questions
1. A developer declares an annotation @Audited without @Retention, puts it on BookingService, and a framework that checks isAnnotationPresent(Audited.class) never finds it. Why?
Without @Retention, the annotation has CLASS retention, so it is stored in the class file but not kept for the running program. Reflection therefore cannot see it. Adding @Retention(RetentionPolicy.RUNTIME) to the declaration of @Audited fixes the problem.
2. The tiny scanner checks one level of meta-annotations. A class is marked @ApiController, which carries @WebController, which carries @Managed. Is it found, and how would you change isManaged?
<details><summary>Show answer</summary>It is not found, because @Managed is two steps away from the class. The method isManaged must follow the chain. For each annotation on the class, it checks that annotation's own annotations, then their annotations, and so on. It must also remember which annotations it has already visited, because some annotations carry themselves: @Retention, for example, is marked with @Retention. Without that memory, the search would never end. Spring follows the whole chain, which is why @RestController still counts as @Component.
</details>3. A teammate creates an annotation @BookingApi that carries @RestController and puts it on a new controller. Will Spring find the controller during scanning?
<details><summary>Show answer</summary>Yes, as long as @BookingApi has RUNTIME retention and the class is in the package of BookingApplication or below it. The second condition is the scanning rule from What Spring Boot Is: scanning starts at the package of the application class. The chain @BookingApi, @RestController, @Controller, @Component reaches @Component, and Spring follows meta-annotation chains. Spring's own annotations already have RUNTIME retention, but a new custom annotation needs it declared explicitly.
</details>4. Why does a framework read all annotations at startup instead of reading them on each request?
<details><summary>Show answer</summary>Reading classes through reflection has a cost, and the annotations do not change while the program runs. Reading them once and keeping the result, such as a route table, makes each request a quick lookup. It also means a mistake such as a missing constructor stops the application at startup, before any customer sends a request.
</details>Reading Progress
0%
On This Page
Annotations Are Data in the Class File
Reflection: Reading Classes at Run Time
Meta-Annotations and Stereotypes
How Spring Finds Your Classes
What Reflection Costs
Key Takeaways
Practice Questions