javaadvanced

Custom Annotations and Reflection

Create and process custom annotations with reflection: field validation, auto-logging, and metadata.

java
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

// Define annotations
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface NotEmpty {
    String message() default "Field must not be empty";
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Range {
    int min() default 0;
    int max() default Integer.MAX_VALUE;
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface LogExecutionTime {}

// Validator using reflection
public class AnnotationProcessor {

    public static List<String> validate(Object obj) {
        List<String> errors = new ArrayList<>();
        Class<?> clazz = obj.getClass();

        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);

            try {
                Object value = field.get(obj);

                // @NotEmpty check
                if (field.isAnnotationPresent(NotEmpty.class)) {
                    NotEmpty ann = field.getAnnotation(NotEmpty.class);
                    if (value == null || (value instanceof String s && s.isBlank())) {
                        errors.add(field.getName() + ": " + ann.message());
                    }
                }

                // @Range check
                if (field.isAnnotationPresent(Range.class)) {
                    Range ann = field.getAnnotation(Range.class);
                    if (value instanceof Integer v && (v < ann.min() || v > ann.max())) {
                        errors.add(field.getName() + ": must be between " + ann.min() + " and " + ann.max());
                    }
                }
            } catch (IllegalAccessException e) {
                errors.add("Cannot access: " + field.getName());
            }
        }
        return errors;
    }

    // Method execution timer via proxy
    @SuppressWarnings("unchecked")
    public static <T> T createTimedProxy(T target, Class<T> iface) {
        return (T) Proxy.newProxyInstance(
            iface.getClassLoader(),
            new Class<?>[]{iface},
            (proxy, method, args) -> {
                Method realMethod = target.getClass().getMethod(
                    method.getName(), method.getParameterTypes());

                if (realMethod.isAnnotationPresent(LogExecutionTime.class)) {
                    long start = System.nanoTime();
                    Object result = method.invoke(target, args);
                    long ms = (System.nanoTime() - start) / 1_000_000;
                    System.out.printf("%s() took %d ms%n", method.getName(), ms);
                    return result;
                }
                return method.invoke(target, args);
            });
    }

    // Test it
    static class UserForm {
        @NotEmpty String name = "";
        @NotEmpty(message = "Email required") String email = null;
        @Range(min = 1, max = 150) int age = 200;
    }

    public static void main(String[] args) {
        List<String> errors = validate(new UserForm());
        errors.forEach(System.out::println);
        // name: Field must not be empty
        // email: Email required
        // age: must be between 1 and 150
    }
}

Use Cases

  • Custom validation frameworks
  • AOP-style method interception
  • Metadata-driven code generation

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.