An annotation attaches structured metadata to a declaration or type use; its effect depends on a compiler, tool, or runtime reader that interprets it.
Java annotations: retention and explicit processing
Java 8+. Use a JDK that supports this release.
Metadata is not execution
A marker named Audited does not create an audit log. The program must read that marker and perform whatever action the application requires. The sample reads a service label, prints it, and stops; it deliberately contains no implicit interception machinery.
Retention controls how long the metadata survives. SOURCE metadata is unavailable after compilation. CLASS metadata is stored in class files but is not exposed as a runtime annotation through ordinary reflection. RUNTIME lets this program read the annotation while it runs.
Target limits legal placements. A type-level service marker should not accidentally be placed on a local variable and then silently ignored by a reader that only checks classes.
Define missing metadata behavior
getAnnotation may return null. Decide whether absence is allowed, defaults to another policy, or rejects registration. The sample uses an explicit unlabelled state rather than dereferencing a missing annotation.
Annotation values describe metadata; they cannot carry arbitrary mutable service objects. Keep operational state outside the annotation. If a value names a handler class, instantiating and validating that handler is a separate step.
Annotation processing during compilation and reflection at runtime are different mechanisms. A compile-time generator can create source files without the runtime retaining its marker. This lesson uses the runtime case only.
Separate runtime inspection from compile-time processing
Runtime retention makes a marker available to reflective consumers. Source retention serves a compiler-time consumer and does not keep the annotation available for later reflection. An annotation does no work merely by existing; the consumer owns validation, generated output or execution behavior. Compile-time processing is a separate build operation that needs a declared processor and a test for accepted and rejected declarations.
Working program
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class ServiceLabels {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ServiceLabel { String value(); }
@ServiceLabel("shipment-index")
static final class ShipmentIndex { }
public static void main(String[] args) {
ServiceLabel label = ShipmentIndex.class.getAnnotation(ServiceLabel.class);
System.out.println(label == null ? "unlabelled" : label.value());
}
}Output
shipment-indexCost and design choices
One annotation lookup does not scan every object instance. Processing a registry of n service classes still requires work for those n classes and any generated registrations. Cache a validated registry rather than repeating inspection per request.
The marker does not establish authorization, a transaction, or a thread-safety guarantee. Such behavior requires tested code that owns those operations.
Common Mistakes
- Do not expect a marker to execute behavior by itself.
- Do not use CLASS retention when the reader requires runtime reflection.
- Do not assume annotation inheritance applies to every placement.
Connect the contracts
An annotation records metadata; compare it with the behavior enforced by interface contracts.
Apply this contract in Spring
Spring MVC request validation: reject invalid commands before mutation, Spring transactional methods: call paths and rollback assumptions. These lessons keep framework assembly separate from the Java contract.
