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

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 the makeSound() method in Dog 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 ?

Stage
What Happens

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