java source code analysis - runtime annotation processor

Posted by kaushikgotecha on Thu, 17 Feb 2022 19:27:37 +0100

java source code analysis - runtime annotation processor

In the previous "java source code analysis - basic principles of annotation", we learned some basic concepts and principles of annotation. At the end of the article, we also clicked on the annotation processor. At that time, I said that annotation information is actually obtained through reflection, which is not completely correct. In this article, we will discuss the annotation processor in detail.

1. Annotation processor

As the name suggests, annotation processor is used to process annotation information. If there is no annotation processor, annotation is not much different from annotation. It is the existence of annotation processor that makes annotations work. So how is java implemented? Java annotation processing is generally divided into two types. The most common and explicit is the annotation implementation of Spring and Spring Boot. When the runtime container starts, the class is scanned according to the annotation and loaded into the Spring container, which is called the runtime annotation processor. The other one to be introduced is called compile time annotation processor, which is used to process the Java syntax tree generated before java file compilation through the API provided by JDK during compile time to realize the desired function. This film mainly introduces the main principle of runtime annotation processor.

2. Runtime annotation processor

We know that the runtime annotation processor dynamically obtains annotation information at runtime through the reflection mechanism. Here, a precondition is that the value of the meta annotation @ Retention of the defined annotation must be retentionpolicy Runtime, that is, the life cycle of the annotation must be runtime, otherwise it cannot be obtained through reflection.

Let's go deeper with an example:

Customize an annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Personname {

    String name() default "";

}

Then we get annotation information through reflection:

public class PersonNameTest {
    public static void main(String[] args) throws NoSuchMethodException {
        //Get getPerson Method through Class object
        Method method = PersonNameTest.class.getDeclaredMethod("getPerson", null);
        //Get the annotation on the Method according to the Method
        Personname annotation = method.getAnnotation(Personname.class);
        System.out.println(annotation);
        String name = annotation.name();
        System.out.println(name);
        Class<? extends Annotation> aClass = annotation.annotationType();
        System.out.println(aClass);
    }

    @Personname
    public static String getPerson(){
        return "";
    }
}

//Operation results:
//@test.java.lang.annotation.Personname(name=)		
//                 									(* here is an empty string)
//interface test.java.lang.annotation.Personname 		 (* Note the type of annotation here)

You can see that we passed the method Getannotation (personname. Class) obtains an instance annotation of personname type, and then calls the name() of a similar method through this instance object to obtain the default value (""), and then through annotation Annotationtype () gets the type of this annotation instance, and the print result is: interface test java. lang.annotation. Personname.

I don't know if you will have any questions here. Anyway, I'm confused. As we said above, isn't the Annotation type similar to the interface, or the interface of the interface inherits the Annotation interface at the bottom, and the basic principle should be similar to the interface. Unless there is a subclass implementation of the interface, you can't instantiate an instance? Why did you get the instance through reflection? Also call the name method of this instance. What's going on?

Don't panic. To find out what annotations are, we can take a detailed look by generating bytecode files. We compile personname Class passed

javap -c -v Personname.class

Command to decompile:

It is suggested to save the image directly from the external link (typ-1030vpnig-1030image-1030vpn2) (typ-1030image-1030vpnv-image-1030vpn2) (the image may be uploaded directly from the external link)

Through the above, we can intuitively see two main information:

(1) Annotation is an interface that inherits Java lang.annotation. Annotation interface;

(2) The interface that the annotation actually corresponds to defines an abstract name() method.

Since the annotation is transformed into an interface at the bytecode level, how is it instantiated? How is the annotation element of the annotation assigned (the default value is "")? The problem is still unresolved? No way, then we can only Debug.

Through Debug, we see an amazing sign on the console: $Proxy, what's this? This is the Proxy. That is to say, the annotation instance we see is not the real person name annotation type, but the Proxy implementation class that implements the interface corresponding to the person name annotation is obtained through the Proxy.

[the external link image transfer fails. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-mbral0xm-1615020372564) (C: \ users \ viruser. V-desktop \ appdata \ roaming \ typora \ typora user images \ image-20210306105427671. PNG)]

To put it bluntly, Java generates an instance that implements the "annotation corresponding interface" through dynamic proxy, and the proxy class instance implements the "annotation attribute corresponding method". This step is similar to the assignment process of "annotation member attribute", so that the child can obtain the annotation member attribute through reflection when the program is running.

After knowing the overall implementation idea, the rest is to see how java realizes the above steps step by step through the Debug source code.

Due to the complexity of the whole process of debugging source code, here are some important steps to explain:

(1)getAnnotation(Class annotationClass)

Get annotation instance through Class type

----------------Method class
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
        return super.getAnnotation(annotationClass);
    }



----------------Parent class Executable class
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
        Objects.requireNonNull(annotationClass);	//Null verification, class is required to be non null
        return annotationClass.cast(declaredAnnotations().get(annotationClass));
    }

This is our entry. It is through this method that we finally get the instance of annotation.

(2)declaredAnnotations()

This method returns a Map, key is the class of the class that inherits the Annotation interface, and value is the Annotation object. In other words, this method will generate a Map collection of the corresponding relationship between the claass and the Annotation object. The implementation class object of the Annotation can be obtained by passing in the class of the Annotation class (of course, it must be a proxy class object).

private transient Map<Class<? extends Annotation>, Annotation> declaredAnnotations;//Map collection

private synchronized  Map<Class<? extends Annotation>, Annotation> declaredAnnotations() {
        if (declaredAnnotations == null) {
            Executable root = getRoot();
            if (root != null) {
                declaredAnnotations = root.declaredAnnotations();
            } else {
                declaredAnnotations = AnnotationParser.parseAnnotations(	//Analyze the data and assemble the generated Map
                    getAnnotationBytes(),
                    sun.misc.SharedSecrets.getJavaLangAccess().
                    getConstantPool(getDeclaringClass()),
                    getDeclaringClass());
            }
        }
        return declaredAnnotations;
    }

Declaraedannotations() calls annotationparser Parseannotations () method, and pass in the byte value of the annotation, the access permission of java, JavaLangAccess, the constant pool information of the annotation and the bytecode object of the annotation. The Map combination is assembled through these information.

(3)parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2)

Call the parseAnnotations method of the AnnotationParser class and return map < class <? Extensions annotation >, annotation > set.

public static Map<Class<? extends Annotation>, Annotation> parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2) {
        if (var0 == null) {
            return Collections.emptyMap();
        } else {
            try {
                return parseAnnotations2(var0, var1, var2, (Class[])null);
            } catch (BufferUnderflowException var4) {
                throw new AnnotationFormatError("Unexpected end of annotations.");
            } catch (IllegalArgumentException var5) {
                throw new AnnotationFormatError(var5);
            }
        }
    }

private static Map<Class<? extends Annotation>, Annotation> parseAnnotations2(byte[] var0, ConstantPool var1, Class<?> var2, Class<? extends Annotation>[] var3) {
        LinkedHashMap var4 = new LinkedHashMap();	//Create a LinkedHashMap collection
        ByteBuffer var5 = ByteBuffer.wrap(var0);	//Cache buffer
        int var6 = var5.getShort() & '\uffff';

        for(int var7 = 0; var7 < var6; ++var7) {
            Annotation var8 = parseAnnotation2(var5, var1, var2, false, var3);//*Generate annotation instance
            if (var8 != null) {
                Class var9 = var8.annotationType();
                if (AnnotationType.getInstance(var9).retention() == RetentionPolicy.RUNTIME && var4.put(var9, var8) != null) {
                    throw new AnnotationFormatError("Duplicate annotation for class: " + var9 + ": " + var8);
                }
            }
        }
    
private static Annotation parseAnnotation2(ByteBuffer var0, ConstantPool var1, Class<?> var2, boolean var3, Class<? extends Annotation>[] var4) {
        int var5 = var0.getShort() & '\uffff';
        Class var6 = null;
        String var7 = "[unknown]";

        try {
            try {
                var7 = var1.getUTF8At(var5);
                var6 = parseSig(var7, var2);
            } catch (IllegalArgumentException var18) {
                var6 = var1.getClassAt(var5);
            }
        } catch (NoClassDefFoundError var19) {
            if (var3) {
                throw new TypeNotPresentException(var7, var19);
            }

            skipAnnotation(var0, false);
            return null;
        } catch (TypeNotPresentException var20) {
            if (var3) {
                throw var20;
            }

            skipAnnotation(var0, false);
            return null;
        }

        if (var4 != null && !contains(var4, var6)) {
            skipAnnotation(var0, false);
            return null;
        } else {
            AnnotationType var8 = null;

            try {
                var8 = AnnotationType.getInstance(var6);//Get annotation instance according to annotation type
            } catch (IllegalArgumentException var17) {
                skipAnnotation(var0, false);
                return null;
            }

            Map var9 = var8.memberTypes();
            LinkedHashMap var10 = new LinkedHashMap(var8.memberDefaults());
            int var11 = var0.getShort() & '\uffff';

            for(int var12 = 0; var12 < var11; ++var12) {
                int var13 = var0.getShort() & '\uffff';
                String var14 = var1.getUTF8At(var13);
                Class var15 = (Class)var9.get(var14);
                if (var15 == null) {
                    skipMemberValue(var0);
                } else {
                    Object var16 = parseMemberValue(var15, var0, var1, var2);
                    if (var16 instanceof AnnotationTypeMismatchExceptionProxy) {
                        ((AnnotationTypeMismatchExceptionProxy)var16).setMember((Method)var8.members().get(var14));
                    }

                    var10.put(var14, var16);
                }
            }

            return annotationForMap(var6, var10);//Returns the annotation solution instance of the corresponding type
        }
    }    

The source code is a little long. We just need to get the main idea. Here, we mainly parse the bytecode through the parseAnnotation2 method, and then call annotationtype according to the Annotation type Class GetInstance (VAR6) to get the instance of Annotation.

(4)getInstance(Class<? extends Annotation> var0)

This is the method of AnnotationParser, which is used to obtain the annotation type AnnotationType instance. Internally, the bytecode information, that is, the information in the constant pool, is parsed and then assembled.

public static AnnotationType getInstance(Class<? extends Annotation> var0) {
        JavaLangAccess var1 = SharedSecrets.getJavaLangAccess();
        AnnotationType var2 = var1.getAnnotationType(var0);
        if (var2 == null) {
            var2 = new AnnotationType(var0);	//New AnnotationType object
            if (!var1.casAnnotationType(var0, (AnnotationType)null, var2)) {
                var2 = var1.getAnnotationType(var0);

                assert var2 != null;
            }
        }

        return var2;
    }
    
    private AnnotationType(final Class<? extends Annotation> var1) {
        if (!var1.isAnnotation()) {
            throw new IllegalArgumentException("Not an annotation type");
        } else {
            Method[] var2 = (Method[])AccessController.doPrivileged(new PrivilegedAction<Method[]>() {
                public Method[] run() {
                    return var1.getDeclaredMethods();//Getting the method object in the annotation is actually the annotation element object in the annotation
                }
            });
            this.memberTypes = new HashMap(var2.length + 1, 1.0F);//Member type Map
            this.memberDefaults = new HashMap(0);				//Member default Map
            this.members = new HashMap(var2.length + 1, 1.0F);	 //Member Map
            Method[] var3 = var2;
            int var4 = var2.length;

            for(int var5 = 0; var5 < var4; ++var5) {//Traverse the Method [], and get the relevant information of each Method: name, type, return value type and return value
                Method var6 = var3[var5];
                if (Modifier.isPublic(var6.getModifiers()) && Modifier.isAbstract(var6.getModifiers()) && !var6.isSynthetic()) {
                    if (var6.getParameterTypes().length != 0) {
                        throw new IllegalArgumentException(var6 + " has params");
                    }

                    String var7 = var6.getName();
                    Class var8 = var6.getReturnType();
                    this.memberTypes.put(var7, invocationHandlerReturnType(var8));
                    this.members.put(var7, var6);
                    Object var9 = var6.getDefaultValue();//Gets the default value of the member property
                    if (var9 != null) {
                        this.memberDefaults.put(var7, var9);
                    }
                }
            }

            if (var1 != Retention.class && var1 != Inherited.class) {
                JavaLangAccess var10 = SharedSecrets.getJavaLangAccess();
                Map var11 = AnnotationParser.parseSelectAnnotations(var10.getRawClassAnnotations(var1), var10.getConstantPool(var1), var1, new Class[]{Retention.class, Inherited.class});
                Retention var12 = (Retention)var11.get(Retention.class);
                this.retention = var12 == null ? RetentionPolicy.CLASS : var12.value();
                this.inherited = var11.containsKey(Inherited.class);
            } else {
                this.retention = RetentionPolicy.RUNTIME;
                this.inherited = false;
            }

        }
    }

(5)newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

This method is used to obtain the proxy class object instance.

----------------AnnotationParser
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));
        }
    });
}

-----------------Proxy class
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

Here it is clear that it is based on the annotation information obtained above, including the annotation name, type, default value, etc., and then invokes the Proxy agent to get the final annotation agent class. To be clear, the call of the dynamic proxy method will eventually be passed to the invoke method of the bound InvocationHandler instance.

[the external chain image transfer fails, and the source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-g0jwkrno-1615020372566) (C: \ users \ viruser. V-desktop \ appdata \ roaming \ typora \ typora user images \ image-20210306150815782. PNG)]

(6)invoke(Object var1, Method var2, Object[] var3)

Familiar with JDK dynamic agent, the code here should look very simple, that is, to generate a standard JDK dynamic agent, and the instance of InvocationHandler is AnnotationInvocationHandler. You can see its invoke method of InvocationHandler interface:

public Object invoke(Object var1, Method var2, Object[] var3) {
        String var4 = var2.getName();//Gets the name of the currently executed method
        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:
                Object var6 = this.memberValues.get(var4); //Get the assignment of member attribute from memberValues with method name
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {//This step is the actual logic to get the return value of the annotation member attribute
                        var6 = this.cloneArray(var6);//You need to judge whether the data is. If it is data, you need to clone an array
                    }

                    return var6;
                }
            }
        }
    }

The last step is to return. In this way, we finally get the proxy implementation class of this annotation type.

We simplify the above steps as follows:

(1) Get the bytecode information of the annotation class. The annotation is required to be visible at runtime @ Target(ElementType.METHOD);

(2) Parse the annotation bytecode information and generate map < < annotation Class, annotation instance annotation >

(3) Parse the bytecode and construct an AnnotationType instance. This instance contains the relevant information of the annotation and its annotation elements, which will be passed as parameters;

(4) Through the JDK co agent, according to the information of the obtained annotation and its annotation elements, finally call the invoke method of AnnotationInvocationHandler to realize the proxy implementation class that finally obtains the annotation.

Topics: JDK