這一篇 Java 注解秤标,寫得太好了绝淡!

前言

Java注解是在JDK1.5被引入的技術(shù),配合反射可以在運行期間處理注解苍姜,配合apt tool可以在編譯器處理注解牢酵,在JDK1.6之后,apt tool被整合到了javac里面。

什么是注解

注解其實就是一種標記,常常用于代替冗余復(fù)雜的配置(XML、properties)又或者是編譯器進行一些檢查如JDK自帶的Override、Deprecated等于微,但是它本身并不起任何作用,可以說有它沒它都不影響程序的正常運行声畏,注解的作用在于「注解的處理程序」激率,注解處理程序通過捕獲被注解標記的代碼然后進行一些處理,這就是注解工作的方式季蚂。

在java中茫船,自定義一個注解非常簡單,通過@interface就能定義一個注解扭屁,實現(xiàn)如下

public @interface PrintMsg {}

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

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

我們發(fā)現(xiàn)寫與不寫這個注解的效果是相同的算谈,這也印證了我們說的注解只是一種「標記」,有它沒它并不影響程序的運行料滥。

元注解

在實現(xiàn)這個注解功能之前然眼,我們先了解一下元注解。

元注解:對注解進行注解葵腹,也就是對注解進行標記高每,元注解的背后處理邏輯由apt tool提供,對注解的行為做出一些限制践宴,例如生命周期鲸匿,作用范圍等等。

@Retention

用于描述注解的生命周期阻肩,表示注解在什么范圍有效带欢,它有三個取值,如下表所示:

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

@Target

用于描述注解作用的「對象類型」锦溪,這個就非常多了,如下表所示:

類型 作用的對象類型
TYPE 類府怯、接口刻诊、枚舉
FIELD 類屬性
METHOD 方法
PARAMETER 參數(shù)類型
CONSTRUCTOR 構(gòu)造方法
LOCAL_VARIABLE 局部變量
ANNOTATION_TYPE 注解
PACKAGE
TYPE_PARAMETER 1.8之后,泛型
TYPE_USE 1.8之后牺丙,除了PACKAGE之外任意類型

@Documented

將注解的元素加入Javadoc中

@Inherited

如果被這個注解標記了则涯,被標記的類、接口會繼承父類冲簿、接口的上面的注解

@Repeatable

表示該注解可以重復(fù)標記

注解的屬性

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

首先給這個注解加億點點細節(jié)吝沫,生命周期改為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());    }}

輸出如下:

2020my name is PrintMsg

到這里就有兩個疑問了:

  1. getAnnotation獲取到的是什么?一個實例惨险?注解是一個類羹幸?
  2. 我們明明調(diào)用的是count(),name(),但是為什么說是注解的屬性辫愉?

等下聊

到底什么是注解栅受?

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

  • 運行時注解的本質(zhì)是實現(xiàn)了Annotation接口的特殊接口恭朗,JDK在運行時為其創(chuàng)建代理類屏镊,注解方法的調(diào)用實際是通過AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中維護了一個Map冀墨,Map中存放的是方法名與返回值的映射闸衫,對注解中自定義方法的調(diào)用其實最后就是用方法名去查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: 2Constant 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()拿到的到底是什么呢踏烙?不難想到师骗,通過動態(tài)代理生成了代理類,是這樣的嘛讨惩?通過啟動參數(shù)-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true或者在上述代碼中添加:

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

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

可以看出JDK通過動態(tài)代理實現(xiàn)了一個類繼承我們自定義的PrintMsg接口,由于這個方法字節(jié)碼太長了荐捻,看起來頭疼黍少,利用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());        }    }}

小結(jié)

至此就解決了第一個疑問了,「所謂的注解其實就是一個實現(xiàn)了Annotation的接口处面,而我們通過反射獲取到的實際上是通過JDK動態(tài)代理生成的代理類厂置,這個類實現(xiàn)了我們的注解接口」

AnnotationInvocationHandler

那么問題又來了,具體是如何調(diào)用的呢魂角?

$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)方法上打斷點追蹤結(jié)果如下:

原來我們對于count方法的調(diào)用傳遞給了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執(zhí)行為例

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

通過查看方法調(diào)用棧平挑,我們發(fā)現(xiàn)在下圖這個時候countname還沒有賦值

在方法中加入斷點重新調(diào)試得到如下結(jié)果

2020出現(xiàn)了,再跟進parseMemberValue方法中系草,再次重新調(diào)試

再跟進parseConst方法

康康javap反編譯的字節(jié)碼中的常量池吧

#71 = Integer            2020

好巧啊通熄,正好是2020!找都!

因此發(fā)現(xiàn)最后是從ConstantPool中根據(jù)偏移量來獲取值的唇辨,至此另一個疑問也解決了,我們在注解中設(shè)置的方法能耻,最終在調(diào)用的時候赏枚,是從一個以<方法名亡驰,屬性值>為鍵值對的map中獲取屬性值,定義成方法只是為了在反射調(diào)用作為參數(shù)而已饿幅,所以也可以將它看成屬性吧凡辱。

總結(jié)

運行時注解的產(chǎn)生作用的步驟如下:

  1. 對annotation的反射調(diào)用使得動態(tài)代理創(chuàng)建實現(xiàn)該注解的一個類
  2. 代理背后真正的處理對象為AnnotationInvocationHandler,這個類內(nèi)部維護了一個map栗恩,這個map的鍵值對形式為<注解中定義的方法名透乾,對應(yīng)的屬性名>
  3. 任何對annotation的自定義方法的調(diào)用(拋開動態(tài)代理類繼承自object的方法),最終都會實際調(diào)用AnnotatiInvocationHandler的invoke方法,并且該invoke方法對于這類方法的處理很簡單磕秤,拿到傳遞進來的方法名乳乌,然后去查map
  4. map中memeberValues的初始化是在AnnotationParser中完成的,是勤快的市咆,在方法調(diào)用前就會初始化好汉操,緩存在map里面
  5. AnnotationParser最終是通過ConstantPool對象從常量池中拿到對應(yīng)的數(shù)據(jù)的,再往下ConstantPool對象就不深入了

編譯時注解初探

由于編譯時注解的很多處理邏輯內(nèi)化在Javac中床绪,這里不做過多探討客情,僅對《深入理解JVM》中的知識點進行梳理和總結(jié)。

在JDK5中癞己,Java語言提供了對于注解的支持膀斋,此時的注解只在程序運行時發(fā)揮作用,但是在JDK6中痹雅,JDK新加入了一組插入式注解處理器的標準API仰担,這組API使得我們對于注解的處理可以提前至編譯期,從而影響到前端編譯器的工作<ㄉ纭摔蓝!常用的Lombok就是通過注解處理器來實現(xiàn)的

「自定義簡單注解處理器」

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

//返回值表示是否修改Element元素public abstract boolean process(Set<? extends TypeElement> annotations,                                RoundEnvironment roundEnv);
  • annotations:這個注解處理器處理的注解集合

  • roundEnv:當前round的抽象語法樹結(jié)點,每一個結(jié)點都為一個Element朴沿,一共有18種Element包含了Java中 的所有元素:

  • PACKAGE(包)

  • ENUM(枚舉)

  • CLASS(類)

  • ANNOTATION_TYPE(注解)

  • INTERFACE(接口)

  • ENUM_CONSTANT(枚舉常量)

  • FIELD(字段)

  • PARAMETER(參數(shù))

  • LOCAL_VARIABLE(本地變量)

  • EXCEPTION_PARAMETER(異常)

  • METHOD(方法)

  • CONSTRUCTOR(構(gòu)造方法)

  • STATIC_INIT(靜態(tài)代碼塊)

  • INSTANCE_INIT(實例代碼塊)

  • TYPE_PARAMETER(參數(shù)化類型猜谚,泛型尖括號中的)

  • RESOURCE_VARIABLE(資源變量,try-resource)

  • MODULE(模塊)

  • OTHER(其他)

此外還有一個重要的實例變量processingEnv赌渣,它提供了上下文環(huán)境魏铅,需要創(chuàng)建新的代碼,向編譯器輸出信息坚芜,獲取其他工具類都可以通過它

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

  • @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.javaG:\ideaIU\ideaProjects\cookcode\src\main\java>javac com\hustdj\jdkStudy\annotation\PrintNameProcessor.javaG:\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.AnnotationTest1 個警告

最后給大家送下福利铸敏,大家可以在后臺私信面試,即可以獲取一份我整理的最新Java面試題資料屯仗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末搞坝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子魁袜,更是在濱河造成了極大的恐慌桩撮,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峰弹,死亡現(xiàn)場離奇詭異店量,居然都是意外死亡,警方通過查閱死者的電腦和手機鞠呈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門融师,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蚁吝,你說我怎么就攤上這事旱爆。” “怎么了窘茁?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵怀伦,是天一觀的道長。 經(jīng)常有香客問我山林,道長房待,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任驼抹,我火速辦了婚禮桑孩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘框冀。我一直安慰自己流椒,他們只是感情好,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布明也。 她就那樣靜靜地躺著镣隶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诡右。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天轻猖,我揣著相機與錄音帆吻,去河邊找鬼。 笑死咙边,一個胖子當著我的面吹牛猜煮,可吹牛的內(nèi)容都是我干的次员。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼王带,長吁一口氣:“原來是場噩夢啊……” “哼淑蔚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起愕撰,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤刹衫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后搞挣,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體带迟,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年囱桨,在試婚紗的時候發(fā)現(xiàn)自己被綠了仓犬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡舍肠,死狀恐怖搀继,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情翠语,我是刑警寧澤叽躯,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站啡专,受9級特大地震影響险毁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜们童,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一畔况、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧慧库,春花似錦跷跪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至甘磨,卻和暖如春橡羞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背济舆。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工卿泽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人滋觉。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓签夭,卻偏偏與公主長得像齐邦,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子第租,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容