google-site-verification: google61fe8ba583a51912.html
Annotation Processing at Runtime in Java

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

  1. Flexibility: Process annotations based on runtime conditions
  2. No build-time dependencies: Doesn't require special compilation steps
  3. Dynamic behavior: Can change behavior based on runtime environment
  4. Framework integration: Essential for frameworks like Spring, Hibernate
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper