Java Annotations: @interface, Retention, ElementType, and Runtime Processing
Annotations are metadata tags that attach information to code without changing its behavior. Java defines many built-in annotations (@Override, @Deprecated, @FunctionalInterface), and you can define your own with @interface. Meta-annotations control where annotations can be applied (ElementType) and whether they survive compilation (RetentionPolicy). Frameworks like Spring, JUnit, and Jackson use annotations extensively, understanding how to create and read them at runtime is essential for framework development.
Contents
Built-in Annotations
Java ships with several annotations that the compiler itself understands. @Override@Deprecated@SuppressWarnings@Retention@Target
// @Override — tells compiler this method overrides a superclass method
// Compilation error if no such method exists → catches typos
class Animal {
public String sound() { return "..."; }
}
class Dog extends Animal {
@Override
public String sound() { return "Woof"; } // compiler verifies this overrides
}
// @Deprecated — marks code as obsolete; compiler warns on use
@Deprecated(since = "2.0", forRemoval = true)
public void oldMethod() { /* use newMethod() instead */ }
// @SuppressWarnings — suppress specific compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) getRawList(); // no unchecked warning
@SuppressWarnings({"unchecked", "deprecation"}) // multiple warnings
// @FunctionalInterface — ensure interface has exactly one abstract method
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input);
// default methods allowed
default Transformer<T, R> andLog() { return input -> {
R result = transform(input);
System.out.println(input + " → " + result);
return result;
};}
}
// @SafeVarargs — suppress heap pollution warnings for generic varargs
@SafeVarargs
static <T> List<T> listOf(T... elements) {
return new ArrayList<>(Arrays.asList(elements));
}
Creating Custom Annotations
Custom annotations are declared with the @interfacejava.lang.annotation.Annotation@interfacedefaultvalueStringClass
import java.lang.annotation.*;
// @interface declares an annotation type
// Elements (methods) define annotation attributes
public @interface Route {
String path(); // required attribute (no default)
String method() default "GET"; // optional attribute with default
boolean auth() default false;
}
// Using the annotation
@Route(path = "/users", method = "GET")
public class UserController { }
@Route(path = "/admin", method = "POST", auth = true)
public class AdminController { }
// Annotation with array attribute
public @interface Roles {
String[] value(); // "value" is the magic name — can be used positionally
}
@Roles({"ADMIN", "MODERATOR"}) // with name
class AdminPanel { }
@Roles("USER") // single element shorthand when attribute is named "value"
class UserDashboard { }
// Annotation with Class and enum attributes
public @interface Config {
Class<?> handler();
LogLevel level() default LogLevel.INFO;
String[] tags() default {};
}
@Config(handler = DefaultHandler.class, level = LogLevel.DEBUG, tags = {"v2", "beta"})
class Service { }
Meta-annotations: Retention and Target
@RetentionRetentionPolicy.SOURCE@OverrideCLASS.classRUNTIME@TargetElementTypeTYPEMETHODFIELDPARAMETER@Target@Inherited@Documented
import java.lang.annotation.*;
// @Retention — how long the annotation survives
// SOURCE — discarded by compiler (only for source tools like @Override)
// CLASS — in .class file but not loaded by JVM (default; used by bytecode tools)
// RUNTIME — available via reflection at runtime (required for framework use)
// @Target — where the annotation can be applied
// TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE,
// ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE, RECORD_COMPONENT
@Retention(RetentionPolicy.RUNTIME) // readable at runtime
@Target({ElementType.METHOD, ElementType.TYPE}) // on classes and methods
public @interface Benchmark {
String description() default "";
int warmUpRuns() default 5;
}
// @Documented — include in Javadoc output
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {
String message() default "Field must not be null";
}
// @Inherited — subclass inherits annotation from superclass
// Only works on TYPE-targeted annotations
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Auditable { }
@Auditable
class BaseEntity { }
class User extends BaseEntity { }
// User.class.isAnnotationPresent(Auditable.class) → true (inherited)
// Practical full example
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Timed {
String metric() default ""; // metric name for monitoring
}
Reading Annotations at Runtime
The Java reflection API exposes annotations through AnnotatedElementClassMethodFieldConstructorisAnnotationPresent(Class)getAnnotation(Class)nullgetAnnotations()getDeclaredAnnotations()getParameterAnnotations()RetentionPolicy.RUNTIME
import java.lang.reflect.*;
// Reading class-level annotations
Class<?> cls = UserController.class;
if (cls.isAnnotationPresent(Route.class)) {
Route route = cls.getAnnotation(Route.class);
System.out.println("Path: " + route.path());
System.out.println("Method: " + route.method());
System.out.println("Auth: " + route.auth());
}
// Reading all annotations on a class
for (Annotation ann : cls.getAnnotations()) { // includes inherited
System.out.println(ann.annotationType().getSimpleName());
}
for (Annotation ann : cls.getDeclaredAnnotations()) { // only directly present
System.out.println(ann.annotationType().getSimpleName());
}
// Reading method-level annotations
for (Method method : cls.getDeclaredMethods()) {
if (method.isAnnotationPresent(Timed.class)) {
Timed timed = method.getAnnotation(Timed.class);
String metric = timed.metric().isEmpty() ? method.getName() : timed.metric();
System.out.println("Timing: " + metric);
}
}
// Reading parameter annotations
Method m = cls.getMethod("createUser", String.class, int.class);
Annotation[][] paramAnnotations = m.getParameterAnnotations();
for (int i = 0; i < paramAnnotations.length; i++) {
for (Annotation ann : paramAnnotations[i]) {
System.out.println("Param " + i + ": " + ann.annotationType().getSimpleName());
}
}
// Reading field annotations
for (Field field : cls.getDeclaredFields()) {
if (field.isAnnotationPresent(NotNull.class)) {
NotNull nn = field.getAnnotation(NotNull.class);
System.out.println(field.getName() + ": " + nn.message());
}
}
@Retention(RetentionPolicy.RUNTIME)@ComponentRepeatable Annotations
Prior to Java 8 you could not apply the same annotation type more than once to the same element: the workaround was to manually wrap multiple instances inside a container annotation. Java 8 made this transparent with @Repeatable@Repeatable(ContainerType.class)value()getAnnotationsByType()getAnnotation()
import java.lang.annotation.*;
// Before Java 8: couldn't apply same annotation twice — workaround was a container
// Java 8+: @Repeatable
// Step 1: Create the container annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedules {
Schedule[] value(); // container holds array of @Schedule
}
// Step 2: Mark the repeatable annotation with its container
@Repeatable(Schedules.class) // ← points to container
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Schedule {
String cron();
String zone() default "UTC";
}
// Step 3: Use multiple times on the same target
@Schedule(cron = "0 0 9 * * MON-FRI")
@Schedule(cron = "0 0 18 * * FRI", zone = "US/Eastern")
public void generateReport() { /* ... */ }
// Reading repeatable annotations at runtime
Method m = ReportService.class.getMethod("generateReport");
// getAnnotationsByType works for both single and repeated uses
Schedule[] schedules = m.getAnnotationsByType(Schedule.class);
for (Schedule s : schedules) {
System.out.println("Cron: " + s.cron() + " Zone: " + s.zone());
}
// Alternatively, get the container
Schedules container = m.getAnnotation(Schedules.class);
if (container != null) {
for (Schedule s : container.value()) {
System.out.println(s.cron());
}
}