Annotations in Java Tutorial: Custom Metadata, Retention and Reflection Examples

Create a custom Java annotation from zero, inspect its values through reflection, and learn how retention, targets and defaults control the result.

KnowledgeGate Team

Exam prep & CS education

Updated 3 Nov 20265 min read
Annotations in Java Tutorial: Custom Metadata, Retention and Reflection Examples

An annotation looks like a comment beginning with @, but Java can give that text a precise structure for compilers, tools and programs to inspect. A missing retention policy can even make a reflection call return null. Java annotations have a precise structure, can be retained for reflection, and require deliberate choices about declarations, targets and defaults.

What annotations mean in Java

An annotation is metadata attached to a program element. // review this method is source text for a person. @Override asks the compiler to check an override, while @Audit("refund") can be read by reflection code.

The annotation itself does not automatically change behaviour. Its consumer supplies the effect: a compiler, build tool, framework, annotation processor or your reflection code.

Keep declaration and use separate:

  • @interface Audit { ... } declares a metadata type.

  • @Audit("refund") uses that type and attaches values to an allowed element.

Oracle's Java SE 21 JLS section 9.6 defines the declaration syntax and legal member types. Built-in annotations communicate code conditions; meta-annotations such as @Retention and @Target control custom annotations; reflection can read only metadata available at runtime. The Java Course places this model inside the wider language sequence.

Declare a custom @Audit annotation

Create one file named AuditDemo.java:

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface Audit {
    String value();
    int risk() default 1;
    String[] reviewers() default {};
}

@Audit(value = "settlement", risk = 3,
       reviewers = {"Asha", "Ravi"})
class PaymentService {
    @Audit("refund")
    public void refund(int orderId) {
        System.out.println("refund order=" + orderId);
    }
}

public class AuditDemo {
    public static void main(String[] args)
            throws NoSuchMethodException {
        Audit classAudit =
            PaymentService.class.getAnnotation(Audit.class);

        Method method = PaymentService.class
            .getDeclaredMethod("refund", int.class);
        Audit methodAudit = method.getAnnotation(Audit.class);

        System.out.println("class action=" + classAudit.value()
            + ", risk=" + classAudit.risk()
            + ", reviewers="
            + String.join("|", classAudit.reviewers()));
        System.out.println("method action=" + methodAudit.value()
            + ", risk=" + methodAudit.risk()
            + ", reviewers=" + methodAudit.reviewers().length);

        new PaymentService().refund(1042);
    }
}

value() is required, and its special name permits the shorthand @Audit("refund"). risk() defaults to 1, while reviewers() defaults to an empty array. These members are declarations without method bodies, not mutable object fields. A member without a default must be supplied at every use.

The class supplies all three values. The method supplies only value, so it receives both defaults. @Target({TYPE, METHOD}) permits these two placements.

Read annotations with reflection

Compile and run without a framework or external dependency:

Code
javac AuditDemo.java
java AuditDemo

The exact output is:

Code
class action=settlement, risk=3, reviewers=Asha|Ravi
method action=refund, risk=1, reviewers=0
refund order=1042

The class use explicitly supplies settlement, 3, Asha and Ravi. The method use explicitly supplies refund; 1 and the zero-length array come from defaults. The Java SE 21 AnnotatedElement documentation defines when getAnnotation can expose an annotation.

Notice the final call. new PaymentService().refund(1042) invokes the method normally. Reading @Audit did not invoke it.

Diagram tracing @Audit on the PaymentService class and refund method through getAnnotation to the printed class and method output lines.

Retention policies: SOURCE, CLASS and RUNTIME

The Java SE 21 annotation package documents these retention meanings:

Policy

After compilation

Visible through reflection

Typical consumer

SOURCE

Discarded by the compiler

No

Source or compiler tool

CLASS

Recorded in the class file, but not required to be retained by the VM at runtime

No

Bytecode tool

RUNTIME

Recorded in the class file and retained by the VM

Yes

Runtime framework or reflection

With RUNTIME, both calls return Audit objects. Change only the declaration to RetentionPolicy.CLASS, and PaymentService.class.getAnnotation(Audit.class) returns null. Calling classAudit.value() then throws NullPointerException.

Use RUNTIME when runtime inspection is intended, and null-check when absence is valid. If an annotation declaration has no @Retention, the default is CLASS, not RUNTIME.

Diagram comparing SOURCE, CLASS and RUNTIME retention along the compile-to-JVM path, with only RUNTIME reaching the getAnnotation call.

@Target and built-in annotations that prevent mistakes

ElementType.TYPE allows @Audit on PaymentService; ElementType.METHOD allows it on refund. This placement fails to compile because PARAMETER is absent:

java
public void refund(@Audit("id") int orderId) { }

Keep the annotation off parameters, or deliberately add ElementType.PARAMETER when the design needs parameter metadata.

@Override catches a different mistake. In a class extending Object, @Override public String toSting() fails because no superclass method has that signature. Correcting it to toString() makes the override valid. OOP for Teaching CS Exams: Classes and Inheritance develops that inheritance context.

@Deprecated marks an API from which callers should move away. @SuppressWarnings("unchecked") suppresses that named compiler warning within its scope. Prefer the narrowest justified suppression and fix the type-safety problem where possible. Neither annotation rewrites business logic.

Common annotation errors and their repairs

  • Reflection returns null: choose RUNTIME for this program, and check for null where metadata is optional.

  • Target mismatch: align @Target with actual placements.

  • Missing required member: bare @Audit fails. Write @Audit("refund") or declare a deliberate default.

Annotation values also have restrictions. This fails because level is not a constant variable:

java
class RefundService {
    static int level = 3;

    @Audit(value = "refund", risk = level)
    void refund() { }
}

This form is valid:

java
class RefundService {
    static final int LEVEL = 3;

    @Audit(value = "refund", risk = LEVEL)
    void refund() { }
}

Valid member types are primitives, String, Class, enum types, annotation types and one-dimensional arrays of those types. null is illegal. Most importantly, metadata is not execution: @Audit("refund") neither logs a refund nor blocks a risk level. Some consumer must read it and act.

How interviews and tests assess Java annotations

Interview and objective-test questions commonly probe declaration versus use, retention, legal targets, defaults, reflection output and null diagnosis. Practise by changing one element of the runnable program at a time and predicting both compilation and output.

  1. Change the method use to @Audit(value="refund", risk=4). Output becomes method action=refund, risk=4, reviewers=0.

  2. Remove the class reviewers argument. The joined string is empty, so the class line ends with reviewers=.

  3. Try @Audit("id") int orderId. The missing target is ElementType.PARAMETER.

  4. Change retention to CLASS and print "class annotation visible=" + (classAudit != null). Output is class annotation visible=false.

For the behaviour check, change only the method metadata from @Audit("refund") to @Audit("cancel"). The metadata line becomes method action=cancel, risk=1, reviewers=0, but the unchanged method call still prints refund order=1042.

Annotations in Java, the short version and next step

Declare metadata with @interface. Use value shorthand only when it reads clearly, choose @Target deliberately, select RUNTIME only for runtime inspection, read optional annotations safely, and remember that a consumer supplies behaviour.

Type AuditDemo.java, verify all three original lines, complete the four exercises, then explain why CLASS changes reflection but does not stop refund(1042) from running. Continue with DSA using Java once core syntax and objects are comfortable, use Coding & Skills to explore the broader catalogue, and revise String Handling in Java: Pool, Immutability and Interview Questions next.