Java Custom Annotation: Syntax, Retention, and Processing
java custom annotation: Learn how to create Java custom annotations, control their retention and targets, and process them at runtime with reflection.
Creating a java custom annotation is a direct way to attach metadata to code and have that metadata drive behavior. The declaration itself is minimal: an annotation is defined with the @interface keyword, and its visibility and usage scope are controlled by meta-annotations. The annotation does not contain logic. It only carries data. The behavior comes from code that reads the annotation at runtime or from a processor that runs during compilation.
public @interface LogExecution { }
This compiles and can be placed on a class or method, but it does nothing by itself. To make it useful, you need to control where it can appear, how long it survives, what data it carries, and which code interprets it.
Declaring a Custom Annotation with @interface
The @interface keyword declares an annotation type. The body of the annotation looks like an interface whose methods define the annotation's elements. Each element has a type and an optional default value.
public @interface LogExecution { String level() default "INFO"; boolean includeArgs() default false; }
Elements must have a type that is a primitive, String, Class, an enum, another annotation, or an array of any of these. You cannot use arbitrary objects as element types. This restriction exists because annotation values are resolved at compile time and stored in the class file.
An annotation type implicitly extends java.lang.annotation.Annotation and cannot explicitly extend anything else. You also cannot use generics in annotation element declarations.
Controlling Retention and Target
Two meta-annotations determine where an annotation is visible and where it can be applied.
@Retention controls how long the annotation is kept. The three policies are SOURCE, CLASS, and RUNTIME. The default is CLASS, which stores the annotation in the class file but does not make it visible to reflection.
| Retention policy | Visible at runtime | Typical use |
|---|---|---|
| SOURCE | No | Compile-time code generation |
| CLASS | No | Bytecode-level tools |
| RUNTIME | Yes | Reflection-based processing |
@Target restricts where the annotation can be applied. Common targets include METHOD, FIELD, TYPE, PARAMETER, and CONSTRUCTOR. If you omit @Target, the annotation can be applied to most declarations, which is rarely what you want.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface LogExecution { String level() default "INFO"; boolean includeArgs() default false; }
For runtime processing, RUNTIME retention is required. If you forget it, method.isAnnotationPresent() will always return false even though the annotation is present in the source code.
Processing Annotations at Runtime with Reflection
The most common way to make a custom annotation do something is to read it with reflection and branch on its values. This is typically done inside an interceptor, a proxy, or a central processing method.
public class LoggingSupport { public static void invokeLogged(Object target, Method method, Object... args) throws Exception { LogExecution annotation = method.getAnnotation(LogExecution.class); if (annotation == null) { method.invoke(target, args); return; } long start = System.nanoTime(); Object result = method.invoke(target, args); long elapsed = System.nanoTime() - start; System.out.printf("%s executed in %d ns%n", method.getName(), elapsed); if (annotation.includeArgs()) { System.out.printf("args: %s%n", java.util.Arrays.toString(args)); } } }
This approach works when the calling code routes method calls through the processing method. It does not intercept calls made directly on the object. For transparent interception, you typically combine the annotation with a dynamic proxy or a framework such as Spring AOP.
Common Use Cases: Validation and Cross-Cutting Behavior
Custom annotations are useful when the same behavior applies to many methods or fields and you want to keep the rule next to the declaration.
A field-level validation annotation is a typical example. You annotate fields with constraints, and a central validator reads those annotations before persisting an object.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface NotBlank { String message() default "Field must not be blank"; }
The validator scans the object's fields, checks each annotated field, and collects violations. This keeps validation rules attached to the data rather than scattered across service methods.
Other common uses include authorization checks, rate limiting, retry logic, and audit logging. In each case, the annotation marks the code that needs the behavior, and a framework or a wrapper applies it.
Runtime Cost and Performance Considerations
Reading annotations through reflection is not free. Each getAnnotation call performs a lookup in the class's annotation data. In a hot path, this can add measurable overhead, especially when the same method is invoked repeatedly.
The practical mitigation is to cache the result. If you process a method's annotations once and store the outcome, subsequent calls avoid repeated reflection lookups.
private final Map<Method, LogExecution> cache = new ConcurrentHashMap<>(); private LogExecution annotationFor(Method method) { return cache.computeIfAbsent(method, m -> m.getAnnotation(LogExecution.class)); }
If the annotation is only needed at compile time, prefer SOURCE retention and an annotation processor. That moves the work out of the runtime entirely and produces code or diagnostics during the build. This is the pattern used by libraries that generate code from annotations.
Common Pitfalls and Misconceptions
The most frequent mistake is declaring an annotation without RUNTIME retention and then expecting it to be visible to reflection. The default retention is CLASS, so the annotation exists in the class file but is invisible to getAnnotation.
Another misconception is that the annotation itself performs work. An annotation is passive metadata. Without a processor or a reflective reader, it has no effect. If you add an annotation and nothing happens, the reading code is missing, not the annotation.
A subtler issue is using annotations for configuration that changes frequently. Annotation values are fixed at compile time. If a value needs to change per deployment, a properties file or environment variable is the better choice.
Also, be careful with @Target(ElementType.PARAMETER) or TYPE_USE. These targets are easy to misapply, and a mismatch between the declared target and the actual usage site will cause a compile error that is not always obvious.
When a Custom Annotation Is the Wrong Choice
Annotations add indirection. Before introducing one, consider whether the behavior can be expressed with a plain method call or a small configuration object.
Use an annotation when the same behavior applies across many unrelated classes and you want to mark them declaratively. Avoid it when the behavior is only needed in one place, or when the annotation would hide control flow that the reader needs to see.
If the value changes at runtime, an annotation is the wrong tool. If the behavior is only relevant to a single class, a private method is simpler. If you need conditional behavior based on deployment environment, use configuration rather than annotation values.
Annotations are most valuable when they form part of a small, well-understood contract: mark the code, let a framework or a central processor act on the mark. When that contract is missing, the annotation becomes decoration with no effect.