Processing Annotations at Runtime

A Mini Validation Framework

This is how Bean Validation (@NotBlank, @Min) works in principle: annotations on fields + reflection that reads them and checks the values.

Example

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

public class Main {
    static List<String> validate(Object obj) throws IllegalAccessException {
        List<String> errors = new ArrayList<>();
        Field[] fields = obj.getClass().getDeclaredFields();
        Arrays.sort(fields, Comparator.comparing(Field::getName));
        for (Field f : fields) {
            f.setAccessible(true);
            Object value = f.get(obj);
            if (f.isAnnotationPresent(NotBlank.class) && (value == null || value.toString().isBlank())) {
                errors.add(f.getName() + ": must not be blank");
            }
            Min min = f.getAnnotation(Min.class);
            if (min != null && ((Number) value).longValue() < min.value()) {
                errors.add(f.getName() + ": " + min.message() + " (min " + min.value() + ")");
            }
        }
        return errors;
    }

    public static void main(String[] args) throws Exception {
        System.out.println(validate(new SignupForm("[email protected]", "Ravi", 21)));
        System.out.println(validate(new SignupForm(" ", "", 15)));
    }
}

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface NotBlank {}

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface Min { long value(); String message() default "too small"; }

class SignupForm {
    @NotBlank private final String email;
    @NotBlank private final String name;
    @Min(value = 18, message = "must be an adult") private final int age;
    SignupForm(String email, String name, int age) { this.email = email; this.name = name; this.age = age; }
}

Output

[]
[age: must be an adult (min 18), email: must not be blank, name: must not be blank]

Compile-time Processing

Annotations with SOURCE or CLASS retention are handled by annotation processors during compilation, which generate code instead of using slow reflection: Lombok (@Getter, @Builder), MapStruct (DTO mappers), Dagger/Hilt (Android DI) and Micronaut/Quarkus (DI without runtime reflection).