Changing annotation values at runtime with Java Reflection
Annotations are supposed to be fixed once the code is compiled. I needed one to change, so I went looking for where Java actually keeps them.
I was preparing for a meeting with a customer, where I’d walk them through an implementation I’d done most of, and writing it down was how I made sure I really understood it.
A problem
Annotations are one of those things every Java developer uses without thinking:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Greeter {
String greet() default "";
}
public class Greetings {
@Greeter(greet = "Good morning")
public String morning;
}
The value "Good morning" is written into the class file at compile time. At runtime, you can read it with reflection:
Field field = Greetings.class.getField("morning");
Greeter greeter = field.getAnnotation(Greeter.class);
greeter.greet(); // "Good morning"
But you can’t change it. Annotations are immutable. There’s no setGreet().
In my case, I needed exactly that. In our tests, I wanted the same class to behave as if it had different annotation values, without writing a copy of the class for every variation and without recompiling.
So the question was: if annotations can’t be changed, where does Java actually keep them? Because if I could find the storage, maybe I could change what’s in it.
Finding where annotations live
I went reading the JDK source for java.lang.reflect.Field. Inside, there’s a private method that looked very promising:
// inside java.lang.reflect.Field (JDK source, simplified)
private Map<Class<? extends Annotation>, Annotation> declaredAnnotations() {
Map<Class<? extends Annotation>, Annotation> declAnnos;
if ((declAnnos = declaredAnnotations) == null) {
synchronized (this) {
if ((declAnnos = declaredAnnotations) == null) {
// parse the annotations from the class file once...
declAnnos = AnnotationParser.parseAnnotations(...);
// ...and cache them in this map
declaredAnnotations = declAnnos;
}
}
}
return declAnnos;
}
There it was. The first time you ask for a field’s annotations, Java parses them from the class file and caches them in a Map, keyed by annotation type. Every later call to getAnnotation() reads from that map.
The annotation object itself is immutable. But the map holding it isn’t. If I could get hold of that map, I could swap the annotation for a different one.
Breaking in (carefully)
The method is private, and since Java 9, the module system protects the JDK’s internals from exactly this kind of poking. So first, I had to explicitly open java.lang.reflect to my code. In Maven, for tests, that’s done in the Surefire plugin:
<argLine>--add-opens java.base/java.lang.reflect=ALL-UNNAMED</argLine>
--add-opens tells the JVM: “I know this package is internal. Let unnamed modules (normal classpath code) reflect into it anyway.” Without it, setAccessible(true) throws an InaccessibleObjectException.
Then the helper itself:
public final class AnnotationHelper {
@SuppressWarnings("unchecked")
public static <A extends Annotation> void replaceAnnotation(
Field field, Class<A> annotationType, A newAnnotation) {
try {
field.getAnnotation(annotationType); // make sure the cache is populated
Method method = Field.class.getDeclaredMethod("declaredAnnotations");
method.setAccessible(true);
var annotations = (Map<Class<? extends Annotation>, Annotation>) method.invoke(field);
annotations.put(annotationType, newAnnotation);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Could not replace annotation on " + field, e);
}
}
}
And to create the new annotation value, you can implement the annotation interface directly:
Greeter evening = new Greeter() {
public String greet() { return "Good evening"; }
public Class<? extends Annotation> annotationType() { return Greeter.class; }
};
Field field = Greetings.class.getField("morning");
AnnotationHelper.replaceAnnotation(field, Greeter.class, evening);
field.getAnnotation(Greeter.class).greet(); // "Good evening"
It worked. And it felt a little bit like I’d done something I wasn’t supposed to.
Why you should be careful
Because I had. A few things I learned, some the hard way:
- It’s global. The cache belongs to the
Fieldobject Java hands out for that class. Change it in one test, and it may still be changed in the next one. Always put the original back afterwards. - Frameworks read annotations too. Imagine changing
@Transactionalor@Autowiredon a live Spring application. Spring may have already read them at startup and built proxies around them, or it may read them again later. The behaviour can get very confusing, very quickly. - It depends on JDK internals.
declaredAnnotations()is a private implementation detail. A future Java version can rename it, restructure it, or lock it down further, and your code breaks without warning. --add-opensweakens encapsulation that exists for security reasons. It’s fine in a test run. I wouldn’t ship it in production.
When it might actually make sense
- Testing, which was my case: simulating different annotation configurations without recompiling or duplicating classes.
- Libraries and tools that do runtime bytecode tricks, like proxy generators or instrumentation agents, sometimes need this level of access.
For almost everything else, there’s a cleaner way: make the value configurable instead of hard-coding it in an annotation, or read it from a property the annotation points to.
What I took away
The most valuable part wasn’t the trick itself. It was the habit of asking “where does this actually live?” and then going to read the JDK source to find out. “Annotations are immutable” turned out to mean “the annotation objects are immutable”. The map holding them was just a map.
Discussion
Enjoy!