//
you're reading...
Java annotations

Changing annotation values at runtime

Sometime back last week, I was tasked with adding some reporters via annotations into a cucumber based test. So it all boiled down to “Altering annotations at runtime” [ I was trying to alter @CucumberOptions annotation wherein I was going to be adding one or more “plugins” attribute]. I had worked a lot with TestNG and so thought that this must be a piece of cake.. afterall, TestNG lets me use AnnotationTransformers to change annotations. What I didn’t realise was how difficult this would turn out to be and not to mention, the flakiness of the solution.

TestNG does it in an elegant way.. and once again I stand amazed at the amount of thought process that was put into building it.. But since we don’t currently have all that luxury when it comes to playing around with the annotation that I was trying to alter at runtime, I had to resort to using “Reflections”…

So here’s how the journey went.

Lets first imagine that I have an annotation such as the one below that I would like to alter.

@Retention (RetentionPolicy.RUNTIME)
public @interface Greet {
    /**
     * @return - The name of the person to greet.
     */
    String name() default "";
}

I was now looking to alter the value of “name” at runtime based on some condition. An example usage of this would be something like below :

public class AnnotationExample {
    public static void main(String[] args) {
        Greet greet = Demo.class.getAnnotation(Greet.class);
        System.err.println("Hello there [" + greet.name() + "]";
    }

    @Greet (name = "Dragon Warrior")
    public static class Demo {
    }
}

In the above example, we have an example class named “Demo” which has our custom annotation “Greet” and we are now using reflection to get the value that was set viz., “Dragon Warrior”..
The output would something like below

Hello there [Dragon Warrior]

Process finished with exit code 0

So lets go about building the logic that would basically help us alter the value of the “name” attribute in the Greet annotation at runtime.

I learnt that the approach to alter annotation values at runtime differs across JDK versions. So in JDK7 its a different approach and in JDK8 its a different version. We are conveniently ignoring JDK6 because hardly anyone uses it.

Here’s how that utility method could look like :

public static boolean isJDK7OrLower() {
    boolean jdk7OrLower = true;
    try {
        Class.class.getDeclaredField("annotations");
    } catch (NoSuchFieldException e) {
        //Willfully ignore all exceptions
        jdk7OrLower = false;
    }
    return jdk7OrLower;
}

Explanation :
The java class “Class” basically has a private member named “annotations” which is a map of annotation classes to annotation objects

Map<Class<? extends Annotation>, Annotation annotations>

So if we find this field in the “Class” class, then it basically means we are on JDK7.

Now lets see how to alter this annotation.

From what I understand, Java basically maintains a map wherein the key is the class of the annotation and the value is the instance of the annotation.

So lets first create an implementation of our annotation.

public class DynamicGreetings implements Greet {
    private String name;

    public DynamicGreetings(String name) {
        this.name = name;
    }

    @Override
    public String name() {
        return name;
    }

    @Override
    public Class<? extends Annotation> annotationType() {
        return DynamicGreetings.class;
    }
}

Now lets build the complete logic which alters the annotation at runtime

public static void alterAnnotationOn(Class clazzToLookFor, Class<? extends Annotation> annotationToAlter,Annotation annotationValue) {
    if (isJDK7OrLower()) {
        try {
            Field annotations = Class.class.getDeclaredField(ANNOTATIONS);
            annotations.setAccessible(true);
            Map<Class<? extends Annotation>, Annotation> map =
                (Map<Class<? extends Annotation>, Annotation>) annotations.get(clazzToLookFor);
            map.put(annotationToAlter, annotationValue);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

Lets try testing our code… Run the below sample..

public class AnnotationExample {
    public static void main(String[] args) {
        System.err.println("JDK 7 ? " + AnnotationHelper.isJDK7OrLower());
        Greet greet = Demo.class.getAnnotation(Greet.class);
        System.err.println("Hello there [" + greet.name() + "]");
        DynamicGreetings altered = new DynamicGreetings("KungFu Panda");
        AnnotationHelper.alterAnnotationOn(Demo.class, Greet.class, altered);
        greet = Demo.class.getAnnotation(Greet.class);
        System.err.println("After alteration...Hello there [" + greet.name() + "]");

    }

    @Greet (name = "Dragon Warrior")
    public static class Demo {
    }
}

You should see an output as below :

JDK 7 ? true
Hello there [Dragon Warrior]
After alteration...Hello there [KungFu Panda]

Process finished with exit code 0

As I mentioned before, the implementation doesn’t work on JDK8 [ Notice how we had put an edit check which tests the JDK version…]. So lets see how we can enhance our implementation to work on JDK8..

Here’s the complete implementation of the AnnotationHelper class along with support for JDK8

public class AnnotationHelper {
    private static final String ANNOTATIONS = "annotations";
    public static final String ANNOTATION_DATA = "annotationData";

    public static boolean isJDK7OrLower() {
        boolean jdk7OrLower = true;
        try {
            Class.class.getDeclaredField(ANNOTATIONS);
        } catch (NoSuchFieldException e) {
            //Willfully ignore all exceptions
            jdk7OrLower = false;
        }
        return jdk7OrLower;
    }

    public static void alterAnnotationOn(Class clazzToLookFor, Class<? extends Annotation> annotationToAlter,Annotation annotationValue) {
        if (isJDK7OrLower()) {
            try {
                Field annotations = Class.class.getDeclaredField(ANNOTATIONS);
                annotations.setAccessible(true);
                Map<Class<? extends Annotation>, Annotation> map =
                    (Map<Class<? extends Annotation>, Annotation>) annotations.get(clazzToLookFor);
                map.put(annotationToAlter, annotationValue);
            } catch (Exception  e) {
                e.printStackTrace();
            }
        } else {
            try {
                //In JDK8 Class has a private method called annotationData().
                //We first need to invoke it to obtain a reference to AnnotationData class which is a private class
                Method method = Class.class.getDeclaredMethod(ANNOTATION_DATA, null);
                method.setAccessible(true);
                //Since AnnotationData is a private class we cannot create a direct reference to it. We will have to
                //manage with just Object
                Object annotationData = method.invoke(clazzToLookFor);
                //We now look for the map called "annotations" within AnnotationData object.
                Field annotations = annotationData.getClass().getDeclaredField(ANNOTATIONS);
                annotations.setAccessible(true);
                Map<Class<? extends Annotation>, Annotation> map =
                    (Map<Class<? extends Annotation>, Annotation>) annotations.get(annotationData);
                map.put(annotationToAlter, annotationValue);
            } catch (Exception  e) {
                e.printStackTrace();
            }
        }
    }
}

And here’s the output when you run the above sample code (“AnnotationExample”] on JDK8.

JDK 7 ? false
Hello there [Dragon Warrior]
After alteration...Hello there [KungFu Panda]

Process finished with exit code 0
Advertisements

Discussion

7 thoughts on “Changing annotation values at runtime

  1. simply amazing!

    Posted by Sandeep Pandey | May 30, 2016, 9:51 pm
  2. This code does not work with java 8
    java.lang.NoSuchMethodException: annotationData

    Posted by Droid | January 5, 2017, 12:30 pm
    • I am no expert on JDKs, but I currently have the below JDK installed on my machine, and this code works fine.

      java version “1.8.0_66”
      Java(TM) SE Runtime Environment (build 1.8.0_66-b17)
      Java HotSpot(TM) 64-Bit Server VM (build 25.66-b17, mixed mode)

      Since all of these are essentially hacks, the method am guessing is susceptible to change because we are relying on reflection and trying to access the internals of “Class.java” which was never exposed and meant to be available for the general public. You might have to look at your JDK’s Class.java to figure out what is the exact method. Since I don’t have multiple JDK flavors available at my disposal I haven’t done that research to figure out if this would work on all flavors of JDK8

      Posted by Confusions Personified | January 6, 2017, 9:11 am
  3. very cool!

    Posted by Den | April 25, 2017, 2:32 pm
  4. instead Class.class.getDeclaredMethod(ANNOTATION_DATA, null) can be used Class.class.getDeclaredMethod(ANNOTATION_DATA, new Class[0]); for SuppressWarnings

    Posted by Den | April 25, 2017, 2:37 pm
  5. This works well for annotations for the class ! but how do I change the annotations on methods at runtime ?

    Posted by Ayush Choukse | August 1, 2017, 3:52 am

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: