前言
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)
方法上打斷點追蹤結果如下:
原來我們對於
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
他是一個map,存放的是方法名(String)與值的鍵值對
這裡以
count()
方法的invoke執行為例
可以看到它走了default的分支,從上面的map中取到了,我們所定義的2020,那這個
memberValues
是什麼時候解析出來的呢?
透過檢視方法呼叫棧,我們發現在下圖這個時候
count
和
name
還沒有賦值
在方法中加入斷點重新除錯得到如下結果
2020出現了,再跟進
parseMemberValue
方法中,再次重新除錯
再跟進
parseConst
方法
康康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面試題資料。