1. 程式人生 > >JDK中註解的底層實現

JDK中註解的底層實現

前提

用Java快三年了,註解算是一個常用的型別,特別是在一些框架裡面會大量使用註解做元件標識、配置或者策略。但是一直沒有深入去探究JDK中的註解到底是什麼,底層是怎麼實現了?於是參考了一些資料,做了一次稍微詳細的分析。

JDK的註解描述

參考JavaSE-8裡面的JLS-9.6對註解的描述如下:

anno-1

註解的宣告如下:

{InterfaceModifier} @ interface Identifier AnnotationTypeBody

介面修飾符 @ interface 註解識別符號 註解型別的內容

其中:

  • 註解型別宣告中的識別符號指定了註解型別的名稱。
  • 如果註解型別與它的任何封閉類或介面具有相同的簡單名稱,則編譯時會出現錯誤。
  • 每個註解型別的直接父介面都是java.lang.annotation.Annotation

既然所有註解型別的父介面都是java.lang.annotation.Annotation,那麼我們可以看一下Annotation介面的文件:

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

JavaSE-8中的文件對Annotation的描述和JLS-9.6中差不多,不過最後指明瞭可重複註解的相容性考慮的問題,可重複主機在JDK1.8中由元註解@Repeatable實現。下面基於JDK8的最後一個版本java version 1.8.0_181探究一下註解在JDK中的底層實現。

註解實現探究

我們先定義一個十分簡單的Counter註解如下:

package club.throwable.annotation;

import java.lang.annotation.*;

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

    int count() default 0;
}

我們先從直接使用@Counter註解,從直觀上觀察@Counter例項的型別:

@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());
    }
}

anno-2

@Counter例項從Debug過程中觀察發現是JDK的一個代理類(並且InvocationHandler的例項是sun.reflect.annotation.AnnotationInvocationHandler,它是一個修飾符為default的sun包內可用的類),為了驗證這一點我們使用JDK的反編譯命令檢視@Counter的位元組碼:

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

@Counter反編譯後的位元組碼如下:

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])

如果熟悉位元組碼,從直觀上可以得到下面的資訊:

  • 註解是一個介面,它繼承自java.lang.annotation.Annotation父介面。
  • @Counter對應的介面介面除了繼承了java.lang.annotation.Annotation中的抽象方法,自身定義了一個抽象方法public abstract int count();

既然註解最後轉化為一個介面,註解中定義的註解成員屬性會轉化為抽象方法,那麼最後這些註解成員屬性怎麼進行賦值的呢?答案就是:為註解對應的介面生成一個實現該介面的動態代理類。直接點說就是:Java通過動態代理的方式生成了一個實現了"註解對應介面"的例項,該代理類例項實現了"註解成員屬性對應的方法",這個步驟類似於"註解成員屬性"的賦值過程,這樣子就可以在程式執行的時候通過反射獲取到註解的成員屬性(這裡註解必須是執行時可見的,也就是使用了@Retention(RetentionPolicy.RUNTIME),另外需要理解JDK原生動態代理和反射相關內容)。

註解對應的動態代理類例項

上面一些已經指出了,註解的最底層實現就是一個JDK的動態代理類,而這個動態代理類的生成過程我們完全可以通過Debug跟蹤,這裡列舉一下筆者跟蹤整個過程的流水賬:

  • 1、Class<?>#getAnnotation(Class<A> annotationClass),通過型別獲取註解例項。
  • 2、Class<?>#annotationData(),獲取註解的資料。
  • 3、Class<?>#createAnnotationData(int classRedefinedCount),構建註解的資料。
  • 4、AnnotationParser#parseAnnotations(byte[] var0, ConstantPool var1, Class<?> var2),這裡已經是sun包下的類,無法看到原始碼,這個方法用於解析註解,這一步使用到位元組碼中常量池的索引解析,常量解析完畢會生成一個成員屬性鍵值對作為下一個環節的入參,常量池的解析可以看AnnotationParser#parseMemberValue方法
  • 5、AnnotationParser#annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1),同樣是sun.reflect.annotation.AnnotationParser中的方法,用於生成註解的動態代理類。

注意第5步,貼出它的原始碼:

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

屬性JDK動態代理的這裡的程式碼應該看起來很簡單,就是生成一個標準的JDK動態代理,而InvocationHandler的例項是AnnotationInvocationHandler,可以看它的成員變數、構造方法和實現InvocationHandler介面的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 this.hashCodeImpl();
            case 2:
                return this.type;
            default:
                //利用方法名稱從memberValues獲取成員屬性的賦值
                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;
                }
            }
        }
    }
//忽略其他方法    

這裡需要重點注意一下的是:AnnotationInvocationHandler的成員變數Map<String, Object> memberValues存放著註解的成員屬性的名稱和值的對映,註解成員屬性的名稱實際上就對應著介面中抽象方法的名稱,例如上面我們定義的@Counter註解生成代理類後,它的AnnotationInvocationHandler例項中的memberValues屬性存放著鍵值對count=1

既然知道了註解底層使用了JDK原生的Proxy,那麼我們可以直接輸出代理類到指定目錄去分析代理類的原始碼,有兩種方式可以輸出Proxy類的原始碼:

  • 1、通過Java系統屬性設定System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
  • 2、通過-D引數指定,其實跟1差不多,引數是:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

這裡使用方式1,修改一下上面用到的main方法:

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

執行完畢之後,專案中多了一個目錄:

anno-3

其中$Proxy0是@Retention註解對應的動態代理類,而$Proxy1才是我們的@Counter對應的動態代理類,當然如果有更多的註解,那麼有可能生成$ProxyN。接著我們直接看$Proxy1的原始碼:

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

顯然,$Proxy實現了Counter介面,它在程式碼的最後部分使用了靜態程式碼塊例項化了成員方法的Method例項,在前面的程式碼對這些Method進行了快取,在呼叫成員方法的時候都是直接委託到InvocationHandler(AnnotationInvocationHandler)例項完成呼叫。我們在分析AnnotationInvocationHandler的時候看到,它只用到了Method的名稱從Map從匹配出成員方法的結果,因此呼叫過程並不是反射呼叫,反而是直接的呼叫,效率類似於通過Key從Map例項中獲取Value一樣,是十分高效的。

小結

既然知道了註解的底層原理,我們可以編寫一個"註解介面"和InvocationHandler實現來簡單模擬整個過程。先定義一個介面:

public interface CounterAnnotation extends Annotation {

    int count();
}

InvocationHandler的簡單實現:

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

編寫一個main方法:

public class CounterAnnotationMain {

    public static void main(String[] args) throws Exception{
        //這裡模擬了註解成員屬性從常量池解析的過程
        Map<String,Object> values = new HashMap<>(8);
        values.put("count", 1);
        //生成代理類
        CounterAnnotation proxy = (CounterAnnotation)Proxy.newProxyInstance(CounterAnnotationMain.class.getClassLoader(),
                new Class[]{CounterAnnotation.class},
                new CounterAnnotationInvocationHandler(values, CounterAnnotation.class));
        System.out.println(proxy.count());
    }
}
//執行後控制檯輸出:1

(本文完 c-1-d e-20181006)