Skip to main content

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));
}
}

Pro Tips

@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.