這一篇 Java 註解,寫得太好了

前言

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

什麼是註解

註解其實就是一種標記,常常用於代替冗餘複雜的配置(XML、properties)又或者是編譯器進行一些檢查如JDK自帶的

Override、Deprecated

等,但是它本身並不起任何作用,可以說有它沒它都不影響程式的正常執行,註解的作用在於

「註解的處理程式」

,註解處理程式透過捕獲被註解標記的程式碼然後進行一些處理,這就是註解工作的方式。

在java中,自定義一個註解非常簡單,透過

@interface

就能定義一個註解,實現如下

public @interface PrintMsg {}

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

@PrintMsgpublic 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_PARAMETER1。8之後,泛型TYPE_USE1。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()); }}

輸出如下:

2020my name is PrintMsg

到這裡就有兩個疑問了:

getAnnotation獲取到的是什麼?一個例項?註解是一個類?

我們明明呼叫的是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: 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()

拿到的到底是什麼呢?不難想到,透過動態代理生成了代理類,是這樣的嘛?透過啟動引數

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

方法上打斷點追蹤結果如下:

這一篇 Java 註解,寫得太好了

原來我們對於

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

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

這裡以

count()

方法的invoke執行為例

這一篇 Java 註解,寫得太好了

可以看到它走了default的分支,從上面的map中取到了,我們所定義的2020,那這個

memberValues

是什麼時候解析出來的呢?

透過檢視方法呼叫棧,我們發現在下圖這個時候

count

name

還沒有賦值

這一篇 Java 註解,寫得太好了

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

這一篇 Java 註解,寫得太好了

2020出現了,再跟進

parseMemberValue

方法中,再次重新除錯

這一篇 Java 註解,寫得太好了

再跟進

parseConst

方法

這一篇 Java 註解,寫得太好了

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

#71 = Integer 2020

好巧啊,正好是2020!!

因此發現最後是從

ConstantPool

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

總結

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

對annotation的反射呼叫使得動態代理建立實現該註解的一個類

代理背後真正的處理物件為

AnnotationInvocationHandler

,這個類內部維護了一個map,這個map的鍵值對形式為<註解中定義的方法名,對應的屬性名>

任何對annotation的自定義方法的呼叫(拋開動態代理類繼承自object的方法),最終都會實際呼叫

AnnotatiInvocationHandler

的invoke方法,並且該invoke方法對於這類方法的處理很簡單,拿到傳遞進來的方法名,然後去查map

map中memeberValues的初始化是在

AnnotationParser

中完成的,是勤快的,在方法呼叫前就會初始化好,快取在map裡面

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。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面試題資料。