Annotations are API, and as API they need to be on the classpath at compile time. And usually, you do not want your (portable) business code to depend on yet another API.
In this article, I will present you how to allow your library users not to depend on the library API just for the annotations you provide, using java.lang.reflect and its proxy facilities.
My examples will be based on Ehcache source code, since it is for the @IgnoreSizeOf annotation that this whole idea of annotation proxying came up ! (for reference, @IgnoreSizeOf is the annotation used by Ehcache to filter out classes from the SizeOf computation)
Doing this on the Ehcache @IgnoreSizeOf annotation will allow framework developers, who embed Ehcache in their framework (and want to benefit from the sizeOf feature), to not introduce a dependency on Ehcache for their users.
Our use case : you want to use the original annotation but not to depend on it.
Instead of using and depending on Ehcache’s @IgnoreSizeOf (net.sf.ehcache.pool.sizeof.annotations.IgnoreSizeOf) :
1 2 3 4 5 6 |
package net.sf.ehcache.pool.sizeof.annotations; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE, ElementType.PACKAGE }) public @interface IgnoreSizeOf { boolean inherited() default false; } |
you want to annotate your classes with your custom com.myproject.ehcache.IgnoreSizeOf annotation , which of course has no relationship (well, it looks like it, acts like it; but it is not the same, see this as duck typing) with the original @IgnoreSizeOf annotation :
1 2 3 4 5 |
package com.myproject.ehcache; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE, ElementType.PACKAGE }) public @interface IgnoreSizeOf { } |
You can notice that the inherited method is missing in the custom annotation, more on this in the next chapter.
The big picture : use java.lang.proxy to transform the type of your annotation
Once you have scanned your class or field or package (any AnnotatedElement) for your custom annotation :
1 |
Annotation customAnnotation = element.getAnnotation(com.myproject.ehcache.IgnoreSizeOf.class); |
It is as simple as writing those 2 lines of code :
1 2 3 4 |
import net.sf.ehcache.pool.sizeof.annotations.IgnoreSizeOf; /* some class and method declarations here */ InvocationHandler handler = new AnnotationInvocationHandler(customAnnotation); IgnoreSizeOf proxiedAnnotation = Proxy.newProxyInstance(IgnoreSize.getClassLoader(), new Class[] {referenceAnnotation}, handler); |
Well not exactly… all the interesting code is actually in the InvocationHandler, that will redirect method calls to our custom annotation methods or, if it can’t, to the reference annotation methods.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
private static class AnnotationInvocationHandler implements InvocationHandler { private final Annotation customAnnotation; public AnnotationInvocationHandler(Annotation customAnnotation) { this.customAnnotation = customAnnotation; } public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { //trying to call the method on the custom annotation, if it exists Method methodOnCustom = getMatchingMethodOnGivenAnnotation(method); if (methodOnCustom != null) { return methodOnCustom.invoke(customAnnotation, args); } else { //otherwise getting the default value of the reference annotation method Object defaultValue = method.getDefaultValue(); if (defaultValue != null) { return defaultValue; } throw new UnsupportedOperationException( "The method \"" + method.getName() + "\" does not exist in the custom annotation, and there is no default value for" + " it in the reference annotation, please implement this method in your custom annotation."); } } private Method getMatchingMethodOnGivenAnnotation(Method method) { try { Method customMethod = customAnnotation.getClass().getDeclaredMethod(method.getName(), method.getParameterTypes()); if (customMethod.getReturnType().isAssignableFrom(method.getReturnType())) { return customMethod; } return null; } catch (NoSuchMethodException e) { return null; } } } |
One method to implement here : the invoke method, called on our proxied annotation every time we call a method on it.
If the method called on the proxy matches a method in our custom annotation, it just delegates the call to it.
But if not (for example remember our custom annotation does not have the inherited() method, but the reference one has), and this is where the JDK annotations are cool, the reference annotation default return value will be returned instead.
What if there is no default value in the reference annotation ? In that case you need to provide this method in your custom annotation (with a default or you can force your consumer to provide a value for this method).
In this example the consumer of Ehcache does not even need to write a complete annotation to mimic the original one ! (since the only method in the original annotation has a default value)
Actually, even in the Oracle JDK they use proxying to resolve annotations, in sun.reflect.annotation.AnnotationParser
1 2 3 4 5 6 7 |
/** * Returns an annotation of the given type backed by the given * member -> value map. */ public static Annotation annotationForMap(Class type, Map memberValues) { return (Annotation) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, new AnnotationInvocationHandler(type, memberValues)); } |
using their own sun.reflect.annotation.AnnotationInvocationHandler
Conclusion
Even if this code was written specifically with Ehcache @IgnoreSizeOf annotation in mind, it is generic enough for any framework authors to use (even more since it is Apache 2 licensed)
You can checkout Ehcache source code from http://svn.terracotta.org/svn/ehcache/trunk/ehcache/ehcache-core/ and look for AnnotationSizeOfFilter and AnnotationProxyFactory (or, even better, the test class : FilteredSizeOfTest) to learn more detail on how to provide a mechanism for letting your consumer not depend on your annotations !
Special thanks to Alex Snaps and Chris Dennis for their guidance !