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, β¦) andString. ClassandClass<?>.- 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
RUNTIMEretention β reflection returnsnull, 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.