Annotation
About
Annotations are one of Java’s most powerful language features introduced in Java 5. They enable a declarative programming style by allowing developers to embed metadata directly into the source code. Instead of writing complex logic or maintaining large configuration files (like XML), annotations let you describe behavior, attach rules, or delegate responsibilities to compilers, tools, and frameworks — all with just a few lines of markup.
Annotations are a form of metadata that provide data about the program, but they are not part of the program logic itself. In simpler terms, annotations are tags or labels that we can attach to code elements like classes, methods, variables, parameters, and packages.
These tags are used by:
The compiler (for compile-time instructions or warnings)
The runtime environment (for behavior modification via reflection)
Frameworks and tools (like Spring, Hibernate, JUnit, etc.)
Annotations do not change what the code does, but they influence how the code is processed.
Why Annotations Matter
Before annotations, configuration was often done using XML or verbose code. Annotations made it easier to:
Make code self-descriptive and declarative
Eliminate external configuration files
Reduce boilerplate code
Help frameworks like Spring, Hibernate, JUnit, etc., act automatically
Annotations enable cleaner, more maintainable, and readable code - especially in modern Java development.
Characteristics
Annotations are passive
They don’t perform any action themselves — they are just markers or metadata. The behavior is determined by the tools or code that interprets them (such as Spring, JUnit, Hibernate, or annotation processors).
They reduce boilerplate
Annotations can simplify logic that otherwise would require repetitive setup, reflection, or configuration files.
They improve readability
Code becomes more self-explanatory. For example:
@Entity
public class User { ... }
Immediately tells the reader: “This class represents a database entity.”
They support tools and frameworks
Tools like Lombok, frameworks like Spring, and libraries like Jackson rely heavily on annotations to work without needing verbose logic.
They work with reflection and processing
At runtime or compile time, annotations can be accessed using reflection APIs or annotation processors to dynamically alter behavior, generate code, validate constraints, or apply custom rules.
Processing Annotations
Annotations by themselves are just metadata — they don’t do anything until some processor or tool acts upon them. Processing annotations means writing code (or using a framework) that reads, interprets, and reacts to annotations, either during compile-time or runtime.
This is where annotations truly shine — enabling features like dependency injection, automatic configuration, validation, object mapping, and more.
Two Types of Annotation Processing
1. Compile-Time Annotation Processing
At compile time, you can use Annotation Processors (via the Java Compiler API or tools like Lombok and AutoValue) to:
Generate new source files
Validate annotated elements
Modify or augment code
Enforce coding constraints
These processors implement the javax.annotation.processing.Processor
interface (or extend AbstractProcessor
) and are registered via the META-INF
services mechanism.
Example Use Case:
A custom @Builder
annotation might generate a builder class at compile time.
Tools/Frameworks that use compile-time processing:
Lombok
Dagger
AutoValue
MapStruct
2. Runtime Annotation Processing
At runtime, Java provides Reflection APIs to inspect annotations and act on them dynamically.
This is common in frameworks like:
Spring (e.g.,
@Autowired
,@RequestMapping
)Hibernate (
@Entity
,@Id
)JUnit (
@Test
,@BeforeEach
)
We can use methods like .getAnnotations()
, .isAnnotationPresent()
, or .getAnnotation()
on classes, methods, fields, etc.
Example
Compile Time Annotation - @Override
@Override
class Animal {
void makeSound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark");
}
}
What's Happening Here ?
The annotation
@Override
is applied on themakeSound()
method inDog
class.This tells the compiler: “Ensure this method is overriding a method from the superclass.”
How It Works ?
1. At the Compiler Level
The Java compiler sees the
@Override
annotation and checks if the method:Exists in a superclass or interface.
Matches the method signature exactly.
If the method does not override a valid method (say we misspell it as
makeSoun()
), the compiler throws an error:method does not override or implement a method from a supertype
No special bytecode is added for
@Override
. It exists only at compile-time. It’s not stored in the.class
file.
2. At Runtime
@Override
does not exist at runtime because it has no@Retention(RetentionPolicy.RUNTIME)
.We cannot access it via reflection.
Method m = Dog.class.getMethod("makeSound"); Annotation[] annotations = m.getAnnotations(); // @Override will not appear
3. Why It’s Useful
Prevents bugs caused by mistyped method names.
Helps document intent clearly — readers know this method is overriding a parent one.
Improves tooling support (like showing warnings in IDEs).
Runtime Annotation - Custom
Create the Annotation
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
// Custom annotation available at runtime
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
String value() default "default-log";
}
Apply the Annotation
public class Service {
@MyLog("fetching-user-data")
public void getUser() {
System.out.println("Inside getUser()");
}
@MyLog
public void updateUser() {
System.out.println("Inside updateUser()");
}
}
Step 3: Read and Use the Annotation at Runtime (via Reflection)
import java.lang.reflect.Method;
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
Service service = new Service();
Method[] methods = service.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(MyLog.class)) {
MyLog annotation = method.getAnnotation(MyLog.class);
System.out.println("Calling method: " + method.getName() +
" | Log Tag: " + annotation.value());
method.invoke(service); // actually call the method
}
}
}
}
What’s Happening Internally ?
Annotation Declared
The annotation is marked with @Retention(RUNTIME)
so it is kept at runtime
Annotation Used
The annotation is added to methods like getUser()
and updateUser()
During Execution
Java Reflection reads the annotation from the method
Effect
We can dynamically control behavior based on the annotation (e.g., logging)
Why Runtime Annotations ?
Frameworks like Spring, JPA, Hibernate, Jackson, and JUnit use runtime annotations to add behavior dynamically.
Examples:
@Entity
in JPA@Test
in JUnit@RestController
in Spring Boot
These annotations do not change the logic themselves, but external tools and frameworks interpret them to apply behavior.
Where Are Annotations Used ?
Use Case
Purpose
Common Annotations
Example
Code Metadata
Marking information about code for documentation or tooling
@Override
, @Deprecated
, @SuppressWarnings
@Override
on a method to ensure proper overriding
Dependency Injection
Injecting objects into classes at runtime
@Autowired
, @Inject
, @Resource
Spring injects a bean using @Autowired
Configuration
Declaring how components should behave or be wired
@Component
, @Configuration
, @Bean
Spring defines a configuration class with @Configuration
Validation
Enforcing field constraints
@NotNull
, @Min
, @Email
, @Valid
@NotNull
ensures a field is not left empty
Persistence (ORM)
Mapping Java objects to database tables
@Entity
, @Id
, @Column
, @Table
JPA maps a class to a DB table with @Entity
Testing
Marking test methods or lifecycle hooks
@Test
, @BeforeEach
, @AfterAll
JUnit runs test methods annotated with @Test
Security
Defining access restrictions
@PreAuthorize
, @RolesAllowed
Spring Security uses @PreAuthorize("hasRole('ADMIN')")
Web (REST APIs)
Mapping HTTP routes to methods
@RequestMapping
, @GetMapping
, @PostMapping
Spring maps an endpoint using @GetMapping("/users")
Serialization
Controlling how objects are converted to/from JSON/XML
@JsonProperty
, @XmlElement
Jackson uses @JsonProperty
to rename JSON fields
Concurrency
Marking thread-safe or guarded regions
@GuardedBy
, @ThreadSafe
(from JSR-305)
IDEs can understand concurrency behavior
Custom Logic/Control
Triggering actions or logic in frameworks
Custom annotations (e.g., @Retry
, @Cacheable
)
Spring retries a method call using @Retryable
Compile-Time Processing
Generating code or validating usage during compilation
@Builder
, @AutoValue
, @Generated
Lombok generates code using @Builder
Logging / Monitoring
Intercepting methods for logging or metrics
Custom annotations + AOP (e.g., @LogExecutionTime
)
Spring AOP intercepts methods using annotation-based advice
Last updated