­

關於註解的思考

關於註解的一點點思考

簡介

Java註解是在JDK1.5被引入的技術,配合反射可以在運行期間處理註解,配合apt tool可以在編譯器處理註解,在JDK1.6之後,apt tool被整合到了javac裡面。

什麼是註解

註解其實就是一種標記,常常用於代替冗餘複雜的配置(XML、properties)又或者是編譯器進行一些檢查如JDK自帶的Override、Deprecated等,但是它本身並不起任何作用,可以說有它沒它都不影響程式的正常運行,註解的作用在於註解的處理程式,註解處理程式通過捕獲被註解標記的程式碼然後進行一些處理,這就是註解工作的方式。

在java中,自定義一個註解非常簡單,通過@interface就能定義一個註解,實現如下

public @interface PrintMsg {
}

寫個測試類給他加上我們寫的這個註解吧

@PrintMsg
public class AnnotationTest {
    public static void main(String[] args) {
        System.out.println("annotation test OK!");
    }
}

我們發現寫與不寫這個註解的效果是相同的,這也印證了我們說的註解只是一種標記,有它沒它並不影響程式的運行。

元註解

在實現這個註解功能之前,我們先了解一下元註解。

元註解:對註解進行註解,也就是對註解進行標記,元註解的背後處理邏輯由apt tool提供,對註解的行為做出一些限制,例如生命周期,作用範圍等等。

@Retention

用於描述註解的生命周期,表示註解在什麼範圍有效,它有三個取值,如下表所示:

類型 作用
SOURCE 註解只在源碼階段保留,在編譯器進行編譯的時候這類註解被抹除,常見的@Override就屬於這種註解
CLASS 註解在編譯期保留,但是當Java虛擬機載入class文件時會被丟棄,這個也是@Retention的默認值。@Deprecated和@NonNull就屬於這樣的註解
RUNTIME 註解在運行期間仍然保留,在程式中可以通過反射獲取,Spring中常見的@Controller、@Service等都屬於這一類

@Target

用於描述註解作用的對象類型,這個就非常多了,如下表所示:

類型 作用的對象類型
TYPE 類、介面、枚舉
FIELD 類屬性
METHOD 方法
PARAMETER 參數類型
CONSTRUCTOR 構造方法
LOCAL_VARIABLE 局部變數
ANNOTATION_TYPE 註解
PACKAGE
TYPE_PARAMETER 1.8之後,泛型
TYPE_USE 1.8之後,除了PACKAGE之外任意類型

@Documented

將註解的元素加入Javadoc中

@Inherited

如果被這個註解標記了,被標記的類、介面會繼承父類、介面的上面的註解

@Repeatable

表示該註解可以重複標記

註解的屬性

除了元註解之外,我們還能給註解添加屬性,註解中的屬性以無參方法的形式定義,方法名為屬性名,返回值為成員變數的類型,還是以上述註解為例:

首先給這個註解加億點點細節,生命周期改為Runtime,使得運行期存在可以被我們獲取

@Retention(RetentionPolicy.RUNTIME)
public @interface PrintMsg {
    int count() default 1;
    String name() default "my name is PrintMsg";
}

@PrintMsg(count = 2020)
public class AnnotationTest {
    public static void main(String[] args) {
        //通過反射獲取該註解
        PrintMsg annotation = AnnotationTest.class.getAnnotation(PrintMsg.class);
        System.out.println(annotation.count());
        System.out.println(annotation.name());
    }
}

輸出如下:

2020
my name is PrintMsg

到這裡就有兩個疑問了:

  1. getAnnotation獲取到的是什麼?一個實例?註解是一個類?
  2. 我們明明調用的是count(),name(),但是為什麼說是註解的屬性?

等下聊

到底什麼是註解?

按照註解的生命周期以及處理方式的不同,通常將註解分為運行時註解編譯時註解

  • 運行時註解的本質是實現了Annotation介面的特殊介面,JDK在運行時為其創建代理類,註解方法的調用實際是通過AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中維護了一個Map,Map中存放的是方法名與返回值的映射,對註解中自定義方法的調用其實最後就是用方法名去查Map並且放回的一個過程
  • 編譯時註解通過註解處理器來支援,而註解處理器的實際工作過程由JDK在編譯期提供支援,有興趣可以看看javac的源碼

運行時註解原理詳解

之前我們說註解是一種標記,只是針對註解的作用而言,而Java語言層面註解到底是什麼呢?以JSL中的一段話開頭

An annotation type declaration specifies a new annotation type, a special kind of interface type. To distinguish an annotation type declaration from a normal interface declaration, the keyword interface is preceded by an at-sign (@).

簡單來說就是,註解只不過是在interface前面加了@符號的特殊介面,那麼不妨以PrintMsg.class開始來看看,通過javap反編譯的到資訊如下:

public interface com.hustdj.jdkStudy.annotation.PrintMsg extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
  this_class: #1                          // com/hustdj/jdkStudy/annotation/PrintMsg
  super_class: #3                         // java/lang/Object
  interfaces: 1, fields: 0, methods: 2, attributes: 2
Constant pool:
   #1 = Class              #2             // com/hustdj/jdkStudy/annotation/PrintMsg
   #2 = Utf8               com/hustdj/jdkStudy/annotation/PrintMsg
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Class              #6             // java/lang/annotation/Annotation
   #6 = Utf8               java/lang/annotation/Annotation
   #7 = Utf8               count
   #8 = Utf8               ()I
   #9 = Utf8               AnnotationDefault
  #10 = Integer            1
  #11 = Utf8               name
  #12 = Utf8               ()Ljava/lang/String;
  #13 = Utf8               my name is PrintMsg
  #14 = Utf8               SourceFile
  #15 = Utf8               PrintMsg.java
  #16 = Utf8               RuntimeVisibleAnnotations
  #17 = Utf8               Ljava/lang/annotation/Retention;
  #18 = Utf8               value
  #19 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #20 = Utf8               RUNTIME
{
  public abstract int count();
    descriptor: ()I
    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: I#10

  public abstract java.lang.String name();
    descriptor: ()Ljava/lang/String;
    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT
    AnnotationDefault:
      default_value: s#13
}
SourceFile: "PrintMsg.java"
RuntimeVisibleAnnotations:
  0: #17(#18=e#19.#20)

從第一行就不難看出,註解是一個繼承自Annotation介面的介面,它並不是一個類,那麼getAnnotation()拿到的到底是什麼呢?不難想到,通過動態代理生成了代理類,是這樣的嘛?通過啟動參數-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true或者在上述程式碼中添加:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");將通過JDK的proxyGenerator生成的代理類保存下來在com.sun.proxy文件夾下面找到這個class文件,通過javap反編譯結果如下:

public final class com.sun.proxy.$Proxy1 extends java.lang.reflect.Proxy implements com.hustdj.jdkStudy.annotation.PrintMsg

可以看出JDK通過動態代理實現了一個類繼承我們自定義的PrintMsg介面,由於這個方法位元組碼太長了,看起來頭疼,利用idea自帶的反編譯直接在idea中打開該class文件如下:

public final class $Proxy1 extends Proxy
    implements PrintMsg
{

    public $Proxy1(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }

    public final boolean equals(Object obj)
    {
        try
        {
            return ((Boolean)super.h.invoke(this, m1, new Object[] {
                obj
            })).booleanValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String name()
    {
        try
        {
            return (String)super.h.invoke(this, m3, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString()
    {
        try
        {
            return (String)super.h.invoke(this, m2, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int count()
    {
        try
        {
            return ((Integer)super.h.invoke(this, m4, null)).intValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final Class annotationType()
    {
        try
        {
            return (Class)super.h.invoke(this, m5, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode()
    {
        try
        {
            return ((Integer)super.h.invoke(this, m0, null)).intValue();
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m4;
    private static Method m5;
    private static Method m0;

    static 
    {
        try
        {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
                Class.forName("java.lang.Object")
            });
            m3 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("name", new Class[0]);
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m4 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("count", new Class[0]);
            m5 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("annotationType", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        }
        catch(NoSuchMethodException nosuchmethodexception)
        {
            throw new NoSuchMethodError(nosuchmethodexception.getMessage());
        }
        catch(ClassNotFoundException classnotfoundexception)
        {
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());
        }
    }
}
小結

至此就解決了第一個疑問了,所謂的註解其實就是一個實現了Annotation的介面,而我們通過反射獲取到的實際上是通過JDK動態代理生成的代理類,這個類實現了我們的註解介面

AnnotationInvocationHandler

那麼問題又來了,具體是如何調用的呢?

$Proxy1的count方法為例

public final int count()
{
    try
    {
        return ((Integer)super.h.invoke(this, m4, null)).intValue();
    }
    catch(Error _ex) { }
    catch(Throwable throwable)
    {
        throw new UndeclaredThrowableException(throwable);
    }
}

跟進super

public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;
}

這個InvocationHandler是誰呢?通過在Proxy(InvocationHandler h)方法上打斷點追蹤結果如下:

image.png

原來我們對於count方法的調用傳遞給了AnnotationInvocationHandler

看看它的invoke邏輯

public Object invoke(Object var1, Method var2, Object[] var3) {
    //var4-方法名
    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:
                //因為我們是count方法,走這個分支
                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);
                    }
					//返回var6
                    return var6;
                }
        }
    }
}

這個memberValues是啥?

private final Map<String, Object> memberValues;

他是一個map,存放的是方法名(String)與值的鍵值對

這裡以count()方法的invoke執行為例

image.png

可以看到它走了default的分支,從上面的map中取到了,我們所定義的2020,那這個memberValues是什麼時候解析出來的呢?

通過查看方法調用棧,我們發現在下圖這個時候countname還沒有賦值

image.png

在方法中加入斷點重新調試得到如下結果

image.png

2020出現了,再跟進parseMemberValue方法中,再次重新調試

image.png

再跟進parseConst方法

image.png

康康javap反編譯的位元組碼中的常量池吧

#71 = Integer            2020

好巧啊,正好是2020!!

因此發現最後是從ConstantPool中根據偏移量來獲取值的,至此另一個疑問也解決了,我們在註解中設置的方法,最終在調用的時候,是從一個以<方法名,屬性值>為鍵值對的map中獲取屬性值,定義成方法只是為了在反射調用作為參數而已,所以也可以將它看成屬性吧。

總結

運行時註解的產生作用的步驟如下:

  1. 對annotation的反射調用使得動態代理創建實現該註解的一個類
  2. 代理背後真正的處理對象為AnnotationInvocationHandler,這個類內部維護了一個map,這個map的鍵值對形式為<註解中定義的方法名,對應的屬性名>
  3. 任何對annotation的自定義方法的調用(拋開動態代理類繼承自object的方法),最終都會實際調用AnnotatiInvocationHandler的invoke方法,並且該invoke方法對於這類方法的處理很簡單,拿到傳遞進來的方法名,然後去查map
  4. map中memeberValues的初始化是在AnnotationParser中完成的,是勤快的,在方法調用前就會初始化好,快取在map裡面
  5. AnnotationParser最終是通過ConstantPool對象從常量池中拿到對應的數據的,再往下ConstantPool對象就不深入了

編譯時註解初探

由於編譯時註解的很多處理邏輯內化在Javac中,這裡不做過多探討,僅對《深入理解JVM》中的知識點進行梳理和總結。

在JDK5中,Java語言提供了對於註解的支援,此時的註解只在程式運行時發揮作用,但是在JDK6中,JDK新加入了一組插入式註解處理器的標準API,這組API使得我們對於註解的處理可以提前至編譯期,從而影響到前端編譯器的工作!!常用的Lombok就是通過註解處理器來實現的

自定義簡單註解處理器

實現自己的註解處理器,首先需要繼承抽象類javax.annotation.processing.AbstractProcessor,只有process()方法需要我們實現,process()方法如下:

//返回值表示是否修改Element元素
public abstract boolean process(Set<? extends TypeElement> annotations,
                                RoundEnvironment roundEnv);
  • annotations:這個註解處理器處理的註解集合
  • roundEnv:當前round的抽象語法樹結點,每一個結點都為一個Element,一共有18種Element包含了Java中 的所有元素:
    • PACKAGE(包)
    • ENUM(枚舉)
    • CLASS(類)
    • ANNOTATION_TYPE(註解)
    • INTERFACE(介面)
    • ENUM_CONSTANT(枚舉常量)
    • FIELD(欄位)
    • PARAMETER(參數)
    • LOCAL_VARIABLE(本地變數)
    • EXCEPTION_PARAMETER(異常)
    • METHOD(方法)
    • CONSTRUCTOR(構造方法)
    • STATIC_INIT(靜態程式碼塊)
    • INSTANCE_INIT(實例程式碼塊)
    • TYPE_PARAMETER(參數化類型,泛型尖括弧中的)
    • RESOURCE_VARIABLE(資源變數,try-resource)
    • MODULE(模組)
    • OTHER(其他)

此外還有一個重要的實例變數processingEnv,它提供了上下文環境,需要創建新的程式碼,向編譯器輸出資訊,獲取其他工具類都可以通過它

實現一個簡單的編譯器註解處理器也非常簡單,繼承AbstractProcessor實現process()方法,在process()方法中實現自己的處理邏輯即可,此外需要兩個註解配合一下:

  • @SupportedAnnotationTypes:該註解處理器處理什麼註解
  • @SupportedSourceVersion:註解處理器支援的語言版本

實例

@SupportedAnnotationTypes("com.hustdj.jdkStudy.annotation.PrintMsg")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class PrintNameProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Messager messager = processingEnv.getMessager();
        for (Element element : roundEnv.getRootElements()) {
            messager.printMessage(Diagnostic.Kind.NOTE,"my name is "+element.toString());
        }
        //不修改語法樹,返回false
        return false;
    }
}

輸出如下:

G:\ideaIU\ideaProjects\cookcode\src\main\java>javac com\hustdj\jdkStudy\annotation\PrintMsg.java

G:\ideaIU\ideaProjects\cookcode\src\main\java>javac com\hustdj\jdkStudy\annotation\PrintNameProcessor.java

G:\ideaIU\ideaProjects\cookcode\src\main\java>javac -processor com.hustdj.jdkStudy.annotation.PrintNameProcessor com\hustdj\jdkStudy\annotation\AnnotationTest.java
警告: 來自注釋處理程式 'com.hustdj.jdkStudy.annotation.PrintNameProcessor' 的受支援 source 版本 'RELEASE_8' 低於 -source '1.9'
注: my name is com.hustdj.jdkStudy.annotation.AnnotationTest
1 個警告

參考資料

//blog.csdn.net/lylwo317/article/details/52163304

//www.zhihu.com/question/24401191

//juejin.cn/post/6844903879524483085#heading-4

Tags: