12.1 Annotations
What Is an Annotation?
An annotation is metadata attached to source code — classes, methods, fields, parameters, or packages. Annotations do not directly affect program execution, but they communicate information to the compiler, frameworks, or tools for processing at compile-time or runtime.
They look like comments, but unlike // comments, annotations are machine-readable and can trigger real behavior (like Spring's @Autowired injecting dependencies, or @Override telling the compiler to verify the override).
1. Built-in Java Annotations
Standard Annotations
// @Override — tells the compiler this method must override a superclass method
public class Dog extends Animal {
@Override
public void speak() { // compile error if 'speak' doesn't exist in Animal
System.out.println("Woof!");
}
}
// @Deprecated — marks an API as outdated; compiler warns when it's used
class OldApi {
@Deprecated
public void oldMethod() {
System.out.println("Use newMethod() instead.");
}
public void newMethod() {
System.out.println("New and improved!");
}
}
// @SuppressWarnings — suppresses specific compiler warnings
@SuppressWarnings("unchecked")
public void rawTypeMethod() {
List list = new ArrayList(); // raw type — normally a warning
list.add("item");
}
// @FunctionalInterface — ensures the interface has exactly one abstract method
@FunctionalInterface
interface Transformer {
String transform(String s);
// Adding a second abstract method would cause a compile error
}
// @SafeVarargs — suppresses unchecked warnings for varargs generics
@SafeVarargs
public static <T> List<T> combine(List<T>... lists) {
List<T> result = new ArrayList<>();
for (List<T> list : lists) result.addAll(list);
return result;
}
2. Meta-Annotations — Annotating Annotations
Meta-annotations configure how custom annotations behave:
import java.lang.annotation.*;
// @Retention: when the annotation is available
// SOURCE → only in source code (discarded by compiler)
// CLASS → stored in .class file but not at runtime (default)
// RUNTIME → available at runtime via reflection
// @Target: where the annotation can be applied
// ElementType.TYPE → class, interface, enum
// ElementType.METHOD → method
// ElementType.FIELD → field
// ElementType.PARAMETER → method parameter
// ElementType.CONSTRUCTOR → constructor
// ElementType.LOCAL_VARIABLE → local variable
// ElementType.ANNOTATION_TYPE → another annotation
// @Documented — include in Javadoc
// @Inherited — subclasses inherit the annotation
// @Repeatable — allow multiple uses on the same element
3. Creating Custom Annotations
import java.lang.annotation.*;
// Custom annotation for marking test methods with expected exceptions
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExpectException {
Class<? extends Exception> value();
String message() default ""; // optional element with default
}
// Custom annotation for API documentation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ApiController {
String version() default "v1";
String description() default "";
boolean deprecated() default false;
}
// Custom annotation for validation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotBlank {
String message() default "Field must not be blank";
}
// Usage
@ApiController(version = "v2", description = "User management API")
public class UserController {
@NotBlank(message = "Username is required")
private String username;
@ExpectException(value = IllegalArgumentException.class, message = "Should throw on null")
public void testNullInput() {
// test code
}
}
4. Reading Annotations with Reflection
The true power of runtime annotations is revealed through reflection— the ability to inspect and use annotation data at runtime.
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
// --- Define annotations ---
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Column {
String name() default "";
boolean nullable() default true;
int maxLength() default 255;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table {
String name();
}
// --- Use annotations ---
@Table(name = "users")
class User {
@Column(name = "user_id", nullable = false)
private Long id;
@Column(name = "user_name", nullable = false, maxLength = 50)
private String name;
@Column(name = "email_addr", maxLength = 100)
private String email;
private String notMapped; // no @Column — will be skipped
}
// --- Process annotations at runtime ---
public class AnnotationProcessor {
public static void generateDdl(Class<?> clazz) {
// Read class-level annotation
Table table = clazz.getAnnotation(Table.class);
if (table == null) {
System.out.println("No @Table annotation found on " + clazz.getSimpleName());
return;
}
StringBuilder ddl = new StringBuilder("CREATE TABLE " + table.name() + " (\n");
// Read field-level annotations
for (Field field : clazz.getDeclaredFields()) {
Column col = field.getAnnotation(Column.class);
if (col == null) continue; // skip unannotated fields
String colName = col.name().isEmpty() ? field.getName() : col.name();
String sqlType = getSqlType(field.getType(), col.maxLength());
String nullable = col.nullable() ? "" : " NOT NULL";
ddl.append(" ").append(colName)
.append(" ").append(sqlType)
.append(nullable).append(",\n");
}
// Remove trailing comma and close
ddl.setLength(ddl.length() - 2);
ddl.append("\n);");
System.out.println(ddl);
}
private static String getSqlType(Class<?> type, int maxLength) {
if (type == Long.class || type == long.class) return "BIGINT";
if (type == Integer.class || type == int.class) return "INT";
if (type == String.class) return "VARCHAR(" + maxLength + ")";
if (type == Boolean.class || type == boolean.class) return "BOOLEAN";
return "TEXT";
}
public static void main(String[] args) {
generateDdl(User.class);
}
}
Output:
CREATE TABLE users (
user_id BIGINT NOT NULL,
user_name VARCHAR(50) NOT NULL,
email_addr VARCHAR(100)
);
5. Annotation-Based Validation
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotNull { String message() default "must not be null"; }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Min {
int value();
String message() default "must be >= {value}";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Length {
int max();
String message() default "length must be <= {max}";
}
class SignupForm {
@NotNull
@Length(max = 20)
private String username;
@NotNull
@Length(max = 100)
private String email;
@Min(value = 0)
private int age;
public SignupForm(String username, String email, int age) {
this.username = username;
this.email = email;
this.age = age;
}
}
public class Validator {
public static List<String> validate(Object obj) throws IllegalAccessException {
List<String> errors = new ArrayList<>();
for (Field field : obj.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(obj);
if (field.isAnnotationPresent(NotNull.class) && value == null) {
errors.add(field.getName() + ": "
+ field.getAnnotation(NotNull.class).message());
}
if (field.isAnnotationPresent(Length.class) && value instanceof String s) {
int max = field.getAnnotation(Length.class).max();
if (s.length() > max) {
errors.add(field.getName() + ": length " + s.length() + " exceeds max " + max);
}
}
if (field.isAnnotationPresent(Min.class) && value instanceof Integer i) {
int min = field.getAnnotation(Min.class).value();
if (i < min) {
errors.add(field.getName() + ": value " + i + " is less than min " + min);
}
}
}
return errors;
}
public static void main(String[] args) throws Exception {
SignupForm valid = new SignupForm("alice", "alice@example.com", 25);
SignupForm invalid = new SignupForm(null,
"this-email-is-way-too-long-and-exceeds-100-characters@example.com.invalid.example.example.example",
-1);
System.out.println("Valid form errors: " + validate(valid));
System.out.println("Invalid form errors: " + validate(invalid));
}
}
6. Spring Framework Preview
Annotations are central to Spring Framework:
// @Component: Spring manages this class as a bean
@Component
public class EmailService {
// @Autowired: inject a dependency automatically
@Autowired
private SmtpClient smtpClient;
// @Value: inject a value from application.properties
@Value("${mail.from}")
private String fromAddress;
public void sendEmail(String to, String subject, String body) {
smtpClient.send(fromAddress, to, subject, body);
}
}
// @RestController + @RequestMapping: define a REST API endpoint
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody @Valid CreateUserRequest req) {
return ResponseEntity.status(201).body(userService.create(req));
}
}
@Retention matters: If you forget @Retention(RetentionPolicy.RUNTIME), your annotation will be stripped from the .class file and reflection won't find it— a common source of confusion.
Annotation processing at compile-time: Libraries like Lombok, MapStruct, and Dagger use annotation processors that run during compilation (javac) to generate source code — not reflection. This is faster than runtime reflection and has zero overhead at runtime.
Spring AOP: Many Spring annotations (@Transactional, @Cacheable, @Async) are implemented using AOP (Aspect-Oriented Programming)— the annotation is a marker, and Spring's proxy mechanism intercepts method calls to the annotated method and adds behavior (transaction management, caching, async execution). This is why @Transactional only works on public methods called from outside the class.