Annotations are an essential part of modern Java development, providing metadata that guides compiler behavior or configures framework properties. While built-in annotations like @Override or @Deprecated are useful, Java's real power lies in your ability to design custom annotations. When combined with reflection, custom annotations enable you to build highly decoupled, modular, and dynamic systems. They act as declarative markers, allowing you to attach metadata directly to your classes, fields, or methods without affecting the core execution logic. In this guide, we'll cover how to declare a custom annotation and read its properties at runtime.

Illustration of a custom stamp or blueprint applied to class methods
Real-World Analogy: The Factory Sticker

To understand how custom annotations work, think of a physical manufacturing plant that processes shipping packages:

  • Designing the Tag (@interface): You create a custom sticker layout labeled "Priority Rank". This template has a space where you can write a numeric priority level.
  • Labeling the Box (@MyAnnotation(value = 10)): You place this sticker onto a specific shipping container and fill in the priority value.
  • Scanning the Label (Reflection): The warehouse coordinator scans the shipping container at runtime, reads the priority level, and routes the package accordingly. The container itself doesn't change; the tag simply provides instruction to the shipping system.

1. Declaring the Custom Annotation

Declaring a custom annotation requires using the @interface keyword. We must also apply meta-annotations (annotations that annotate other annotations) to define how our custom tag behaves:

  • @Retention(RetentionPolicy.RUNTIME): This is the most crucial meta-annotation. It instructs the compiler to preserve the annotation in the compiled .class file so it can be read by the JVM at runtime. If omitted, the annotation will be discarded after compilation.
  • @Target(ElementType.METHOD): This meta-annotation restricts where the annotation can be placed. In this case, we limit it to methods. If you attempt to place it on a class or field, the compiler will raise an error.
Inside the interface, we define a property, such as int value(), which allows us to pass a value when using the annotation.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME) // Available to JVM at runtime
@Target(ElementType.METHOD)         // Can only be placed on methods
@interface MyAnnotation {
    int value(); // Attribute of the annotation
}

2. Applying the Annotation

Once declared, using your custom annotation is straightforward. You place it above the targeted method and pass the required parameter values inside parentheses, just like you would with framework configurations:

class Hello {
    @MyAnnotation(value = 10)
    public void sayHello() {
        System.out.println("hello annotation");
    }
}

3. Accessing the Value at Runtime (Reflection)

An annotation is just passive metadata until something reads it. To make our custom annotation functional, we use Java's Reflection API. We load the class, retrieve the method representation, check if our annotation is present, and extract the configured values dynamically:

import java.lang.reflect.Method;
 
public class CustomAnnotation {
    public static void main(String args[]) throws Exception {
        Hello h = new Hello();
        
        // Retrieve the method metadata object
        Method m = h.getClass().getMethod("sayHello");
 
        // Inspect the method for our custom annotation
        MyAnnotation manno = m.getAnnotation(MyAnnotation.class);
        
        // Print the extracted value
        System.out.println("value is: " + manno.value()); // Prints 10
    }
}

Conclusion & Practical Applications

Custom annotations are the backbone of popular Java frameworks like Spring and Hibernate. They allow developers to replace verbose XML configuration files with clean, inline markers. By using reflection to scan for these annotations at runtime, frameworks can automate complex behaviors like dependency injection, database mappings, and transaction routing. Mastering custom annotations is a key step toward writing advanced, professional-grade Java code.