Design a small reflection-based validator that reads a @NotBlank annotation on String fields. What must the annotation declare, and what reflection calls do you use?
answer
- @Retention(RUNTIME) + @Target(FIELD)
- getDeclaredFields = private included, inherited excluded
- isAnnotationPresent / getAnnotation
- setAccessible(true) then field.get(obj)
- annotation instance is a proxy; read message()
- walk getSuperclass() for inherited fields
- cache the scan per class
basics
~10 sDefine @NotBlank with @Retention(RUNTIME) and @Target(FIELD). At runtime, loop over getDeclaredFields(), check isAnnotationPresent(NotBlank.class), call setAccessible(true), read the value with field.get(object), and fail if the String is null or blank.
solid answer
~40 sThis is the classic reflection + annotation pattern. The annotation must be @Retention(RetentionPolicy.RUNTIME) (or reflection can't see it) and ideally @Target(ElementType.FIELD) to restrict placement. The validator takes an object, calls obj.getClass().getDeclaredFields() to get all fields including private ones, then for each field calls field.isAnnotationPresent(NotBlank.class) (or getAnnotation to read attributes). For a matching field you call field.setAccessible(true) to bypass private access (subject to module/JPMS rules), read the current value with field.get(obj), and apply the check. You can read annotation attributes — e.g. a message() element — from the returned annotation instance, which is a dynamic proxy. To validate inherited fields you walk the superclass chain via getSuperclass() (getDeclaredFields doesn't include them). This is exactly how Bean Validation (Hibernate Validator), Jackson, and Spring binding work under the hood.
code
java · 27 linesimport java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotBlank { String message() default "must not be blank"; }
class User { @NotBlank private String name; User(String n){ name = n; } }
public class Validator {
static List<String> validate(Object obj) throws IllegalAccessException {
List<String> errs = new ArrayList<>();
for (Class<?> c = obj.getClass(); c != null; c = c.getSuperclass())
for (Field f : c.getDeclaredFields()) {
NotBlank a = f.getAnnotation(NotBlank.class);
if (a == null || f.getType() != String.class) continue;
f.setAccessible(true);
Object v = f.get(obj);
if (v == null || ((String) v).isBlank()) errs.add(f.getName() + ": " + a.message());
}
return errs;
}
public static void main(String[] a) throws Exception {
System.out.println(validate(new User(" "))); // [name: must not be blank]
}
}go deeper
Can place a custom annotation and knows you loop over fields and check for it to validate.
Declares the annotation with RUNTIME retention and FIELD target, uses getDeclaredFields + isAnnotationPresent + setAccessible + field.get correctly, and reads annotation elements.
Handles inherited fields via the superclass walk, reads attributes from the annotation proxy, accounts for JPMS setAccessible failures, and caches reflective metadata for performance.
Frames this as the engine behind Bean Validation/Jackson/Spring, weighs reflection vs. annotation processors vs. bytecode for hot paths, and designs the annotation contract (elements, targets, repeatability, validation groups) for a reusable library.
## The goal We want runtime validation driven by metadata: mark a field `@NotBlank` and have a validator reject objects whose annotated String fields are null or only whitespace. This combines the two pillars of this topic — **reading annotations** and **reflective field access**. ## Step 1 — declare the annotation correctly ```java @Retention(RetentionPolicy.RUNTIME) // MANDATORY so reflection can see it @Target(ElementType.FIELD) // restrict to fields (compile-time guard) public @interface NotBlank { String message() default "must not be blank"; // an annotation 'element' } ``` - `@Retention(RUNTIME)` is non-negotiable: without it the default is CLASS and `isAnnotationPresent` returns false at run time, silently disabling validation. - `@Target(FIELD)` is a compile-time restriction — it makes putting `@NotBlank` on a method a compile error. It does **not** affect reflection visibility. - `message()` is an **annotation element** (annotations look like methods); you read it back at run time from the annotation instance. ## Step 2 — enumerate fields `object.getClass().getDeclaredFields()` returns all fields **declared by that class**, including `private` ones, but **not** inherited fields. (`getFields()` would return only `public` fields, including inherited — usually the wrong choice for validation.) To cover inherited fields you walk up: `for (Class<?> c = obj.getClass(); c != null; c = c.getSuperclass())`. ## Step 3 — detect the annotation For each `Field`, `field.isAnnotationPresent(NotBlank.class)` tells you if it's annotated. If you need the attributes, use `NotBlank a = field.getAnnotation(NotBlank.class)` and call `a.message()`. The returned annotation is a JDK **dynamic proxy** implementing the annotation interface; its element methods return the declared/default values. ## Step 4 — read the field value Private fields are not accessible by default. Call `field.setAccessible(true)` to suppress Java access checks, then `Object value = field.get(obj)` to read the current value. Under the Java Platform Module System (JPMS), `setAccessible(true)` can throw `InaccessibleObjectException` unless the field's package is *open* to your module — a real-world consideration for libraries. ## Step 5 — apply the rule Validate only `String` fields (check `field.getType() == String.class`), then fail if `value == null || ((String) value).isBlank()`, surfacing the `message()`. ## Putting it together ```java public static List<String> validate(Object obj) throws IllegalAccessException { List<String> errors = new ArrayList<>(); for (Class<?> c = obj.getClass(); c != null; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { NotBlank a = f.getAnnotation(NotBlank.class); if (a == null || f.getType() != String.class) continue; f.setAccessible(true); Object v = f.get(obj); if (v == null || ((String) v).isBlank()) errors.add(f.getName() + ": " + a.message()); } } return errors; } ``` ## Why this matters This 20-line skeleton is the conceptual core of Bean Validation (JSR 380 / Hibernate Validator's `@NotBlank`, `@Size`, etc.), Jackson's `@JsonProperty` binding, and Spring's data binding. Knowing it means you understand *how* declarative annotations turn into behavior: a RUNTIME-retained annotation + reflective discovery + reflective access. Performance-sensitive frameworks cache the reflective lookups (which fields are annotated) per class to avoid repeating the scan on every call.
- Why getDeclaredFields() and not getFields()?getDeclaredFields() returns all fields declared by the class including private ones (validation usually targets private fields), while getFields() returns only public fields (including inherited). For full coverage you also walk getSuperclass().
- What can prevent setAccessible(true) from succeeding?The Java Platform Module System: if the field's package isn't 'opens'-ed to your module, setAccessible throws InaccessibleObjectException. Strong encapsulation since Java 9 enforces this.
saying these in an interview costs you the question
- Forgetting @Retention(RUNTIME) so the validator silently never fires
- Using getFields() (only public, includes inherited) when you need all declared fields
- Skipping setAccessible(true) and getting IllegalAccessException on private fields
- Assuming setAccessible always works (JPMS can throw InaccessibleObjectException)
- Forgetting that getDeclaredFields excludes inherited fields