Adding Java Annotations at Runtime

JavaAnnotationsRuntime

Java Problem Overview


Is it possible to add an annotation to an object (in my case in particular, a Method) at runtime?

For a bit more explanation: I have two modules, moduleA and moduleB. moduleB depends upon moduleA, which doesn't depend upon anything. (modA is my core datatypes and interfaces and such, modB is db/data layer) modB also depends on externalLibrary. In my case, modB is handing off a class from modA to externalLibrary, which needs certain methods to be annotated. The specific annotations are all part of externalLib and, as I said, modA doesn't depend on externalLib and I'd like to keep it that way.

So, is this possible, or do you have suggestions for other ways of looking at this problem?

Java Solutions


Solution 1 - Java

It's possible via bytecode instrumentation library such as http://www.csg.is.titech.ac.jp/~chiba/javassist/">Javassist</a>;.

In particular, take a look at http://www.csg.is.titech.ac.jp/~chiba/javassist/html/javassist/bytecode/AnnotationsAttribute.html">AnnotationsAttribute</a> class for an example on how to create / set annotations and http://www.csg.is.titech.ac.jp/~chiba/javassist/tutorial/tutorial3.html#intro">tutorial section on bytecode API for general guidelines on how to manipulate class files.

This is anything but simple and straightforward, though - I would NOT recommend this approach and suggest you consider Tom's answer instead unless you need to do this for a huge number of classes (or said classes aren't available to you until runtime and thus writing an adapter is impossible).

Solution 2 - Java

It is also possible to add an Annotation to a Java class at runtime using the Java reflection API. Essentially one must recreate the internal Annotation maps defined in the class java.lang.Class (or for Java 8 defined in the internal class java.lang.Class.AnnotationData). Naturally this approach is quite hacky and might break at any time for newer Java versions. But for quick and dirty testing/prototyping this approach can be useful at times.

Proove of concept example for Java 8:

public final class RuntimeAnnotations {

	private static final Constructor<?> AnnotationInvocationHandler_constructor;
	private static final Constructor<?> AnnotationData_constructor;
	private static final Method Class_annotationData;
	private static final Field Class_classRedefinedCount;
	private static final Field AnnotationData_annotations;
	private static final Field AnnotationData_declaredAnotations;
	private static final Method Atomic_casAnnotationData;
	private static final Class<?> Atomic_class;

	static{
		// static initialization of necessary reflection Objects
		try {
			Class<?> AnnotationInvocationHandler_class = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
			AnnotationInvocationHandler_constructor = AnnotationInvocationHandler_class.getDeclaredConstructor(new Class[]{Class.class, Map.class});
			AnnotationInvocationHandler_constructor.setAccessible(true);

			Atomic_class = Class.forName("java.lang.Class$Atomic");
			Class<?> AnnotationData_class = Class.forName("java.lang.Class$AnnotationData");

			AnnotationData_constructor = AnnotationData_class.getDeclaredConstructor(new Class[]{Map.class, Map.class, int.class});
			AnnotationData_constructor.setAccessible(true);
			Class_annotationData = Class.class.getDeclaredMethod("annotationData");
			Class_annotationData.setAccessible(true);

			Class_classRedefinedCount= Class.class.getDeclaredField("classRedefinedCount");
			Class_classRedefinedCount.setAccessible(true);

			AnnotationData_annotations = AnnotationData_class.getDeclaredField("annotations");
			AnnotationData_annotations.setAccessible(true);
			AnnotationData_declaredAnotations = AnnotationData_class.getDeclaredField("declaredAnnotations");
			AnnotationData_declaredAnotations.setAccessible(true);
			
			Atomic_casAnnotationData = Atomic_class.getDeclaredMethod("casAnnotationData", Class.class, AnnotationData_class, AnnotationData_class);
			Atomic_casAnnotationData.setAccessible(true);

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

	public static <T extends Annotation> void putAnnotation(Class<?> c, Class<T> annotationClass, Map<String, Object> valuesMap){
		putAnnotation(c, annotationClass, annotationForMap(annotationClass, valuesMap));
	}
	
	public static <T extends Annotation> void putAnnotation(Class<?> c, Class<T> annotationClass, T annotation){
		try {
			while (true) { // retry loop
				int classRedefinedCount = Class_classRedefinedCount.getInt(c);
				Object /*AnnotationData*/ annotationData = Class_annotationData.invoke(c);
				// null or stale annotationData -> optimistically create new instance
				Object newAnnotationData = createAnnotationData(c, annotationData, annotationClass, annotation, classRedefinedCount);
				// try to install it
				if ((boolean) Atomic_casAnnotationData.invoke(Atomic_class, c, annotationData, newAnnotationData)) {
					// successfully installed new AnnotationData
					break;
				}
			}
		} catch(IllegalArgumentException | IllegalAccessException | InvocationTargetException | InstantiationException e){
			throw new IllegalStateException(e);
		}

	}

	@SuppressWarnings("unchecked")
	private static <T extends Annotation> Object /*AnnotationData*/ createAnnotationData(Class<?> c, Object /*AnnotationData*/ annotationData, Class<T> annotationClass, T annotation, int classRedefinedCount) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) AnnotationData_annotations.get(annotationData);
		Map<Class<? extends Annotation>, Annotation> declaredAnnotations= (Map<Class<? extends Annotation>, Annotation>) AnnotationData_declaredAnotations.get(annotationData);

		Map<Class<? extends Annotation>, Annotation> newDeclaredAnnotations = new LinkedHashMap<>(annotations);
		newDeclaredAnnotations.put(annotationClass, annotation);
		Map<Class<? extends Annotation>, Annotation> newAnnotations ;
		if (declaredAnnotations == annotations) {
			newAnnotations = newDeclaredAnnotations;
		} else{
			newAnnotations = new LinkedHashMap<>(annotations);
			newAnnotations.put(annotationClass, annotation);
		}
		return AnnotationData_constructor.newInstance(newAnnotations, newDeclaredAnnotations, classRedefinedCount);
	}

	@SuppressWarnings("unchecked")
	public static <T extends Annotation> T annotationForMap(final Class<T> annotationClass, final Map<String, Object> valuesMap){
		return (T)AccessController.doPrivileged(new PrivilegedAction<Annotation>(){
			public Annotation run(){
				InvocationHandler handler;
				try {
					handler = (InvocationHandler) AnnotationInvocationHandler_constructor.newInstance(annotationClass,new HashMap<>(valuesMap));
				} catch (InstantiationException | IllegalAccessException
						| IllegalArgumentException | InvocationTargetException e) {
					throw new IllegalStateException(e);
				}
				return (Annotation)Proxy.newProxyInstance(annotationClass.getClassLoader(), new Class[] { annotationClass }, handler);
			}
		});
	}
}

Usage example:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnnotation {
    String value();
}

public static class TestClass{}

public static void main(String[] args) {
    TestAnnotation annotation = TestClass.class.getAnnotation(TestAnnotation.class);
    System.out.println("TestClass annotation before:" + annotation);

    Map<String, Object> valuesMap = new HashMap<>();
    valuesMap.put("value", "some String");
    RuntimeAnnotations.putAnnotation(TestClass.class, TestAnnotation.class, valuesMap);

    annotation = TestClass.class.getAnnotation(TestAnnotation.class);
    System.out.println("TestClass annotation after:" + annotation);
}

Output:

TestClass annotation before:null
TestClass annotation after:@RuntimeAnnotations$TestAnnotation(value=some String)

Limitations of this approach:

  • New versions of Java may break the code at any time.
  • The above example only works for Java 8 - making it work for older Java versions would require checking the Java version at runtime and changing the implementation accordingly.
  • If the annotated Class gets redefined (e.g. during debugging), the annotation will be lost.
  • Not thoroughly tested; not sure if there are any bad side effects - use at your own risk...

Solution 3 - Java

It's not possible to add an annotation at runtime, it sounds like you need to introduce an adapter that module B uses to wrap the object from module A exposing the required annotated methods.

Solution 4 - Java

It is possible to create annotations at runtime via a Proxy. You can then add them to your Java objects via reflection as suggested in other answers (but you probably would be better off finding an alternative way to handle that, as messing with the existing types via reflection can be dangerous and hard to debug).

But it is not very easy... I wrote a library called, I hope appropriately, Javanna just to do this easily using a clean API.

It's in JCenter and Maven Central.

Using it:

@Retention( RetentionPolicy.RUNTIME )
@interface Simple {
    String value();
}

Simple simple = Javanna.createAnnotation( Simple.class, 
    new HashMap<String, Object>() {{
        put( "value", "the-simple-one" );
    }} );

If any entry of the map does not match the annotation declared field(s) and type(s), an Exception is thrown. If any value that has no default value is missing, an Exception is thrown.

This makes it possible to assume every annotation instance that is created successfully is as safe to use as a compile-time annotation instance.

As a bonus, this lib can also parse annotation classes and return the values of the annotation as a Map:

Map<String, Object> values = Javanna.getAnnotationValues( annotation );

This is convenient for creating mini-frameworks.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionClaytonView Question on Stackoverflow
Solution 1 - JavaChssPly76View Answer on Stackoverflow
Solution 2 - JavaBalderView Answer on Stackoverflow
Solution 3 - JavaTomView Answer on Stackoverflow
Solution 4 - JavaRenatoView Answer on Stackoverflow