Using Java for several years, annotations are a common type, especially in some frameworks will be a lot of annotations to do component identification, configuration or strategy. But has not been deeply to explore the JDK annotations in the end what is the bottom is how to achieve? So refer to some information, to do a slightly more detailed analysis.

JDK annotations description

Refer to JLS-9.6 inside JavaSE-8 for a description of the annotations as follows.

The annotation is declared as follows.

{InterfaceModifier} @ interface Identifier AnnotationTypeBody

Among them.

  • The identifier in the annotation type declaration specifies the name of the annotation type.
  • If the annotation type has the same simple name as any of its enclosing classes or interfaces, it will be compiled with an error.
  • The direct parent interface of each annotation type is java.lang.annotation.

Since the parent interface of all annotation types is java.lang.annotation.Annotation, we can look at the documentation for the Annotation interface.

public interface Annotation

The common interface extended by all annotation types. Note that an interface that manually extends this one does not define an annotation type. Also note that this interface does not itself define an annotation type. More information about annotation types can be found in section 9.6 of The Java™ Language Specification. The AnnotatedElement interface discusses compatibility concerns when evolving an annotation type from being non-repeatable to being repeatable.

Since: 1.5

The description of Annotation in the JavaSE-8 document is similar to that in JLS-9.6, but finally specifies the compatibility considerations for repeatable annotations, which are implemented in JDK1.8 by the meta-annotation @Repeatable. The following is based on the last version of the JDK8 java version 1.8.0_181 to explore the underlying implementation of annotations in the JDK.

Annotation Implementation Exploration

We start by defining a very simple Counter annotation as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package club.throwable.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
public @interface Counter {

    int count() default 0;
}

Let’s start by looking at the type of @Counter instance visually by using the @Counter annotation directly.

1
2
3
4
5
6
7
8
@Counter(count = 1)
public class Main {

	public static void main(String[] args) throws Exception{
		Counter counter = Main.class.getAnnotation(Counter.class);
		System.out.println(counter.count());
	}
}

The @Counter instance was observed from the Debug process to be a proxy class of the JDK (and the instance of InvocationHandler is sun.reflect.annotation. AnnotationInvocationHandler, which is a class available in the sun package with the modifier default), to verify this we use the JDK’s decompile command to look at the bytecode of @Counter: the

1
javap -c -v D:\Projects\rxjava-seed\target\classes\club\throwable\annotation\Counter.class

The decompiled bytecode of @Counter is as follows.

 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
40
41
Classfile /D:/Projects/rxjava-seed/target/classes/club/throwable/annotation/Counter.class
  Last modified 2018-10-6; size 487 bytes
  MD5 checksum 83cee23f426e5b51a096281068d8b555
  Compiled from "Counter.java"
public interface club.throwable.annotation.Counter extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #19            // club/throwable/annotation/Counter
   #2 = Class              #20            // java/lang/Object
   #3 = Class              #21            // java/lang/annotation/Annotation
   #4 = Utf8               count
   #5 = Utf8               ()I
   #6 = Utf8               AnnotationDefault
   #7 = Integer            0
   #8 = Utf8               SourceFile
   #9 = Utf8               Counter.java
  #10 = Utf8               RuntimeVisibleAnnotations
  #11 = Utf8               Ljava/lang/annotation/Retention;
  #12 = Utf8               value
  #13 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #14 = Utf8               RUNTIME
  #15 = Utf8               Ljava/lang/annotation/Documented;
  #16 = Utf8               Ljava/lang/annotation/Target;
  #17 = Utf8               Ljava/lang/annotation/ElementType;
  #18 = Utf8               TYPE
  #19 = Utf8               club/throwable/annotation/Counter
  #20 = Utf8               java/lang/Object
  #21 = Utf8               java/lang/annotation/Annotation
{
  public abstract int count();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#7}
SourceFile: "Counter.java"
RuntimeVisibleAnnotations:
  0: #11(#12=e#13.#14)
  1: #15()
  2: #16(#12=[e#17.#18])

If one is familiar with bytecode, the following information can be obtained intuitively.

  • Annotation is an interface that inherits from the java.lang.annotation.Annotation parent interface.
  • @Counter corresponds to the interface interface in addition to inheriting the abstract methods in java.lang.annotation.Annotation, itself defines an abstract method public abstract int count();.

Since the annotation is finally transformed into an interface and the annotation member properties defined in the annotation are transformed into abstract methods, how are these annotation member properties finally assigned? The answer is: to generate a dynamic proxy class that implements the interface corresponding to the annotation. To put it directly, Java generates an instance that implements the “interface corresponding to the annotation” by means of a dynamic proxy, and the proxy class instance implements the “method corresponding to the annotation member property”, which is similar to the assignment process of the “annotation member property”.(Here the annotation must be visible at runtime, that is, the use of @Retention(RetentionPolicy.RUNTIME), in addition to the need to understand the JDK native dynamic proxy and reflection-related content).

Annotate the corresponding dynamic proxy class instance

Some of the above has pointed out that the lowest-level implementation of the annotation is a JDK dynamic proxy class, and the generation process of this dynamic proxy class we can completely track through Debug, here is a list of the whole process of the author’s tracking.

  1. Class<? >#getAnnotation(Class<A> annotationClass), get the annotation instance by type.
  2. Class<? >#annotationData(), get the annotation data.
  3. Class<? >#createAnnotationData(int classRedefinedCount), construct the annotation data.
  4. AnnotationParser#parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2) This method is used to parse the annotations. This step uses the index parsing of the constant pool in the byte code, and the parsing of the constants will generate a member property key-value pair as the next entry. method.
  5. AnnotationParser#annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1), also a method in sun.reflect.annotation.AnnotationParser, used to generate dynamic proxy classes for annotations.

Note step 5 and post its source code

1
2
3
4
5
6
7
   public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) {
        return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() {
            public Annotation run() {
                return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1));
            }
        });
    }

The code here should look simple if you are familiar with JDK dynamic proxies. It is to generate a standard JDK dynamic proxy class, and the instance of InvocationHandler is AnnotationInvocationHandler, you can see its member variables, constructor methods and the invoke() method that implements the InvocationHandler interface.

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class AnnotationInvocationHandler implements InvocationHandler, Serializable {

    private static final long serialVersionUID = 6182022883658399397L;
    //The type of the current annotation is saved
    private final Class<? extends Annotation> type;
    //The name of the annotated member property actually corresponds to the name of the abstract method in the interface
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

    public Object invoke(Object var1, Method var2, Object[] var3) {
        //Get the name of the currently executed method
        String var4 = var2.getName();
        Class[] var5 = var2.getParameterTypes();
        if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
            return this.equalsImpl(var3[0]);
        } else if (var5.length != 0) {
            throw new AssertionError("Too many parameters for an annotation method");
        } else {
            byte var7 = -1;
            switch(var4.hashCode()) {
            case -1776922004:
                if (var4.equals("toString")) {
                    var7 = 0;
                }
                break;
            case 147696667:
                if (var4.equals("hashCode")) {
                    var7 = 1;
                }
                break;
            case 1444986633:
                if (var4.equals("annotationType")) {
                    var7 = 2;
                }
            }
            switch(var7) {
            case 0:
                return this.toStringImpl();
            case 1:
                return this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                //Get the assignment of member properties from memberValues using the method name
                Object var6 = this.memberValues.get(var4);
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    //This step is the actual logic of annotating member properties to get the return value
                    //Need to determine if it is data, if it is data need to clone an array
                    //Not an array returns directly
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }
                    return var6;
                }
            }
        }
    }
//Ignore other methods	

Here we need to pay attention to the fact that the member variable Map<String, Object> memberValues of AnnotationInvocationHandler holds the mapping of the names and values of the annotated member properties, and the names of the annotated member properties actually correspond to the names of the abstract methods in the interface, for example For example, when the @Counter annotation we defined above generates a proxy class, the memberValues property of its AnnotationInvocationHandler instance holds the key-value pair count=1.

Now that we know that the underlying annotation uses the JDK native Proxy, we can directly export the proxy class to the specified directory to analyze the source code of the proxy class. There are two ways to export the source code of the Proxy class.

  1. Set System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); by Java system property.
  2. Specified by the -D parameter, which is actually similar to 1, with the parameter: -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true.

Here we use way 1, modifying the main method used above.

1
2
3
4
5
public static void main(String[] args) throws Exception {
    System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    Counter counter = Main.class.getAnnotation(Counter.class);
    System.out.println(counter.count());
}

After execution, an additional directory was added to the project at

Where $Proxy0 is the dynamic proxy class corresponding to the @Retention annotation, and $Proxy1 is the dynamic proxy class corresponding to our @Counter, but of course if there are more annotations, then it is possible to generate $ProxyN. Then we look directly at the source code of $Proxy1.

 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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public final class $Proxy1 extends Proxy implements Counter {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m4;
    private static Method m0;

    public $Proxy1(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int count() throws  {
        try {
            return (Integer)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final Class annotationType() throws  {
        try {
            return (Class)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("club.throwable.annotation.Counter").getMethod("count");
            m4 = Class.forName("club.throwable.annotation.Counter").getMethod("annotationType");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

Obviously, $Proxy1 implements the Counter interface, which instantiates Method instances of member methods using static blocks in the last part of the code, and caches these Methods in the previous code, which are directly delegated to InvocationHandler( AnnotationInvocationHandler) instance to complete the call. When we analyze AnnotationInvocationHandler, we see that it only uses the name of Method to match the result of the member method from Map, which is nearly as efficient as getting Value from the Map instance by Key.

Summary

Now that we know the underlying principle of annotation, we can write an “annotation interface” and an InvocationHandler implementation to simply simulate the whole process. First, define an interface.

1
2
3
4
public interface CounterAnnotation extends Annotation {

	int count();
}

Simple implementation of InvocationHandler.

 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
public class CounterAnnotationInvocationHandler implements InvocationHandler {

	private final Map<String, Object> memberValues;
	private final Class<? extends Annotation> clazz;

	public CounterAnnotationInvocationHandler(Map<String, Object> memberValues, Class<? extends Annotation> clazz) {
		this.memberValues = memberValues;
		this.clazz = clazz;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		String methodName = method.getName();
		Object value;
		switch (methodName) {
			case "toString":
				value = super.toString();
				break;
			case "hashCode":
				value = super.hashCode();
				break;
			case "equals":
				value = super.equals(args[0]);
				break;
			case "annotationType":
				value = clazz;
				break;
			default:
				value = memberValues.get(methodName);
		}
		return value;
	}
}

Write a main method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class CounterAnnotationMain {

  public static void main(String[] args) throws Exception{
      //The process of resolving annotated member properties from a pool of constants is simulated here
      Map<String,Object> values = new HashMap<>(8);
      values.put("count", 1);
      //Generate proxy classes
      CounterAnnotation proxy = (CounterAnnotation)Proxy.newProxyInstance(CounterAnnotationMain.class.getClassLoader(),
              new Class[]{CounterAnnotation.class},
              new CounterAnnotationInvocationHandler(values, CounterAnnotation.class));
      System.out.println(proxy.count());
  }
}
//Console output after running:1

Reference: https://www.throwx.cn/2020/03/16/annotation-implementation/