2

From this answer it's possible to add annotations to Java classes during runtime by creating and installing a new internal AnnotationData object. I was curious if it would be possible for a Field. It seems like the way a Field handles annotations is pretty different from how a Class handles them.

I've been able to successfully add an annotation to the declaredAnnotations field of the Field class with the following class:

public class FieldRuntimeAnnotations {

  private static final Field DECLARED_ANNOTATIONS_FIELD;
  private static final Method DECLARED_ANNOTATIONS_METHOD;

  static {
    try {
      DECLARED_ANNOTATIONS_METHOD = Field.class.getDeclaredMethod("declaredAnnotations");
      DECLARED_ANNOTATIONS_METHOD.setAccessible(true);

      DECLARED_ANNOTATIONS_FIELD = Field.class.getDeclaredField("declaredAnnotations");
      DECLARED_ANNOTATIONS_FIELD.setAccessible(true);

    } catch (NoSuchMethodException | NoSuchFieldException | ClassNotFoundException e) {
            throw new IllegalStateException(e);
    }
  }

  // Public access method
  public static <T extends Annotation> void putAnnotationToField(Field f, Class<T> annotationClass, Map<String, Object> valuesMap) {
    T annotationValues = TypeRuntimeAnnotations.annotationForMap(annotationClass, valuesMap);

    try {

        Object annotationData = DECLARED_ANNOTATIONS_METHOD.invoke(f);

        // Get declared annotations
        Map<Class<? extends Annotation>, Annotation> declaredAnnotations =
                (Map<Class<? extends Annotation>, Annotation>) DECLARED_ANNOTATIONS_FIELD.get(f);

        // Essentially copy our original annotations to a new LinkedHashMap
        Map<Class<? extends Annotation>, Annotation> newDeclaredAnnotations = new LinkedHashMap<>(declaredAnnotations);

        newDeclaredAnnotations.put(annotationClass, annotationValues);

        DECLARED_ANNOTATIONS_FIELD.set(f, newDeclaredAnnotations);

    } catch (IllegalAccessException | InvocationTargetException e) {
            throw new IllegalStateException(e);
    }
  }
}

However, the field's declaring class does not get updated with the proper ReflectionData. So essentially I need to "install" the new field information with its declaring class, but I am having trouble of figuring out how.

To make it clearer what I'm asking, the 3rd assertion in my test here fails:

public class RuntimeAnnotationsTest {

  @Retention(RetentionPolicy.RUNTIME)
  @Target({ElementType.TYPE, ElementType.FIELD})
  public @interface TestAnnotation {}

  public static class TestEntity {
    private String test;
  }

  @Test
  public void testPutAnnotationToField() throws NoSuchFieldException {

    // Confirm class does not have annotation
    TestAnnotation annotation = TestEntity.class.getDeclaredField("test").getAnnotation(TestAnnotation.class);
    Assert.assertNull(annotation);

    Field f = TestEntity.class.getDeclaredField("test");
    f.setAccessible(true);

    FieldRuntimeAnnotations.putAnnotationToField(f, TestAnnotation.class, new HashMap<>());

    // Make sure field annotation gets set
    Assert.assertNotNull(f.getAnnotation(TestAnnotation.class));

    // Make sure the class that contains that field is also updated -- THIS FAILS
    Assert.assertNotNull(TestEntity.class.getDeclaredField("test").getAnnotation(TestAnnotation.class));
  }

} 

I understand what I'm trying to achieve is rather ridiculous, but I'm enjoying the exercise :D ... Any thoughts?

Community
  • 1
  • 1
heez
  • 2,029
  • 3
  • 27
  • 39

1 Answers1

3

With TestEntity.class.getDeclaredField("test") you get a copy of the private internal field of TestEntity.class but you need the original field. I extended your test case getting the original private field from the private method 'privateGetDeclaredFields' in Class.class.

import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import org.junit.Assert;
import org.junit.Test;

public class FieldRuntimeAnnotationsTest {
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.FIELD})
    public @interface TestAnnotation {}

    public static class TestEntity {
        private String test;
    }

    @Test
    public void testPutAnnotationToField() throws NoSuchFieldException {
        // Confirm class does not have annotation
        TestAnnotation annotation = TestEntity.class.getDeclaredField("test").getAnnotation(TestAnnotation.class);
        Assert.assertNull(annotation);

        // This field is a copy of the internal one
        Field f = TestEntity.class.getDeclaredField("test");
        f.setAccessible(true);

        FieldRuntimeAnnotations.putAnnotationToField(f, TestAnnotation.class, new HashMap<>());

        // Make sure field annotation gets set
        Assert.assertNotNull(f.getAnnotation(TestAnnotation.class));

        // Make sure the class that contains that field is not updated -- THE FIELD IS A COPY
        Assert.assertNull(TestEntity.class.getDeclaredField("test").getAnnotation(TestAnnotation.class));

        // Repeat the process with the internal field
        Field f2 = getDeclaredField(TestEntity.class, "test");
        f2.setAccessible(true);

        FieldRuntimeAnnotations.putAnnotationToField(f2, TestAnnotation.class, new HashMap<>());

        // Make sure field annotation gets set
        Assert.assertNotNull(f2.getAnnotation(TestAnnotation.class));

        // Make sure the class that contains that field is also updated -- THE FIELD IS THE ORIGINAL ONE
        Assert.assertNotNull(TestEntity.class.getDeclaredField("test").getAnnotation(TestAnnotation.class));
    }

    public Field getDeclaredField(Class<?> clazz, String name) {
        if (name == null || name.isEmpty()) {
            return null;
        }
        Field[] fields = getDeclaredFields(clazz);
        Field field = null;
        for (Field f : fields) {
            if (name.equals(f.getName())) {
                field = f;
            }
        }
        return field;
    }

    public Field[] getDeclaredFields(Class<?> clazz) {
        if (clazz == null) {
            return new Field[0];
        }
        Method privateGetDeclaredFieldsMethod = null;
        Object value = null;
        try {
            privateGetDeclaredFieldsMethod = Class.class.getDeclaredMethod("privateGetDeclaredFields", boolean.class);
            privateGetDeclaredFieldsMethod.setAccessible(true);
            value = privateGetDeclaredFieldsMethod.invoke(clazz, Boolean.FALSE);
        }
        catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            Assert.fail("Error for " + clazz + ", exception=" + e.getMessage());
    }
        Field[] fields = value == null ? new Field[0] : (Field[])value;
        return fields;
    }
}
Torres
  • 46
  • 3
  • Works like a charm! I was missing that crucial piece of invoking that `Class.class` method `privateGetDeclaredFields` with the field's declaring class with the boolean parameter `false`. Thank you! – heez May 31 '17 at 19:32