Annotation processing at runtime uses Java's Reflection API to inspect and process annotations during program execution. This is different from compile-time annotation processing.
1. Basic Runtime Annotation Processing
Defining Custom Annotations
// Marker annotation (no elements)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Serializable {
}
// Single-value annotation
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Author {
String name();
String date() default "unknown";
}
// Multi-value annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TestCase {
String id();
String description();
String[] tags() default {};
Class<? extends Exception> expectedException() default None.class;
class None extends Exception {}
}
// Annotation with enum
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Validation {
ValidationType type();
int min() default 0;
int max() default Integer.MAX_VALUE;
String pattern() default "";
enum ValidationType {
EMAIL, PHONE, CUSTOM, REQUIRED
}
}
Using Annotations
@Serializable
@Author(name = "John Doe", date = "2024-01-15")
public class User {
@Validation(type = Validation.ValidationType.REQUIRED)
private String username;
@Validation(type = Validation.ValidationType.EMAIL)
private String email;
@Validation(type = Validation.ValidationType.CUSTOM, pattern = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$")
private String password;
private int age;
public User(String username, String email, String password, int age) {
this.username = username;
this.email = email;
this.password = password;
this.age = age;
}
@TestCase(id = "TC001", description = "Test user validation", tags = {"smoke", "validation"})
public boolean validate() {
return username != null && !username.trim().isEmpty() &&
email != null && email.contains("@");
}
@TestCase(id = "TC002", description = "Test password strength", expectedException = Exception.class)
public void changePassword(String newPassword) {
if (newPassword.length() < 8) {
throw new IllegalArgumentException("Password too short");
}
this.password = newPassword;
}
// Getters and setters
public String getUsername() { return username; }
public String getEmail() { return email; }
public String getPassword() { return password; }
public int getAge() { return age; }
}
2. Basic Reflection-based Annotation Processing
public class BasicAnnotationProcessor {
public static void processClassAnnotations(Class<?> clazz) {
System.out.println("Processing class: " + clazz.getName());
// Check if class has specific annotation
if (clazz.isAnnotationPresent(Serializable.class)) {
System.out.println(" - Class is marked as Serializable");
}
// Get Author annotation and process it
Author author = clazz.getAnnotation(Author.class);
if (author != null) {
System.out.println(" - Author: " + author.name());
System.out.println(" - Date: " + author.date());
}
// Get all annotations
Annotation[] annotations = clazz.getAnnotations();
System.out.println(" - Total annotations: " + annotations.length);
}
public static void processFieldAnnotations(Class<?> clazz) {
System.out.println("\nProcessing fields of: " + clazz.getName());
for (Field field : clazz.getDeclaredFields()) {
System.out.println(" Field: " + field.getName() + " (" + field.getType().getSimpleName() + ")");
// Check for Validation annotation
Validation validation = field.getAnnotation(Validation.class);
if (validation != null) {
System.out.println(" - Validation type: " + validation.type());
System.out.println(" - Min: " + validation.min());
System.out.println(" - Max: " + validation.max());
if (!validation.pattern().isEmpty()) {
System.out.println(" - Pattern: " + validation.pattern());
}
}
}
}
public static void processMethodAnnotations(Class<?> clazz) {
System.out.println("\nProcessing methods of: " + clazz.getName());
for (Method method : clazz.getDeclaredMethods()) {
System.out.println(" Method: " + method.getName());
// Process TestCase annotations
TestCase testCase = method.getAnnotation(TestCase.class);
if (testCase != null) {
System.out.println(" - Test ID: " + testCase.id());
System.out.println(" - Description: " + testCase.description());
System.out.println(" - Expected Exception: " + testCase.expectedException().getSimpleName());
System.out.println(" - Tags: " + String.join(", ", testCase.tags()));
}
// Get all parameter annotations
Annotation[][] paramAnnotations = method.getParameterAnnotations();
for (int i = 0; i < paramAnnotations.length; i++) {
for (Annotation annotation : paramAnnotations[i]) {
System.out.println(" - Parameter " + i + " annotation: " + annotation.annotationType().getSimpleName());
}
}
}
}
public static void main(String[] args) {
processClassAnnotations(User.class);
processFieldAnnotations(User.class);
processMethodAnnotations(User.class);
}
}
3. Advanced Runtime Annotation Processing
Validation Framework using Annotations
public class ValidationProcessor {
public static List<String> validate(Object obj) {
List<String> errors = new ArrayList<>();
Class<?> clazz = obj.getClass();
// Validate fields
for (Field field : clazz.getDeclaredFields()) {
Validation validation = field.getAnnotation(Validation.class);
if (validation != null) {
try {
field.setAccessible(true);
Object value = field.get(obj);
validateField(field.getName(), value, validation, errors);
} catch (IllegalAccessException e) {
errors.add("Cannot access field: " + field.getName());
}
}
}
// Validate methods with constraints
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Validation.class)) {
validateMethod(obj, method, errors);
}
}
return errors;
}
private static void validateField(String fieldName, Object value, Validation validation, List<String> errors) {
Validation.ValidationType type = validation.type();
switch (type) {
case REQUIRED:
if (value == null || value.toString().trim().isEmpty()) {
errors.add(fieldName + " is required");
}
break;
case EMAIL:
if (value != null && !isValidEmail(value.toString())) {
errors.add(fieldName + " must be a valid email address");
}
break;
case CUSTOM:
if (value != null && !validation.pattern().isEmpty()) {
if (!value.toString().matches(validation.pattern())) {
errors.add(fieldName + " does not match required pattern");
}
}
break;
}
// Check numeric bounds
if (value instanceof Number) {
double num = ((Number) value).doubleValue();
if (num < validation.min()) {
errors.add(fieldName + " must be at least " + validation.min());
}
if (num > validation.max()) {
errors.add(fieldName + " must be at most " + validation.max());
}
}
}
private static void validateMethod(Object obj, Method method, List<String> errors) {
try {
method.setAccessible(true);
Object result = method.invoke(obj);
if (result instanceof Boolean && !(Boolean) result) {
errors.add("Validation method " + method.getName() + " failed");
}
} catch (Exception e) {
errors.add("Error executing validation method " + method.getName() + ": " + e.getMessage());
}
}
private static boolean isValidEmail(String email) {
return email != null && email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
}
public static void main(String[] args) {
User user = new User("", "invalid-email", "weak", 15);
List<String> errors = validate(user);
System.out.println("Validation errors:");
errors.forEach(System.out::println);
}
}
Test Framework using Annotations
public class TestRunner {
public static void runTests(Class<?> testClass) {
System.out.println("Running tests for: " + testClass.getName());
Object testInstance;
try {
testInstance = testClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
System.out.println("Cannot instantiate test class: " + e.getMessage());
return;
}
int testsRun = 0;
int testsPassed = 0;
int testsFailed = 0;
for (Method method : testClass.getDeclaredMethods()) {
if (method.isAnnotationPresent(TestCase.class)) {
testsRun++;
TestCase testCase = method.getAnnotation(TestCase.class);
System.out.println("\nRunning test: " + testCase.id() + " - " + testCase.description());
System.out.println("Tags: " + String.join(", ", testCase.tags()));
try {
method.setAccessible(true);
method.invoke(testInstance);
// If we expected an exception but none was thrown
if (!testCase.expectedException().equals(TestCase.None.class)) {
System.out.println("❌ FAILED: Expected exception " +
testCase.expectedException().getSimpleName() +
" but none was thrown");
testsFailed++;
} else {
System.out.println("✅ PASSED");
testsPassed++;
}
} catch (Exception e) {
Throwable actualException = e.getCause() != null ? e.getCause() : e;
// Check if this is the expected exception
if (testCase.expectedException().isInstance(actualException)) {
System.out.println("✅ PASSED: Expected exception occurred - " +
actualException.getClass().getSimpleName());
testsPassed++;
} else {
System.out.println("❌ FAILED: " + actualException.getMessage());
actualException.printStackTrace();
testsFailed++;
}
}
}
}
System.out.println("\n=== Test Summary ===");
System.out.println("Tests Run: " + testsRun);
System.out.println("Tests Passed: " + testsPassed);
System.out.println("Tests Failed: " + testsFailed);
}
public static void main(String[] args) {
runTests(User.class);
}
}
4. Dependency Injection Framework using Annotations
// Dependency Injection Annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
String name() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {
String value() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Repository {
}
// Example components
@Service("userService")
class UserService {
public User findUser(String username) {
return new User(username, username + "@example.com", "password", 25);
}
}
@Repository
class UserRepository {
public void save(User user) {
System.out.println("Saving user: " + user.getUsername());
}
}
@Component
class UserController {
@Inject
private UserService userService;
@Inject
private UserRepository userRepository;
public void createUser(String username) {
User user = userService.findUser(username);
userRepository.save(user);
System.out.println("User created: " + username);
}
// Getter for testing
public UserService getUserService() {
return userService;
}
}
Dependency Injection Container
public class DIContainer {
private final Map<String, Object> beans = new HashMap<>();
private final Map<Class<?>, Object> typedBeans = new HashMap<>();
public void registerBean(String name, Object bean) {
beans.put(name, bean);
typedBeans.put(bean.getClass(), bean);
// Also register by all interfaces
for (Class<?> iface : bean.getClass().getInterfaces()) {
typedBeans.put(iface, bean);
}
}
public void scanAndRegister(String basePackage) throws Exception {
// In a real implementation, you would scan the classpath
// For this example, we'll manually register our components
registerBean("userService", new UserService());
registerBean("userRepository", new UserRepository());
}
public <T> T getBean(Class<T> type) {
return type.cast(typedBeans.get(type));
}
public Object getBean(String name) {
return beans.get(name);
}
public void injectDependencies(Object target) throws IllegalAccessException {
Class<?> clazz = target.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
field.setAccessible(true);
Object dependency = getBean(field.getType());
if (dependency != null) {
field.set(target, dependency);
} else {
throw new RuntimeException("No bean found for type: " + field.getType());
}
}
}
}
public <T> T createAndInject(Class<T> type) throws Exception {
T instance = type.getDeclaredConstructor().newInstance();
injectDependencies(instance);
return instance;
}
public static void main(String[] args) throws Exception {
DIContainer container = new DIContainer();
container.scanAndRegister("com.example");
UserController controller = container.createAndInject(UserController.class);
controller.createUser("john_doe");
// Verify injection worked
System.out.println("Injection verified: " + (controller.getUserService() != null));
}
}
5. JSON Serialization using Annotations
// JSON Serialization Annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
String name() default "";
boolean ignore() default false;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JsonSerializable {
}
// Example class with JSON annotations
@JsonSerializable
class Product {
@JsonField(name = "product_id")
private String id;
@JsonField(name = "product_name")
private String name;
@JsonField(ignore = true) // This field will be ignored in JSON
private String internalCode;
private double price; // No annotation - will use field name
public Product(String id, String name, String internalCode, double price) {
this.id = id;
this.name = name;
this.internalCode = internalCode;
this.price = price;
}
// Getters
public String getId() { return id; }
public String getName() { return name; }
public String getInternalCode() { return internalCode; }
public double getPrice() { return price; }
}
JSON Serialization Processor
public class JsonSerializer {
public static String toJson(Object obj) throws IllegalAccessException {
if (!obj.getClass().isAnnotationPresent(JsonSerializable.class)) {
throw new RuntimeException("Class is not annotated with @JsonSerializable");
}
StringBuilder json = new StringBuilder();
json.append("{");
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
boolean firstField = true;
for (Field field : fields) {
JsonField jsonField = field.getAnnotation(JsonField.class);
// Skip ignored fields
if (jsonField != null && jsonField.ignore()) {
continue;
}
field.setAccessible(true);
Object value = field.get(obj);
if (!firstField) {
json.append(",");
}
firstField = false;
// Determine field name for JSON
String fieldName = field.getName();
if (jsonField != null && !jsonField.name().isEmpty()) {
fieldName = jsonField.name();
}
json.append("\"").append(fieldName).append("\":");
appendValue(json, value);
}
json.append("}");
return json.toString();
}
private static void appendValue(StringBuilder json, Object value) {
if (value == null) {
json.append("null");
} else if (value instanceof String) {
json.append("\"").append(escapeJsonString(value.toString())).append("\"");
} else if (value instanceof Number || value instanceof Boolean) {
json.append(value);
} else if (value.getClass().isArray()) {
json.append("[");
Object[] array = (Object[]) value;
for (int i = 0; i < array.length; i++) {
if (i > 0) json.append(",");
appendValue(json, array[i]);
}
json.append("]");
} else {
// For other objects, try to serialize them recursively
try {
if (value.getClass().isAnnotationPresent(JsonSerializable.class)) {
json.append(toJson(value));
} else {
json.append("\"").append(escapeJsonString(value.toString())).append("\"");
}
} catch (Exception e) {
json.append("\"").append(escapeJsonString(value.toString())).append("\"");
}
}
}
private static String escapeJsonString(String str) {
return str.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
public static void main(String[] args) throws IllegalAccessException {
Product product = new Product("123", "Laptop", "INT-789", 999.99);
String json = toJson(product);
System.out.println("JSON: " + json);
}
}
6. Advanced Annotation Processing with Caching
public class AnnotationCache {
private static final Map<Class<?>, List<Field>> VALIDATION_FIELDS_CACHE = new ConcurrentHashMap<>();
private static final Map<Class<?>, List<Method>> TEST_METHODS_CACHE = new ConcurrentHashMap<>();
private static final Map<Class<?>, Boolean> JSON_SERIALIZABLE_CACHE = new ConcurrentHashMap<>();
public static List<Field> getValidationFields(Class<?> clazz) {
return VALIDATION_FIELDS_CACHE.computeIfAbsent(clazz, k ->
Arrays.stream(k.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(Validation.class))
.collect(Collectors.toList())
);
}
public static List<Method> getTestMethods(Class<?> clazz) {
return TEST_METHODS_CACHE.computeIfAbsent(clazz, k ->
Arrays.stream(k.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(TestCase.class))
.collect(Collectors.toList())
);
}
public static boolean isJsonSerializable(Class<?> clazz) {
return JSON_SERIALIZABLE_CACHE.computeIfAbsent(clazz,
k -> k.isAnnotationPresent(JsonSerializable.class)
);
}
public static void clearCache() {
VALIDATION_FIELDS_CACHE.clear();
TEST_METHODS_CACHE.clear();
JSON_SERIALIZABLE_CACHE.clear();
}
}
// Usage example
public class CachedAnnotationProcessor {
public static List<String> validateWithCache(Object obj) {
List<String> errors = new ArrayList<>();
Class<?> clazz = obj.getClass();
for (Field field : AnnotationCache.getValidationFields(clazz)) {
try {
field.setAccessible(true);
Object value = field.get(obj);
Validation validation = field.getAnnotation(Validation.class);
// Perform validation...
} catch (IllegalAccessException e) {
errors.add("Cannot access field: " + field.getName());
}
}
return errors;
}
}
7. Performance Considerations and Best Practices
public class AnnotationPerformanceTips {
// 1. Cache reflection results
private static final Map<Class<?>, Annotation> CLASS_ANNOTATION_CACHE = new ConcurrentHashMap<>();
public static <A extends Annotation> A getCachedAnnotation(Class<?> clazz, Class<A> annotationClass) {
return (A) CLASS_ANNOTATION_CACHE.computeIfAbsent(clazz,
k -> k.getAnnotation(annotationClass)
);
}
// 2. Use setAccessible(true) sparingly and reuse
public static void processFieldsEfficiently(Object obj, Consumer<Field> processor) {
Class<?> clazz = obj.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
boolean accessible = field.canAccess(obj);
if (!accessible) {
field.setAccessible(true);
}
try {
processor.accept(field);
} catch (Exception e) {
// Handle exception
} finally {
if (!accessible) {
field.setAccessible(false);
}
}
}
}
// 3. Batch annotation processing
public static void processAllAnnotations(Class<?> clazz) {
// Process class annotations
processClassAnnotations(clazz);
// Process field annotations
Arrays.stream(clazz.getDeclaredFields())
.forEach(field -> processFieldAnnotations(field));
// Process method annotations
Arrays.stream(clazz.getDeclaredMethods())
.forEach(method -> processMethodAnnotations(method));
}
private static void processClassAnnotations(Class<?> clazz) {
// Process class-level annotations
}
private static void processFieldAnnotations(Field field) {
// Process field-level annotations
}
private static void processMethodAnnotations(Method method) {
// Process method-level annotations
}
}
Key Benefits of Runtime Annotation Processing
- Flexibility: Process annotations based on runtime conditions
- No build-time dependencies: Doesn't require special compilation steps
- Dynamic behavior: Can change behavior based on runtime environment
- Framework integration: Essential for frameworks like Spring, Hibernate
- Configuration: External configuration through annotations
Common Use Cases
- Validation frameworks (Bean Validation)
- Dependency injection (Spring, Guice)
- Testing frameworks (JUnit, TestNG)
- Serialization frameworks (Jackson, Gson)
- Web frameworks (JAX-RS, Spring MVC)
- ORM frameworks (Hibernate, JPA)
Runtime annotation processing is powerful but should be used judiciously due to reflection overhead. Always consider caching and performance optimizations in production code.