dambros
dambros

Reputation: 4392

How to get values of nested class attribute using reflection?

Currently I have classes with several nested classes inside is, like:

public class Foo {

  private Bar bar;

  //getter & setter

  public class Bar {

    private Abc abc;

    //getter & setter

    public class Abc {
      @RequiredParam
      private String property;

      //getter & setter
    }
  }

}

I am trying to get the value of the fields but I am having a hard time how to achieve this.

So far I have:

  public static boolean isValid(Object paramClazz) throws Exception {
    List<Class> classes = new ArrayList<>();
    getClasses(classes, paramClazz.getClass());

    for (Class clazz : classes) {
      for (Field field : clazz.getDeclaredFields()) {
        if (field.isAnnotationPresent(RequiredParam.class)) {
          field.setAccessible(true);
          //how to get the value? field.get(paramClazz) doesn't work
        }
      }
    }
    return true;
  }

  private static void getClasses(List<Class> classes, Class<?> clazz) {
    if (clazz.getDeclaredClasses().length > 0) {
      for (Class<?> c : clazz.getDeclaredClasses()) {
        getClasses(classes, c);
      }
    }
    classes.add(clazz);
  }

My goal is to the able to check if the field annotated with @RequiredParam is not null, so I have the method isValid() which will received an object and should be able to check all fields (even the ones inside nested classes) and see if any is missing.

The problem is when I try to call field.get() and I don't know which object I am supposed to pass to this method. Passing the highest level object won't work, because I need somehow to pass only the Abc object to the method.

How can I get the correct object to pass to the field.get() call, considering I can have more or less nested levels in my classes?

Upvotes: 3

Views: 5708

Answers (1)

Czyzby
Czyzby

Reputation: 3139

This is an example code that scans all fields of an object recursively until it finds values of all annotated fields:

public static Collection<Object> getAnnotatedValues(final Object root) throws ReflectiveOperationException {
    return getAnnotatedValues(root, new HashSet<>());
}

private static Collection<Object> getAnnotatedValues(final Object root, final Set<Object> inspected)
        throws ReflectiveOperationException {
    final List<Object> annotatedValues = new ArrayList<>();
    if (inspected.contains(root)) { // Prevents stack overflow.
        return Collections.EMPTY_LIST;
    }
    inspected.add(root);
    for (final Field field : gatherFields(root.getClass())) {
        field.setAccessible(true);
        final Object currentValue = field.get(root);
        field.setAccessible(false);
        if (field.isAnnotationPresent(RequiredParam.class)) {
            // Found required value, search finished:
            annotatedValues.add(currentValue);
            if (currentValue != null) {
                inspected.add(currentValue);
            }
        } else if (currentValue != null) {
            // Searching for annotated fields in nested classes:
            annotatedValues.addAll(getAnnotatedValues(currentValue, inspected));
        }
    }
    return annotatedValues;
}

private static Iterable<Field> gatherFields(Class<?> fromClass) {
    // Finds ALL fields, even the ones from super classes.
    final List<Field> fields = new ArrayList<>();
    while (fromClass != null) {
        fields.addAll(Arrays.asList(fromClass.getDeclaredFields()));
        fromClass = fromClass.getSuperclass();
    }
    return fields;
}

You might have implemented something similar, but had trouble getting to the last nested class. This is because Field instance is just a description of a class and (unless the field is static) it needs an actual instance the be able to extract a value. From Field#get(Object) method docs:

If the underlying field is a static field, the obj argument is ignored; it may be null.

Otherwise, the underlying field is an instance field. If the specified obj argument is null, the method throws a NullPointerException. If the specified object is not an instance of the class or interface declaring the underlying field, the method throws an IllegalArgumentException.

If you did not initiate fields in your class structure, you would have a hard time extracting a value - even if you found annotated fields, you would still need an instance of the class to extract them. For example, given these classes:

public class Foo {
    private final Bar bar = new Bar();
    public class Bar {
        private final Abc abc = new Abc();
        public class Abc {
            @RequiredParam private final String property = "Result.";
        }
    }
    @Retention(RetentionPolicy.RUNTIME)
    public static @interface RequiredParam {}
}

...getAnnotatedValues(new Foo()) returns collection containing "Result.". You can easily modify the methods to fit your needs (for example, return true as soon as the first valid annotated field is found or simply return false if the collection is empty/contains nulls).

Upvotes: 5

Related Questions