Definition
Creating a custom annotation:
- @interface keyword declares it.
- Elements are declared like methods:
String action() default ""; - @Target restricts where it can be used (METHOD, FIELD, TYPE).
- @Retention(RUNTIME) makes it available for reflection.
- Default values make elements optional.
Creating a custom stamp for bank audit forms. You define what information the stamp carries (action, timestamp), where it can be applied (on methods or classes), and whether it survives into the final document (retention).
Understanding the Concepts
How do you define a custom annotation?
You define a custom annotation using the @interface keyword. Each element inside the annotation looks like a method declaration but acts as an attribute. You control where the annotation can be used with @Target and how long it survives with @Retention.
package com.bank.audit;
import java.lang.annotation.*;
@Target(ElementType.METHOD) // can only be on methods
@Retention(RetentionPolicy.RUNTIME) // available at runtime via reflection
@Documented // included in javadoc
public @interface Audited {
String action() default ""; // optional element with default
String description() default "";
boolean sensitive() default false; // marks PII-handling operations
}
The default keyword makes an element optional. If you omit it, the caller must always supply a value.
How do you use it?
Apply the annotation on any method and supply values for the elements. Elements with defaults can be omitted.
package com.bank.service;
import java.math.BigDecimal;
import com.bank.audit.Audited;
public class TransferService {
@Audited(action = "TRANSFER", description = "Debit and credit between accounts")
public void transfer(Account from, Account to, BigDecimal amount) {
from.debit(amount);
to.credit(amount);
}
@Audited(action = "CLOSE_ACCOUNT", sensitive = true)
public void closeAccount(Account account) {
account.setStatus("CLOSED");
}
}
How do you read it at runtime?
Use the Java Reflection API to discover annotations on classes, methods, or fields. This is how Spring, JPA, and custom interceptors process annotations at runtime.
package com.bank.audit;
import java.lang.reflect.Method;
import java.math.BigDecimal;
public class AuditLogger {
public static void logAuditedMethods(Class<?> clazz) {
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Audited.class)) {
Audited audit = method.getAnnotation(Audited.class);
System.out.println("Method: " + method.getName());
System.out.println(" Action: " + audit.action());
System.out.println(" Desc: " + audit.description());
System.out.println(" Sensitive: " + audit.sensitive());
}
}
}
public static void main(String[] args) {
logAuditedMethods(TransferService.class);
}
}
Method: transfer
Action: TRANSFER
Desc: Debit and credit between accounts
Sensitive: false
Method: closeAccount
Action: CLOSE_ACCOUNT
Desc:
Sensitive: true
What is the value() shortcut for single-element annotations?
If you name the element value, callers can omit the element name when only that one element is supplied.
public @interface RoleRequired {
String value(); // named "value" for the shortcut
}
// Usage: no need to write value = "ADMIN"
@RoleRequired("ADMIN")
public void approveLoan(Loan loan) { }
This convention is used throughout Java: @SuppressWarnings("unchecked") works because the element is named value.
What element types are allowed?
Annotation elements can only return: primitives, String, Class, enums, other annotations, or arrays of these. You cannot use arbitrary objects like BigDecimal or List.
public @interface Config {
int maxRetries() default 3; // primitive
String environment() default "PRODUCTION"; // String
Class<?> handler() default Object.class; // Class
AccountType type() default AccountType.SAVINGS; // enum
Audited[] audits() default {}; // array of annotations
}
What are common mistakes when creating custom annotations?
The most common mistake is using the wrong retention. If you set SOURCE retention but try to read the annotation via reflection at runtime, it will not be there.
// WRONG: SOURCE retention – annotation stripped before runtime
@Retention(RetentionPolicy.SOURCE)
public @interface TransactionLog { String action(); }
// At runtime: method.getAnnotation(TransactionLog.class) returns null!
// CORRECT: RUNTIME retention – annotation survives to runtime
@Retention(RetentionPolicy.RUNTIME)
public @interface TransactionLog { String action(); }
Another mistake is forgetting @Target, which allows the annotation on any element. This compiles but leads to confusing usage when developers place the annotation on fields, parameters, or classes where it makes no sense.
What should a senior engineer know?
- Spring AOP uses custom annotations for cross-cutting concerns:
@Cacheable,@Retry,@RateLimited. Spring intercepts calls to annotated methods via CGLIB proxies. - Lombok processes annotations at compile time to generate code (getters, builders) without reflection overhead at runtime.
- AnnotationProcessor (JSR 269) lets you generate source files during compilation. Used by MapStruct, Dagger, and AutoValue.
- @Repeatable (Java 8+) allows multiple of the same annotation:
@Role("ADMIN") @Role("AUDITOR"). It requires a container annotation that holds an array of the repeated type. - @Inherited on a type annotation means subclasses inherit it. Without @Inherited, only the class that directly bears the annotation has it.
Visual Overview
1. Define: @interface Audited { String action(); }
2. Meta: @Target(METHOD) @Retention(RUNTIME)
3. Apply: @Audited(action = "TRANSFER")
4. Read: method.getAnnotation(Audited.class).action()
Element types: primitive, String, Class, enum, annotation, arrays of these
Processing: runtime (reflection) or compile-time (annotation processor)
Key Takeaway
Create a custom annotation with @interface, add elements as method-like declarations with optional defaults, and apply @Target and @Retention meta-annotations. Use RUNTIME retention when frameworks or your own code need to read the annotation via reflection.
One sentence to remember: @interface defines it, @Target restricts where, @Retention controls when it survives, and reflection reads it at runtime.
---