Writing Custom Annotations in Java

A custom annotation is a kind of typed metadata you attach to your own code. The JDK comes with a handful (@Override, @Deprecated), but frameworks like Spring, JPA and JUnit define hundreds. Writing your own is a two-step affair: declare the annotation, then read it at runtime (reflection) or build time (annotation processor).

The syntax β€” @interface

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)          // available via reflection
@Target(ElementType.METHOD)                   // only on methods
public @interface Audited {
    String action() default "";
    String[] roles() default {};
    int   priority() default 0;
}

Applying it

public class UserService {
    @Audited(action = "delete", roles = {"admin"})
    public void deleteUser(long id) { ... }

    @Audited                                         // all defaults
    public User findUser(long id) { ... }
}

Allowed attribute types

  • Primitive types (int, boolean, …) and String.
  • Class and Class<?>.
  • Enum types.
  • Other annotations.
  • Arrays of any of the above.

No generics, no arbitrary objects, no List β€” arrays only.

The shorthand value

public @interface Json {
    String value();
}

@Json("payload")                    // equivalent to @Json(value = "payload")
private String data;

If the only attribute is named value, callers can skip the value = prefix.

Reading annotations at runtime

for (Method m : UserService.class.getDeclaredMethods()) {
    Audited a = m.getAnnotation(Audited.class);
    if (a != null) {
        System.out.printf("%s -> action=%s, roles=%s%n",
            m.getName(), a.action(), Arrays.toString(a.roles()));
    }
}

Requires @Retention(RetentionPolicy.RUNTIME). With CLASS or SOURCE retention, reflection can't see the annotation.

Restricting where it applies β€” @Target

@Target({ElementType.METHOD, ElementType.FIELD})

Multiple targets separated by commas. Common values: TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, TYPE_USE, LOCAL_VARIABLE.

Making it repeatable (Java 8+)

@Repeatable(Roles.class)
public @interface Role { String value(); }

public @interface Roles { Role[] value(); }

@Role("admin") @Role("auditor")
public void deleteUser(long id) { ... }

Where custom annotations shine

  • Aspect-oriented cross-cutting concerns β€” logging, security, transactions (Spring @Transactional).
  • Declarative mapping β€” JPA @Entity, Jackson @JsonProperty.
  • Testing hooks β€” JUnit @Test, @ParameterizedTest.
  • Build-time checks β€” NullAway, ErrorProne, your own annotation processors.

Common mistakes

  • Missing RUNTIME retention β€” reflection returns null, no errors.
  • No @Target β€” the annotation can be put on anything, including places where it makes no sense.
  • Annotation-driven framework without documentation β€” callers can't tell what the annotation does. Write Javadoc.
  • Implementing behaviour inside the annotation β€” annotations are data. Put the logic in a processor or an aspect.

Related

Pillar: Java annotations. Siblings: meta-annotations, @Override, @Deprecated.