W3docs

Java Custom Annotations

Define your own annotation types in Java, configure their retention and targets, and read them at runtime.

A custom annotation is an annotation type you declare yourself, rather than one supplied by the JDK (like @Override) or a framework (like @Test). The syntax looks like an interface, but the rules are stricter. Once declared, your annotation becomes a real type that you can attach to code, look up via reflection, and process at compile time.

This chapter is the practical guide to writing your own: the @interface keyword, which element types are allowed, how required and optional elements differ, and how a processor reads the values back at runtime. If you have not yet met annotations at all, start with Java annotations and the built-in annotations; to control where your annotation may appear and how long it lives, see meta-annotations.

The @interface declaration

An annotation type is declared with the @interface keyword:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited {
  String value();                                       // required element
  String level() default "INFO";                        // element with a default
  String[] tags() default {};                           // array element with default
}

This declares a new annotation type called Audited whose elements look like methods on an interface but behave as named values at use sites. Each "method" is an element.

Use it like this:

@Audited("UserService.login")                            // value omitted name → "value" element
public User login(String user, String password) { ... }

@Audited(value = "Service.save", level = "WARN", tags = {"db", "write"})
public void save(Entity e) { ... }

The value shortcut (@Audited("...") instead of @Audited(value = "...")) is available only when the element is literally named value, which is why so many annotations use exactly that name for their primary parameter.

What elements are allowed

The body of an @interface is a closed set of element declarations. Each element's return type must be one of:

  • A primitive (int, long, double, boolean, ...).
  • String.
  • Class or a parameterised Class<?>.
  • An enum type.
  • Another annotation type.
  • An array of any of the above.

Default values are written with default. The default must be a compile-time constant of the right type:

@interface RetryPolicy {
  int attempts() default 3;
  long delayMs() default 100;
  Class<? extends Exception>[] on() default {Exception.class};
  Level level() default Level.WARN;
  enum Level { DEBUG, INFO, WARN, ERROR }
}

What you can't declare in an annotation:

  • Methods that take parameters (the () is required but always empty).
  • Generic elements (<T> T value(); is illegal).
  • throws clauses.
  • Inheritance from another interface (annotations implicitly extend java.lang.annotation.Annotation).
  • Constructors.

You can nest types inside an annotation declaration — the Level enum above lives inside @RetryPolicy. That's a useful idiom: it keeps related options scoped to the annotation that uses them.

Required vs. optional elements

An element without default is required at use sites. The compiler fails if you forget it:

@interface Issue { String id(); }                       // required

@Issue                                                  // compile error: missing 'id'
public void brokenLogin() { }

@Issue(id = "JIRA-123")                                 // OK: 'id' supplied
public void fixedLogin() { }

A small piece of style: if there's a single obvious value, name the element value and make it required. If there are several knobs, name them and give sensible defaults so the common call stays short.

Marker annotations

An annotation with no elements at all is a marker:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ThreadSafe { }

Marker annotations don't carry data; their presence or absence is the entire signal. Reflection asks "does this class have @ThreadSafe?" with getAnnotation(ThreadSafe.class) != null or isAnnotationPresent(ThreadSafe.class).

Reading annotations at runtime

For a RUNTIME annotation, reflection exposes several methods on Class, Method, Field, Constructor, and Parameter (see reading annotations with reflection for the full surface):

  • isAnnotationPresent(Class) — quick yes/no.
  • getAnnotation(Class) — returns the annotation instance, or null.
  • getAnnotations() — returns all annotations on the element (declared + inherited via @Inherited).
  • getDeclaredAnnotations() — only those declared directly on the element, ignoring @Inherited.
  • getAnnotationsByType(Class) — handles the @Repeatable case correctly.

Reading is the same shape regardless of which target type you're working with:

Method m = ...;
if (m.isAnnotationPresent(Audited.class)) {
  Audited a = m.getAnnotation(Audited.class);
  log(a.value(), a.level(), a.tags());
}

The returned Audited is a JVM-generated proxy — the element methods (value(), level(), tags()) are real method calls on it.

Annotation equality, identity, and toString

Annotation values implement equals, hashCode, and toString as defined by java.lang.annotation.Annotation:

  • Two annotation instances are equal when they're of the same type and every element compares equal (with array deep-equality).
  • hashCode is derived from the element values in a defined way.
  • toString produces a stable, source-like rendering — useful for logging.

Reflection sometimes returns the same proxy for repeated lookups on the same element, and sometimes returns a fresh one. Use equals, never ==, when comparing annotation instances.

A worked example: define, attach, and reflect

The program declares two annotations (@Audited and @Retry), uses them on a class, and walks the methods with reflection — running each method either inside an auditing wrapper or with a retry loop. The annotations are pure metadata; the behaviour lives in the executor.

java— editable, runs on the server

What to take from the run:

  • greet carried only @Audited, so the executor printed an enter/exit pair around the method but did not retry. The same executor handled save by spotting @Retry on top of @Audited: the first invocation threw (saveCalls == 1), the helper logged the failure and looped, and the second attempt returned saved: data. The annotations themselves did nothing — the invoke helper supplied the behaviour.
  • unannotated ran through the same loop because the executor is uniform. isAnnotationPresent returned false for both annotations, so the helper neither logged nor retried; the method just executed once. That's the pattern for processors: examine annotations, default sensibly when they're absent, never special-case for "this is the annotated path."
  • Each element accessor (a.value(), r.attempts(), r.when()) returned the value written in source. Retry.when() came back as the enum constant ALWAYS because the call site used the default. Defaults are baked into the annotation proxy by the compiler; the caller can't tell whether a value was explicit or defaulted.
  • The toString of Audited printed a source-like form such as @...Audited(level="WARN", value="Service.save"). That's a property of every annotation proxy — useful for logging and for assertEquals in tests. (The order in which elements appear inside the parentheses is not guaranteed and varies between JDK versions, so don't assert on the exact string.)
  • The two annotations are entirely independent at the source level: one method carries both at once and reflection happily returned both. There is no inheritance hierarchy between annotation types; combining behaviours is achieved by stacking annotations on the same element, not by extending one annotation from another.

Where this stops working

A few common surprises:

  • Source retention can't be reflected. If you forget @Retention(RUNTIME), reflection silently returns null. The default is CLASS, not RUNTIME.
  • Targets must match. If @Target(METHOD) and you put the annotation on a class, the compiler refuses.
  • Element defaults must be compile-time constants. You can't default to new ArrayList<>(); you can default to {} for an array, an enum constant, a Class literal, or a primitive literal.
  • Annotations can't reference themselves cyclically. An element of type MyAnn inside @interface MyAnn is rejected.

The next chapter, annotation processing, shows the compile-time side — generating new source files in response to your custom annotations, instead of (or in addition to) reading them at runtime.

Practice

Practice
You declare `@Cached { int ttlSeconds(); }` and put it on a method. At runtime `m.getAnnotation(Cached.class)` returns `null` even though the source clearly has `@Cached(ttlSeconds = 60)`. What's the most likely cause?
You declare `@Cached { int ttlSeconds(); }` and put it on a method. At runtime `m.getAnnotation(Cached.class)` returns `null` even though the source clearly has `@Cached(ttlSeconds = 60)`. What's the most likely cause?
Was this page helpful?