Annotations provide metadata about your code that can be used at compile time or runtime. Java allows you to create custom annotations to add custom metadata to your programs.
1. Basic Annotation Syntax
Simple Marker Annotation
import java.lang.annotation.*;
// Marker annotation - no elements
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Development {
// No elements - acts as a marker
}
Annotation with Elements
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestCase {
// Required element
int id();
// Optional element with default value
String description() default "No description";
// Array element
String[] tags() default {};
// Enum element
Priority priority() default Priority.MEDIUM;
}
enum Priority {
LOW, MEDIUM, HIGH, CRITICAL
}
2. Meta-Annotations
Meta-annotations are annotations that apply to other annotations.
@Retention - Specifies Retention Policy
@Retention(RetentionPolicy.SOURCE) // Discarded during compilation @Retention(RetentionPolicy.CLASS) // Recorded in class file, not retained at runtime @Retention(RetentionPolicy.RUNTIME) // Available at runtime via reflection
@Target - Specifies Applicable Elements
@Target(ElementType.TYPE) // Class, interface, enum
@Target(ElementType.FIELD) // Field
@Target(ElementType.METHOD) // Method
@Target(ElementType.PARAMETER) // Parameter
@Target(ElementType.CONSTRUCTOR) // Constructor
@Target(ElementType.LOCAL_VARIABLE) // Local variable
@Target(ElementType.ANNOTATION_TYPE)// Annotation type
@Target(ElementType.PACKAGE) // Package
// Multiple targets
@Target({ElementType.TYPE, ElementType.METHOD})
Other Meta-Annotations
@Documented // Include in Javadoc @Inherited // Inherited by subclasses @Repeatable // Can be applied multiple times
3. Complete Annotation Examples
1. Validation Annotations
import java.lang.annotation.*;
// Field-level validation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NotNull {
String message() default "Field cannot be null";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Size {
int min() default 0;
int max() default Integer.MAX_VALUE;
String message() default "Size constraint violated";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Email {
String message() default "Invalid email format";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
double min();
double max();
String message() default "Value must be between {min} and {max}";
}
2. API Annotations
import java.lang.annotation.*;
// REST API annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RestController {
String path() default "";
String version() default "v1";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetMapping {
String value() default "";
String produces() default "application/json";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PostMapping {
String value();
String consumes() default "application/json";
String produces() default "application/json";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface PathVariable {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestBody {
boolean required() default true;
}
3. Testing Framework Annotations
import java.lang.annotation.*;
// Testing framework annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
String description() default "";
boolean enabled() default true;
Class<? extends Throwable> expected() default None.class;
long timeout() default 0L;
// Special class to represent no exception expected
class None extends Throwable {
private None() {}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BeforeEach {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterEach {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BeforeAll {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterAll {
}
4. Database Mapping Annotations
import java.lang.annotation.*;
// ORM-like annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Entity {
String tableName();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Id {
String name() default "";
boolean autoGenerated() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column {
String name();
boolean nullable() default true;
int length() default 255;
boolean unique() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface OneToMany {
String mappedBy();
CascadeType[] cascade() default {};
}
enum CascadeType {
ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH
}
4. Using Custom Annotations
Example 1: Validation Framework
// User class with validation annotations
public class User {
@NotNull(message = "Username cannot be null")
@Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
private String username;
@Email(message = "Invalid email address")
private String email;
@Range(min = 18, max = 120, message = "Age must be between 18 and 120")
private int age;
@NotNull
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
// Constructors
public User(String username, String email, int age, String password) {
this.username = username;
this.email = email;
this.age = age;
this.password = password;
}
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
Validation Processor
import java.lang.reflect.Field;
import java.util.*;
public class Validator {
public static List<String> validate(Object obj) {
List<String> errors = new ArrayList<>();
Class<?> clazz = obj.getClass();
// Get all fields including private ones
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true); // Allow access to private fields
try {
Object value = field.get(obj);
// Check @NotNull
if (field.isAnnotationPresent(NotNull.class)) {
NotNull notNull = field.getAnnotation(NotNull.class);
if (value == null) {
errors.add(notNull.message());
}
}
// Check @Size for Strings
if (field.isAnnotationPresent(Size.class) && value instanceof String) {
Size size = field.getAnnotation(Size.class);
String strValue = (String) value;
if (strValue.length() < size.min() || strValue.length() > size.max()) {
errors.add(size.message());
}
}
// Check @Email
if (field.isAnnotationPresent(Email.class) && value instanceof String) {
String email = (String) value;
if (!isValidEmail(email)) {
Email emailAnnotation = field.getAnnotation(Email.class);
errors.add(emailAnnotation.message());
}
}
// Check @Range for numbers
if (field.isAnnotationPresent(Range.class)) {
Range range = field.getAnnotation(Range.class);
if (value instanceof Number) {
double numValue = ((Number) value).doubleValue();
if (numValue < range.min() || numValue > range.max()) {
String message = range.message()
.replace("{min}", String.valueOf(range.min()))
.replace("{max}", String.valueOf(range.max()));
errors.add(message);
}
}
}
} catch (IllegalAccessException e) {
errors.add("Cannot access field: " + field.getName());
}
}
return errors;
}
private static boolean isValidEmail(String email) {
if (email == null) return false;
// Simple email validation
return email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
}
public static void main(String[] args) {
// Test validation
User validUser = new User("john_doe", "[email protected]", 25, "securepassword");
User invalidUser = new User("jo", "invalid-email", 15, "short");
System.out.println("Valid user errors: " + validate(validUser));
System.out.println("Invalid user errors: " + validate(invalidUser));
}
}
Example 2: Simple Testing Framework
// Test class using custom annotations
public class CalculatorTest {
private Calculator calculator;
@BeforeEach
public void setUp() {
calculator = new Calculator();
System.out.println("Test setup completed");
}
@Test(description = "Test addition operation")
public void testAddition() {
int result = calculator.add(5, 3);
assert result == 8 : "Addition test failed";
System.out.println("Addition test passed");
}
@Test(description = "Test subtraction operation", enabled = true)
public void testSubtraction() {
int result = calculator.subtract(10, 4);
assert result == 6 : "Subtraction test failed";
System.out.println("Subtraction test passed");
}
@Test(description = "Test division by zero", expected = ArithmeticException.class)
public void testDivisionByZero() {
calculator.divide(10, 0);
}
@Test(description = "Disabled test", enabled = false)
public void disabledTest() {
System.out.println("This test is disabled");
}
@AfterEach
public void tearDown() {
calculator = null;
System.out.println("Test cleanup completed");
}
}
// Calculator class being tested
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
public int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
}
public int multiply(int a, int b) {
return a * b;
}
}
Test Runner
import java.lang.reflect.Method;
import java.util.*;
public class TestRunner {
public static void runTests(Class<?> testClass) {
System.out.println("Running tests for: " + testClass.getSimpleName());
Object testInstance = null;
List<Method> beforeAllMethods = new ArrayList<>();
List<Method> afterAllMethods = new ArrayList<>();
List<Method> beforeEachMethods = new ArrayList<>();
List<Method> afterEachMethods = new ArrayList<>();
List<Method> testMethods = new ArrayList<>();
// Categorize methods
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(BeforeAll.class)) {
beforeAllMethods.add(method);
} else if (method.isAnnotationPresent(AfterAll.class)) {
afterAllMethods.add(method);
} else if (method.isAnnotationPresent(BeforeEach.class)) {
beforeEachMethods.add(method);
} else if (method.isAnnotationPresent(AfterEach.class)) {
afterEachMethods.add(method);
} else if (method.isAnnotationPresent(Test.class)) {
testMethods.add(method);
}
}
try {
testInstance = testClass.getDeclaredConstructor().newInstance();
// Execute @BeforeAll methods
executeMethods(testInstance, beforeAllMethods);
// Execute test methods
int passed = 0, failed = 0, skipped = 0;
for (Method testMethod : testMethods) {
Test testAnnotation = testMethod.getAnnotation(Test.class);
// Skip disabled tests
if (!testAnnotation.enabled()) {
System.out.println("Skipping disabled test: " + testMethod.getName());
skipped++;
continue;
}
System.out.println("Running test: " + testMethod.getName() +
" - " + testAnnotation.description());
try {
// Execute @BeforeEach methods
executeMethods(testInstance, beforeEachMethods);
// Execute test method
long startTime = System.currentTimeMillis();
testMethod.invoke(testInstance);
long endTime = System.currentTimeMillis();
// Check timeout
if (testAnnotation.timeout() > 0 &&
(endTime - startTime) > testAnnotation.timeout()) {
throw new RuntimeException("Test timed out");
}
System.out.println("✓ Test passed: " + testMethod.getName());
passed++;
} catch (Exception e) {
Throwable cause = e.getCause();
// Check if expected exception was thrown
if (testAnnotation.expected() != Test.None.class &&
cause != null && testAnnotation.expected().isInstance(cause)) {
System.out.println("✓ Test passed (expected exception): " + testMethod.getName());
passed++;
} else {
System.out.println("✗ Test failed: " + testMethod.getName());
System.out.println(" Reason: " + (cause != null ? cause.getMessage() : e.getMessage()));
failed++;
}
} finally {
// Execute @AfterEach methods
executeMethods(testInstance, afterEachMethods);
}
}
// Execute @AfterAll methods
executeMethods(testInstance, afterAllMethods);
// Print summary
System.out.println("\nTest Summary:");
System.out.println("Passed: " + passed + ", Failed: " + failed + ", Skipped: " + skipped);
} catch (Exception e) {
System.err.println("Error running tests: " + e.getMessage());
e.printStackTrace();
}
}
private static void executeMethods(Object instance, List<Method> methods) {
for (Method method : methods) {
try {
method.setAccessible(true);
method.invoke(instance);
} catch (Exception e) {
System.err.println("Error executing method " + method.getName() + ": " + e.getMessage());
}
}
}
public static void main(String[] args) {
runTests(CalculatorTest.class);
}
}
5. Advanced Annotation Features
Repeatable Annotations
import java.lang.annotation.*;
// Container annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Roles {
Role[] value();
}
// Repeatable annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Roles.class)
public @interface Role {
String value();
int level() default 1;
}
// Usage
@Role("ADMIN")
@Role("USER")
@Role("MODERATOR")
class SecureService {
// Class with multiple roles
}
Annotation Inheritance
import java.lang.annotation.*;
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {
String name();
String version() default "1.0";
}
@Service(name = "BaseService")
class BaseService {
// This annotation will be inherited
}
class DerivedService extends BaseService {
// Inherits @Service annotation from BaseService
}
Annotation Processors (Compile-time)
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.tools.*;
import java.util.*;
import java.io.*;
// Simple annotation processor for code generation
@SupportedAnnotationTypes("com.example.GenerateBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class BuilderProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : annotatedElements) {
if (element.getKind() == ElementKind.CLASS) {
generateBuilder((TypeElement) element);
}
}
}
return true;
}
private void generateBuilder(TypeElement classElement) {
String className = classElement.getSimpleName().toString();
String packageName = processingEnv.getElementUtils().getPackageOf(classElement).toString();
String builderClassName = className + "Builder";
try {
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(
packageName + "." + builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
// Generate builder class
out.println("package " + packageName + ";");
out.println();
out.println("public class " + builderClassName + " {");
out.println(" private " + className + " instance = new " + className + "();");
out.println();
out.println(" public " + builderClassName + "() {}");
out.println();
// Generate setter methods for each field
for (Element enclosed : classElement.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.FIELD) {
VariableElement field = (VariableElement) enclosed;
String fieldName = field.getSimpleName().toString();
String fieldType = field.asType().toString();
out.println(" public " + builderClassName + " set" +
capitalize(fieldName) + "(" + fieldType + " value) {");
out.println(" instance." + fieldName + " = value;");
out.println(" return this;");
out.println(" }");
out.println();
}
}
out.println(" public " + className + " build() {");
out.println(" return instance;");
out.println(" }");
out.println("}");
out.println();
out.println("// Helper method");
out.println("class Util {");
out.println(" private static String capitalize(String str) {");
out.println(" if (str == null || str.isEmpty()) return str;");
out.println(" return str.substring(0, 1).toUpperCase() + str.substring(1);");
out.println(" }");
out.println("}");
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Failed to generate builder: " + e.getMessage());
}
}
private String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}
// Annotation to trigger builder generation
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateBuilder {
}
6. Real-World Examples
Dependency Injection Framework
// DI Framework Annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
String name() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
String name() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Configuration {
}
// Usage
@Component
class UserService {
public String getUser(String id) {
return "User " + id;
}
}
@Component
class EmailService {
public void sendEmail(String to, String message) {
System.out.println("Sending email to " + to + ": " + message);
}
}
@Configuration
class AppConfig {
@Autowired
private UserService userService;
@Autowired
private EmailService emailService;
public void processUser(String userId) {
String user = userService.getUser(userId);
emailService.sendEmail(user, "Welcome!");
}
}
Simple DI Container
import java.lang.reflect.Field;
import java.util.*;
public class DIContainer {
private final Map<String, Object> beans = new HashMap<>();
public void registerBean(String name, Object bean) {
beans.put(name, bean);
}
public <T> T getBean(Class<T> clazz) {
return clazz.cast(beans.values().stream()
.filter(clazz::isInstance)
.findFirst()
.orElseThrow(() -> new RuntimeException("Bean not found: " + clazz.getName())));
}
public void autowire(Object instance) {
Class<?> clazz = instance.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
Autowired autowired = field.getAnnotation(Autowired.class);
String beanName = autowired.name();
Object bean;
if (!beanName.isEmpty()) {
bean = beans.get(beanName);
} else {
bean = getBean(field.getType());
}
if (bean != null) {
try {
field.setAccessible(true);
field.set(instance, bean);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to autowire field: " + field.getName(), e);
}
}
}
}
}
public void scanAndRegister(String basePackage) {
// Simplified scanning - in real implementation, use proper classpath scanning
System.out.println("Scanning package: " + basePackage);
}
}
7. Best Practices
1. Use Meaningful Names
// Good
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional {
boolean readOnly() default false;
int timeout() default 30;
}
// Avoid
public @interface Trans { // Too abbreviated
}
2. Provide Sensible Defaults
public @interface Config {
String name(); // Required - no default
boolean enabled() default true; // Optional with sensible default
int priority() default 0; // Optional with sensible default
}
3. Use Strong Types
// Good - type-safe
public @interface CacheConfig {
CacheType type();
int size();
}
enum CacheType {
MEMORY, DISK, DISTRIBUTED
}
// Avoid - stringly typed
public @interface CacheConfig {
String type(); // Error-prone
}
4. Document Your Annotations
/**
* Marks a method as requiring transaction management.
*
* <p>When applied to a method, the framework will automatically
* start a transaction before method execution and commit/rollback
* after execution based on the outcome.</p>
*
* @author John Doe
* @version 1.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Transactional {
/**
* Whether the transaction is read-only
* @return true if read-only, false otherwise
*/
boolean readOnly() default false;
/**
* Transaction timeout in seconds
* @return timeout value
*/
int timeout() default 30;
}
Summary
Custom annotations in Java provide powerful metadata capabilities:
- Basic Annotations - Simple marker annotations with optional elements
- Meta-Annotations - Control annotation behavior (@Retention, @Target, etc.)
- Validation Annotations - Create runtime validation frameworks
- Testing Annotations - Build custom testing frameworks
- Repeatable Annotations - Apply multiple annotations of the same type
- Annotation Processors - Generate code at compile time
Annotations enable frameworks like Spring, Hibernate, and JUnit to provide declarative programming models, making code more readable and maintainable.