Java註解(Annotation)原理詳解

序言

註解在Java中到底是什麼樣的東西?具體是如何實現的?
本文將一層一層深入探究註解的實現原理。爲了儘可能的將分析的過程呈現出來,所以文章包含了大量的截圖和代碼。(ps:如果圖片看不清楚,請將網頁放大來看,chrome可以通過ctrl+鼠標滾輪放大


前期準備

知識方面

開始分析前,提醒一下,下面的分析必須具備以下知識
1. 知道如何自定義註解
2. 理解Java動態代理機制
3. 瞭解Java常量池
如果不具備以上的知識,會看得雲裏霧裏的。上面提到的知識點谷歌百度都可以找到許多相關的文章。

工具方面

  1. Intellij 2016

開始分析

首先寫一個簡單的自定義註解小程序。

先自定義一個運行時註解

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

    String say() default "Hi";

}

然後在Main函數中解析註解

@HelloAnnotation(say = "Do it!")
public class TestMain {
    public static void main(String[] args) {
        HelloAnnotation annotation = TestMain.class.getAnnotation(HelloAnnotation.class);//獲取TestMain類上的註解對象
        System.out.println(annotation.say());//調用註解對象的say方法,並打印到控制檯
    }
}

運行程序,輸出結果如下:

Do it!

下面將圍繞上面的代碼來研究Java註解(Annotation)的實現原理


1. 註解對象具體是什麼?

首先,我們先在main函數第一行斷點,看看HelloAnnotation具體是什麼類的對象

註解

可以看到HelloAnnotation註解的實例是jvm生成的動態代理類的對象。

這個運行時生成的動態代理對象是可以導出到文件的,方法有兩種

  1. 在代碼中加入System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
  2. 在運行時加入jvm 參數 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

這裏使用第一種,↓

這裏寫圖片描述

然後運行程序。

這裏寫圖片描述

可以看到,已經導出了運行時生成的代理類。↑

HelloAnnotation的動態代理類是$Proxy1.class,Intellij自帶了反編譯工具,直接雙擊打開,得到如下的Java代碼

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.sun.proxy;

import com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy1 extends Proxy implements HelloAnnotation {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m3;
    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})).booleanValue();
        } 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 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 String say() throws  {
        try {
            return (String)super.h.invoke(this, m3, (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)).intValue();
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m4 = Class.forName("com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation").getMethod("annotationType", new Class[0]);
            m3 = Class.forName("com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation").getMethod("say", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

從第14行我們可以看到,我們自定義的註解HelloAnnotation是一個接口,而$Proxy1這個Java生成的動態代理類就是它的實現類

我們接着看一下HelloAnnotation的字節碼

 $ javap -verbose HelloAnnotation 
Warning: Binary file HelloAnnotation contains com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation
Classfile /home/kevin/Workspace/IdeaProjects/JavaLearn/out/production/JavaLearn/com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation.class
  Last modified Aug 6, 2016; size 496 bytes
  MD5 checksum a6c87f863669f6ab9050ffa310160ea5
  Compiled from "HelloAnnotation.java"
public interface com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #18            // com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
   #2 = Class              #19            // java/lang/Object
   #3 = Class              #20            // java/lang/annotation/Annotation
   #4 = Utf8               say
   #5 = Utf8               ()Ljava/lang/String;
   #6 = Utf8               AnnotationDefault
   #7 = Utf8               Hi
   #8 = Utf8               SourceFile
   #9 = Utf8               HelloAnnotation.java
  #10 = Utf8               RuntimeVisibleAnnotations
  #11 = Utf8               Ljava/lang/annotation/Target;
  #12 = Utf8               value
  #13 = Utf8               Ljava/lang/annotation/ElementType;
  #14 = Utf8               TYPE
  #15 = Utf8               Ljava/lang/annotation/Retention;
  #16 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #17 = Utf8               RUNTIME
  #18 = Utf8               com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/annotation/Annotation
{
  public abstract java.lang.String say();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: s#7}
SourceFile: "HelloAnnotation.java"
RuntimeVisibleAnnotations:
  0: #11(#12=[e#13.#14])
  1: #15(#12=e#16.#17)

看到第7行。很明顯,HelloAnnotation就是繼承了Annotation的接口。再看第10行,flag字段中,我們可以看到,有個ACC_ANNOTATION標記,說明是一個註解,所以註解本質是一個繼承了Annotation的特殊接口。

而Annotation接口聲明瞭以下方法。

package java.lang.annotation;

public interface Annotation {
    boolean equals(Object var1);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}

這些方法,已經被$Proxy1實現了。(這就是動態代理的機制)

小結

現在我們知道了HelloAnnotation註解(接口)是一個繼承了Annotation接口的特殊接口,而我們通過反射獲取註解時,返回的是Java運行時生成的動態代理對象$Proxy1,該類就是HelloAnnotation註解(接口)的具體實現類。


2. 動態代理類$Proxy1是如何處理annotation.say()方法的調用?

無論是否瞭解動態代理,這裏只需要明確一點,動態代理方法的調用最終會傳遞給綁定的InvocationHandler實例的invoke方法處理。我們可以看看源碼

$Proxy1.java

public final class $Proxy1 extends Proxy implements HelloAnnotation {
   .....
   public final String say() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    ....
}

Proxy.java

public class Proxy implements java.io.Serializable {

    /**
     * the invocation handler for this proxy instance.
     * @serial
     */
    protected InvocationHandler h;

從上面不難看出,say方法最終會執行(String)super.h.invoke(this, m3, (Object[])null);,而這其中的h對象類型就是InvocationHandler接口的某個實現類

斷點調試,看看InvocationHandler具體實現類是哪個。

這裏寫圖片描述

可以看到h對象是AnnotationInvocationHandler的實例。讓我們來看看該實現類的invoke方法。

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    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) {
        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 Integer.valueOf(this.hashCodeImpl());
            case 2:
                return this.type;
            default:
                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 {
                    if(var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }
    .......
}

我們直接從invoke方法第一行開始單步調試,看看invoke方法是如何處理我們annotation.say()方法的調用的。

這裏再貼一次測試代碼,不然就得翻到前面了

@HelloAnnotation(say = "Do it!")
public class TestMain {
    public static void main(String[] args) {
        HelloAnnotation annotation = TestMain.class.getAnnotation(HelloAnnotation.class);
        System.out.println(annotation.say());
    }
}

這裏寫圖片描述

可以看到,say方法的返回值是從一個Map中獲取到的。這個map以key(註解方法名)—value(註解方法對應的值)存儲TestMain類上的註解

那memberValues這個Map對象是怎麼生成的,繼續調試
通過方法調用棧找到memberValues的本源

這裏寫圖片描述

我們繼續跟進parseMemberValue()方法

這裏寫圖片描述

在parseMemberValue()中會調用parseConst方法,繼續跟進到parseConst方法

這裏寫圖片描述

可以看到,memberValues是通過常量池獲取到,return var2.getUTF8At(var3);中的var3就是常量池中的序號。繼續執行返回到parseMemberValue()方法

這裏寫圖片描述

可以看到獲取的就是我們定義在TestMain類上註解的say的值——“Do it!”

這裏可以通過javap -verbose TestMain查看TestMain字節碼中的常量池

$ javap -verbose TestMain                                           
Warning: Binary file TestMain contains com.kevin.java.annotation.runtimeAnnotation.TestMain
Classfile /home/kevin/Workspace/IdeaProjects/JavaLearn/out/production/JavaLearn/com/kevin/java/annotation/runtimeAnnotation/TestMain.class
  Last modified Aug 10, 2016; size 1117 bytes
  MD5 checksum 610b7176c7dfdad08bc4862247df7123
  Compiled from "TestMain.java"
public class com.kevin.java.annotation.runtimeAnnotation.TestMain
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool://常量池
   #1 = Methodref          #11.#30        // java/lang/Object."<init>":()V
   #2 = String             #31            // sun.misc.ProxyGenerator.saveGeneratedFiles
   #3 = String             #32            // true
   #4 = Methodref          #33.#34        // java/lang/System.setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
   #5 = Class              #35            // com/kevin/java/annotation/runtimeAnnotation/TestMain
   #6 = Class              #36            // com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
   #7 = Methodref          #37.#38        // java/lang/Class.getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
   #8 = Fieldref           #33.#39        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = InterfaceMethodref #6.#40         // com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation.say:()Ljava/lang/String;
  #10 = Methodref          #41.#42        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #43            // java/lang/Object
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lcom/kevin/java/annotation/runtimeAnnotation/TestMain;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               annotation
  #24 = Utf8               Lcom/kevin/java/annotation/runtimeAnnotation/HelloAnnotation;
  #25 = Utf8               SourceFile
  #26 = Utf8               TestMain.java
  #27 = Utf8               RuntimeVisibleAnnotations
  #28 = Utf8               say
  #29 = Utf8               Do it!
  #30 = NameAndType        #12:#13        // "<init>":()V
  #31 = Utf8               sun.misc.ProxyGenerator.saveGeneratedFiles
  #32 = Utf8               true
  #33 = Class              #44            // java/lang/System
  #34 = NameAndType        #45:#46        // setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
  #35 = Utf8               com/kevin/java/annotation/runtimeAnnotation/TestMain
  #36 = Utf8               com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
  #37 = Class              #47            // java/lang/Class
  #38 = NameAndType        #48:#49        // getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
  #39 = NameAndType        #50:#51        // out:Ljava/io/PrintStream;
  #40 = NameAndType        #28:#52        // say:()Ljava/lang/String;
  #41 = Class              #53            // java/io/PrintStream
  #42 = NameAndType        #54:#55        // println:(Ljava/lang/String;)V
  #43 = Utf8               java/lang/Object
  #44 = Utf8               java/lang/System
  #45 = Utf8               setProperty
  #46 = Utf8               (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
  #47 = Utf8               java/lang/Class
  #48 = Utf8               getAnnotation
  #49 = Utf8               (Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
  #50 = Utf8               out
  #51 = Utf8               Ljava/io/PrintStream;
  #52 = Utf8               ()Ljava/lang/String;
  #53 = Utf8               java/io/PrintStream
  #54 = Utf8               println
  #55 = Utf8               (Ljava/lang/String;)V
{
  public com.kevin.java.annotation.runtimeAnnotation.TestMain();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/kevin/java/annotation/runtimeAnnotation/TestMain;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #2                  // String sun.misc.ProxyGenerator.saveGeneratedFiles
         2: ldc           #3                  // String true
         4: invokestatic  #4                  // Method java/lang/System.setProperty:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
         7: pop
         8: ldc           #5                  // class com/kevin/java/annotation/runtimeAnnotation/TestMain
        10: ldc           #6                  // class com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
        12: invokevirtual #7                  // Method java/lang/Class.getAnnotation:(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
        15: checkcast     #6                  // class com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
        18: astore_1
        19: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        22: aload_1
        23: invokeinterface #9,  1            // InterfaceMethod com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation.say:()Ljava/lang/String;
        28: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        31: return
      LineNumberTable:
        line 13: 0
        line 14: 8
        line 15: 19
        line 16: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
           19      13     1 annotation   Lcom/kevin/java/annotation/runtimeAnnotation/HelloAnnotation;
}
SourceFile: "TestMain.java"
RuntimeVisibleAnnotations:
  0: #24(#28=s#29)

仔細看第40行#29 = Utf8 Do it!,可以看到#29與var3的29對應(也就常量池的索引),對應的值就是Do it!

以上就是say方法調用的細節。

總結

註解本質是一個繼承了Annotation的特殊接口,其具體實現類是Java運行時生成的動態代理類。通過代理對象調用自定義註解(接口)的方法,會最終調用AnnotationInvocationHandler的invoke方法。該方法會從memberValues這個Map中索引出對應的值。而memberValues的來源是Java常量池。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章