Custom Annotations
Define and process custom annotations with annotation processors and the Reflection API.
Overview
Key Points
- •@interface declares a custom annotation type
- •@Retention(RUNTIME) keeps the annotation available via reflection at runtime
- •@Retention(CLASS) retains in bytecode but not at runtime (used by tools)
- •@Retention(SOURCE) discarded after compilation (used by IDE tools)
- •@Target(ElementType.METHOD) restricts where the annotation can be applied
- •Annotation elements look like methods: String value() default "default";
- •An annotation with a single element named value() allows shorthand: @MyAnnotation("text")
- •Repeatable annotations use @Repeatable(Container.class) and a container annotation
- •Process annotations at runtime with reflection: method.getAnnotation(MyAnnotation.class)
Examples
Defining and reading a custom @Loggable annotation
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Loggable {
String level() default "INFO";
}
class UserService {
@Loggable
public void createUser(String name) {
System.out.println("Creating user: " + name);
}
@Loggable(level = "DEBUG")
public void deleteUser(String name) {
System.out.println("Deleting user: " + name);
}
}
public class Main {
public static void main(String[] args) throws Exception {
UserService service = new UserService();
for (Method method : UserService.class.getDeclaredMethods()) {
Loggable annotation = method.getAnnotation(Loggable.class);
if (annotation != null) {
System.out.println("[" + annotation.level() + "] Calling: " + method.getName());
method.invoke(service, "Alice");
}
}
}
}The annotation processor loops over methods, finds @Loggable, logs at the specified level, then invokes the method. This is exactly how frameworks like Spring AOP and JUnit work.
Validation annotation: @NotEmpty for fields
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotEmpty {
String message() default "Field must not be empty";
}
class UserForm {
@NotEmpty
private String username;
@NotEmpty(message = "Email is required")
private String email;
private String bio; // no validation
UserForm(String username, String email, String bio) {
this.username = username;
this.email = email;
this.bio = bio;
}
}
class Validator {
public static void validate(Object obj) throws Exception {
for (Field field : obj.getClass().getDeclaredFields()) {
NotEmpty notEmpty = field.getAnnotation(NotEmpty.class);
if (notEmpty != null) {
field.setAccessible(true);
Object value = field.get(obj);
if (value == null || value.toString().isEmpty()) {
throw new IllegalArgumentException(notEmpty.message() + " [" + field.getName() + "]");
}
}
}
System.out.println("Validation passed");
}
}
public class Main {
public static void main(String[] args) throws Exception {
Validator.validate(new UserForm("alice", "alice@example.com", null));
Validator.validate(new UserForm("", "alice@example.com", null)); // throws
}
}This is a simplified version of what Bean Validation (JSR 380) does with @NotNull, @NotEmpty, and @Size. Production validators also check constraint groups and collect all violations rather than stopping at the first one.
Repeatable annotations
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(Roles.class)
@interface Role {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Roles {
Role[] value();
}
class AdminService {
@Role("ADMIN")
@Role("SUPERUSER")
public void deleteEverything() {
System.out.println("Deleting...");
}
}
public class Main {
public static void main(String[] args) throws Exception {
var method = AdminService.class.getMethod("deleteEverything");
Role[] roles = method.getAnnotationsByType(Role.class);
for (Role r : roles) {
System.out.println("Requires role: " + r.value());
}
}
}Repeatable annotations require both the annotation itself and a container annotation. getAnnotationsByType unwraps the container transparently.
Common Mistakes
- Using @Retention(SOURCE) when you need runtime access. SOURCE annotations are discarded by the compiler. Use RUNTIME if you plan to read the annotation with reflection at runtime.
- Forgetting @Target and letting annotations be placed anywhere. Always restrict with @Target to catch misuse at compile time.
- Confusing annotation elements with fields. Annotation elements are declared like methods (String value();), not like fields. You cannot have logic or state in an annotation.
- Reading annotations on methods obtained from interfaces. Annotations are not inherited by default when overriding. Check AnnotatedElement.isAnnotationPresent() on the actual implementation class.
Note
For compile-time annotation processing (code generation, validation without runtime overhead), look into the javax.annotation.processing.Processor API. Libraries like Lombok, MapStruct, and Dagger use compile-time processors to generate boilerplate code from annotations.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4