Advanced Java August 30 ,2025

Annotations in Java (Part-2)

 This is a continuation of Part-1 (Annotations in Java).
In Part-1, we learned how annotations are defined and controlled using meta-annotations like @Retention, @Target, @Inherited, etc.

Now, in Part-2, we’ll focus on:

  • How annotations are processed (compile-time & runtime).
  • Built-in annotation use cases.
  • Framework applications (Spring, JUnit, Hibernate).
  • Advanced topics like annotation processors.
  • Best practices & limitations.
     

5. Processing Annotations

Annotations themselves don’t do anything until they are processed. Java provides mechanisms to process them either at compile-time (e.g., using Annotation Processors) or at runtime (e.g., using Reflection API).

Using Reflection API to Read Annotations at Runtime

The Reflection API allows us to access annotations dynamically while the program is running.

Example: 

import java.lang.annotation.*;
import java.lang.reflect.*;

// Step 1: Define custom annotation
@Retention(RetentionPolicy.RUNTIME) // must be RUNTIME for reflection
@Target(ElementType.METHOD)
@interface MyAnnotation {
    String value();
    int priority() default 1; // default value
}

// Step 2: Use annotation
class DemoClass {
    @MyAnnotation(value = "Task1", priority = 2)
    public void taskOne() {
        System.out.println("Executing Task One...");
    }

    @MyAnnotation("Task2") // priority will take default = 1
    public void taskTwo() {
        System.out.println("Executing Task Two...");
    }
}

// Step 3: Process annotation using Reflection
public class AnnotationProcessor {
    public static void main(String[] args) {
        try {
            Class cls = DemoClass.class;

            // Iterate through methods
            for (Method method : cls.getDeclaredMethods()) {
                // Check if method has @MyAnnotation
                if (method.isAnnotationPresent(MyAnnotation.class)) {
                    MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);

                    System.out.println("Method: " + method.getName());
                    System.out.println("  Value: " + annotation.value());
                    System.out.println("  Priority: " + annotation.priority());
                    System.out.println();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Output:

Method: taskOne
  Value: Task1
  Priority: 2

Method: taskTwo
  Value: Task2
  Priority: 1

Accessing Annotation Values Programmatically

As seen above, once we retrieve an annotation using getAnnotation(), we can call its methods to fetch values.

  • annotation.value() → gets "Task1" or "Task2".
  • annotation.priority() → gets 2 or default 1.
Compile-Time vs Runtime Processing
AspectCompile-Time ProcessingRuntime Processing
WhenDuring code compilationDuring program execution
HowUsing Annotation Processors (e.g., javax.annotation.processing.Processor)Using Reflection API
Use CasesCode generation, validation, dependency injection frameworks (e.g., Lombok, Dagger, MapStruct)Logging, runtime validation, frameworks like Spring, JUnit
PerformanceNo runtime cost (errors detected early)Slower (adds runtime overhead)
Retention Policy NeededSOURCE or CLASSRUNTIME

6. Annotation Use Cases in Java

Annotations in Java provide metadata that influences how the compiler, JVM, or frameworks behave. They are widely used in compile-time validation, runtime processing, and frameworks.

1. Compile-Time Instructions

Annotations that guide the Java compiler during code compilation.

Examples:

  • @Override
    Ensures that a method correctly overrides a method from its superclass. If not, the compiler throws an error.
class Parent {
    void display() {}
}

class Child extends Parent {
    @Override
    void display() {   // ✅ Valid override
        System.out.println("Child display");
    }
}
  • @SuppressWarnings
    Tells the compiler to suppress specific warnings (like unchecked, deprecation).
@SuppressWarnings("unchecked")
void useList() {
    java.util.List list = new java.util.ArrayList(); // No warning
    list.add("Hello");
}

Purpose:

  • Code safety check
  • Removes unnecessary compiler warnings

2. Runtime Processing

Annotations that can be read at runtime (with Reflection API) to modify behavior dynamically.

Examples:

  • @Entity (Hibernate/JPA)
    Marks a class as a database entity.
@Entity
class User {
    @Id
    private int id;
    private String name;
}
  • @Test (JUnit)
    Identifies a test method that should be executed by the test runner.
import org.junit.Test;

public class MyTest {
    @Test
    public void checkAddition() {
        assert(2 + 3 == 5);
    }
}

Purpose:

  • Read metadata at runtime
  • Change object behavior dynamically

3. Framework Usage

Most frameworks heavily rely on annotations for configuration and dependency injection.

a) Spring Framework

  • @Autowired → Injects dependency automatically.
@Component
class Service {
    void serve() { System.out.println("Service running..."); }
}

class Client {
    @Autowired
    Service service;
}
  • @Component → Marks a class as a Spring-managed bean.

b) JUnit (Testing)

  • @Test → Marks methods as test cases.
  • @Before → Runs before each test method.
  • @After → Runs after each test method.
import org.junit.*;

public class ExampleTest {
    @Before
    public void setup() { System.out.println("Setup before test"); }

    @Test
    public void testMethod() { System.out.println("Test running"); }

    @After
    public void cleanup() { System.out.println("Cleanup after test"); }
}

c) Hibernate / JPA

  • @Entity → Maps class to database table.
  • @Table → Specifies table name.
  • @Column → Maps field to DB column.
import jakarta.persistence.*;

@Entity
@Table(name="users")
class User {
    @Id
    @GeneratedValue
    private int id;

    @Column(name="username")
    private String name;
}

Purpose:

  • Eliminates boilerplate XML configuration
  • Simplifies dependency injection, testing, ORM

7. Advanced Topics in Java Annotations

1. Annotation Processing Tool (APT) in Java

  • Definition:
    The Annotation Processing Tool (APT) is a tool provided by Java (before Java 8, now replaced by javac built-in support) that allows developers to process annotations at compile time.
  • Purpose:
    • Generate additional code, XML files, or documentation.
    • Validate annotation usage (e.g., ensuring methods annotated with @MyCustomAnnotation follow specific rules).

Example: Lombok and MapStruct libraries internally use annotation processing to generate boilerplate code.

2. javax.annotation.processing package

  • This package provides APIs to write annotation processors.
  • Key Interfaces/Classes:
    • Processor: Interface for custom processors.
    • AbstractProcessor: Base class to simplify writing processors.
    • ProcessingEnvironment: Provides utilities like messager (logging), filer (for file creation), etc.
    • RoundEnvironment: Represents the current round of annotation processing.
    • SupportedAnnotationTypes: Defines annotations the processor supports.
    • SupportedSourceVersion: Defines Java version support.

3. Abstract Syntax Tree (AST) based annotation processing

  • AST Definition: The Abstract Syntax Tree is the internal tree representation of source code created by the compiler.
  • Annotation processors can traverse AST nodes and analyze code structure.
  • This is useful for:
    • Code generation (e.g., generating getters/setters).
    • Static analysis (e.g., checking correct usage of annotations).

Example: Lombok modifies the AST to inject code like @Getter, @Setter.

4. How annotation processors are registered (META-INF/services)

  • Annotation processors are discovered using the Service Provider Interface (SPI).
  • Steps:
    1. Create a file in META-INF/services/ directory.
    2. File name → javax.annotation.processing.Processor.
    3. Inside the file → fully qualified class name of your processor.

Example:

META-INF/services/javax.annotation.processing.Processor

File content:

com.example.MyAnnotationProcessor

This way, the Java compiler automatically detects your processor during compilation.

5. Example: Creating an Annotation Processor

Step 1: Define a custom annotation

import java.lang.annotation.*;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
    String value();
}

Step 2: Create a processor

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.Set;

@SupportedAnnotationTypes("MyAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            String className = element.getSimpleName().toString();
            processingEnv.getMessager().printMessage(javax.tools.Diagnostic.Kind.NOTE,
                    "Found @MyAnnotation at " + className);
        }
        return true; // annotation claimed
    }
}

Step 3: Register processor

META-INF/services/javax.annotation.processing.Processor

com.example.MyAnnotationProcessor

Step 4: Usage Example

@MyAnnotation("Hello")
public class TestClass {
}

When you compile, the processor logs:

Note: Found @MyAnnotation at TestClass

8. Best Practices & Limitations of Annotations

Best Practices

  1. Use annotations for metadata, not business logic
    • Annotations should describe behavior/configuration (e.g., @Entity, @Autowired) instead of implementing core functionality.
  2. Prefer standard annotations over custom ones
    • Use built-in annotations (@Override, @Deprecated, @SuppressWarnings) before creating custom ones.
    • Helps with readability and consistency across projects.
  3. Use annotations for declarative configuration
    • Best suited for declarative tasks like dependency injection, validation, or mapping (@Column, @NotNull).
    • Avoid complex logic inside annotation processors.
  4. Balance annotations and external configuration
    • Small/medium projects: annotations improve simplicity and reduce XML/JSON config files.
    • Large projects: prefer configuration files for dynamic changes without recompilation.
  5. Document custom annotations properly
    • Always use @Documented so annotations appear in Javadoc.
    • Provide clear descriptions and usage examples.
  6. Follow naming conventions
    • Annotation names should be self-explanatory, e.g., @Transactional, @Cacheable, not vague names like @DoIt.
  7. Use retention policy wisely
    • RetentionPolicy.SOURCE → For compile-time checks only (e.g., Lombok).
    • RetentionPolicy.CLASS → For bytecode tools, not loaded at runtime.
    • RetentionPolicy.RUNTIME → For frameworks like Spring/Hibernate.

Limitations of Annotations

  1. Overuse reduces readability

    • Too many annotations clutter the code (annotation hell).
    • Example:
    @Entity
    @Table(name="users")
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class User { ... }
    

    → Can confuse new developers.

  2. Hard-coded metadata
    1. Annotation values are fixed at compile-time.
    2. Unlike config files, you cannot change them without recompiling.
  3. Hidden complexity
    1. Over-reliance on annotations can hide actual behavior, making debugging harder.
    2. Example: Spring’s @Transactional manages transactions behind the scenes — not obvious to newcomers.
  4. Framework lock-in
    1. Heavy use of framework-specific annotations ties code to that framework.
    2. Migrating from Spring to another framework becomes difficult.
  5. Limited expressiveness
    1. Annotations only store constant values (primitives, enums, Strings, arrays).
    2. Cannot directly store objects or dynamic logic.
  6. Runtime overhead (for reflection)
    1. Annotations with RetentionPolicy.RUNTIME require reflection to read, which adds runtime overhead.
  7. Learning curve for beginners
    1. Custom annotations + processors may confuse new developers who are unfamiliar with meta-programming concepts.

Conclusion

Annotations in Java are a powerful feature that bridge the gap between code and metadata, making programs more readable, maintainable, and framework-friendly. Introduced in Java 5, they have evolved into an essential tool for modern Java development.

  • Predefined annotations like @Override, @Deprecated, and @SuppressWarnings simplify everyday coding tasks.
  • Meta-annotations provide control over how custom annotations behave and where they can be applied.
  • Custom annotations allow developers to define domain-specific metadata, while reflection and annotation processing make it possible to act on them dynamically at compile-time or runtime.
  • Frameworks like Spring, Hibernate, and JUnit heavily rely on annotations for declarative programming, dependency injection, ORM mapping, and testing.

That said, annotations should be used wisely. Overuse can lead to cluttered code, reduced readability, and framework lock-in. A balance between annotations and external configuration files ensures flexibility and maintainability, especially in large-scale projects.

In short:
Annotations make Java development simpler, cleaner, and more expressive — but they are most effective when applied judiciously, with clear documentation and an understanding of their impact on performance and maintainability.

Next Blog- Java I/O (NIO)

Sanjiv
0

You must logged in to post comments.

Get In Touch

123 Street, New York, USA

+012 345 67890

techiefreak87@gmail.com

© Design & Developed by HW Infotech