目錄
一、為什么要學注解?
二、注解是什么?
三、為什么要使用注解?
四、注解的作用
五、注解的分類
5.1 元注解
@Retention(/ r??ten?(?)n /) ★★★★★
@Target ★★★★★
@Inherited(/ ?n?her?t?d /)?★★
@Repeatable(/ r??pi?t?bl /)?★★
@Documented(/?d?kjument?d /)?★
5.2 標準注解
5.3 自定義注解
六、使用反射操作注解
七、注解的底層實現-動態代理
八、總結:注解工作流程
一、為什么要學注解?
????????在日常開發中,基本都是在使用別人定義或是各種框架的注解,比如Spring框架中常用的一些注解:@Controller、@Service、@RequestMapping,以此來實現某些功能,但是卻不知道如何實現的,所以如果想學習這些框架的實現原理,那么注解就是我們必知必會的一個點。其次,可以利用注解來自定義一些實現,比如在某個方法上加一個自定義注解,就可以實現方法日志的自動記錄打印,這樣也可以展現足夠的逼格。所以如果你想走上人生巔峰,更好的利用框架,又或者想要高一點的逼格,從團隊中突出,那么學習注解都是前提。
二、注解是什么?
????????在Java中注解其實就是寫在接口、類、屬性、方法上的一個標簽,或者說是一個特殊形式的注釋,注解在代碼運行時是可以被反射讀取并進行相應的操作,而如果沒有使用反射或者其他檢查,那么注解是沒有任何真實作用的,也不會影響到程序的正常運行結果。
? ????????舉個例子:@Override就是一個注解,它的作用是告訴閱讀者(開發人員、編譯器)這個方法重寫了父類的方法,對于開發人員只是一個標志,而編譯器則會多做一些事情,編譯器如果發現方法標注了這個注解,就會檢查這個方法到底是不是真的覆寫了父類的方法,如果沒有那就是在欺騙他的感情,甭廢話,編譯時直接給你報個錯,不留情面的那種。而如果不添加@Override注解,程序也是可以正常運行的,不過缺乏了靜態的檢查,本來是想覆寫父類的hello方法的,卻寫成了he110方法,這就會有些尷尬了。
在spring框架中的注解會影響到程序的運行,是因為spring內部使用反射操作了對應的注解。
?????????上面的說法是為了方便理解的,那么下面來個稍微正式一點的:注解是提供一種為程序元素設置元數據的方法,理解起來還是一樣的,程序元素就是指接口、類、屬性、方法,這些都是屬于程序的元素,那啥叫元數據呢?
????????元數據就是描述數據的數據(data about data),舉個簡單的例子,系統上有一個sm.png文件,這個文件才是我們真正需要的數據本身,而這個文件的屬性則可以稱之為sm.png的元數據,是用來描述png文件的創建時間、修改時間、分辨率等信息的,這些信息無論是有還是沒有都不影響它作為圖片的性質,都可以使用圖片軟件打開。
- 元數據是添加到程序元素如方法、字段、類和包上的額外信息,注解就是一種載體形式
- 注解不能直接干擾程序代碼的運行
三、為什么要使用注解?
? ????????以Spring為例,早期版本的Spring是通過XML文件的形式對整個框架進行配置的,一個縮減版的配置文件如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"><!-- 配置事物管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!-- 配置注解驅動事物管理 --><tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
????????在xml文件中可以定義Spring管理的Bean、事物切面等,話說當年非常流行xml配置的。優點呢就是整個項目的配置信息集中在一個文件中,從而方便管理,是集中式的配置。缺點也顯而易見,當配置信息非常多的時候,配置文件會變得越來越大不易查看管理,特別是多人協作開發時會導致一定的相互干擾。
? ????????現在都提倡解耦、輕量化或者說微小化,那么注解就順應了這一需求,各個包或模塊在內部方法或類上使用注解即可實現指定功能,而且使用起來灰常方便,簡單易懂。缺點呢就是不方便統一管理,如果需要修改某一類功能,則需要整體搜索逐個修改,是分散式的存在各個角落。
? ????????這里擴充一下,Spring注解替代了之前Spring xml文件,是不是說Spring的xml也是一種元數據呢?對的,Spring的配置文件xml也是元數據的一種表現形式。不過xml的方式是集中式的元數據,不需要和代碼綁定的,而注解是一種分散式的元數據設置方式。
四、注解的作用
????????根本來說注解就是一個注釋標簽。開發者的視角可以解讀出這個類/方法/屬性的作用以及該怎么使用,而從框架的視角則可以解析注解本身和其屬性實現各種功能,編譯器的角度則可以進行一些預檢查(@Override)和抑制警告(@SuppressWarnings)等。
- 作為特定標記,用于告訴編譯器一些信息
- 編譯時動態處理,如動態生成代碼
- 運行時動態處理,作為額外信息的載體,如獲取注解信息
五、注解的分類
? 通常來說注解分為以下三類
- 元注解 – Java內置的注解,標明該注解的使用范圍、生命周期等。
- 標準注解 – Java提供的基礎注解,標明過期的元素,標明是復寫父類方法的方法,標明抑制警告。
- 自定義注解 – 第三方定義的注解,含義和功能由第三方來定義和實現。
5.1 元注解
????????用于定義注解的注解,通常用于注解的定義上,標明該注解的使用范圍、生效范圍等。元XX 都代表最基本最原始的東西,因此,元注解就是最基本不可分解的注解,我們不能去改變它只能使用它來定義自定義的注解。元注解包含以下五種: @Retention、@Target、@Documented、@Inherited、@Repeatable,其中最常用的是@Retention和@Target下面分別介紹一下這五種元注解。
@Retention(/ r??ten?(?)n /) ★★★★★
????????中文翻譯為保留的意思,標明自定義注解的生命周期
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {RetentionPolicy value();
}
? ????????從編寫Java代碼到運行主要周期為:源文件→ Class文件 → 運行時數據,@Retention則標注了自定義注解的信息要保留到哪個階段,分別對應的value取值為:SOURCE →CLASS→RUNTIME。
- SOURCE 源代碼java文件,生成的class文件中就沒有該信息了
- CLASS class文件中會保留注解,但是jvm加載運行時就沒有了
- RUNTIME 運行時,如果想使用反射獲取注解信息,則需要使用RUNTIME,反射是在運行階段進行反射的
?
value取值為:SOURCE
value取值為:CLASS
各個生命周期的用途:
- Source:一個最簡單的用法,就是自定義一個注解例如@ThreadSafe,用來標識一個類時線程安全的,就和注釋的作用一樣,不過更引人注目罷了。
- Class:這個有啥用呢?個人覺得主要是起到標記作用,還沒有做實驗,例如標記一個@Proxy,JVM加載時就會生成對應的代理類。
- Runtime:反射實在運行階段執行的,那么只有Runtime的生命周期才會保留到運行階段,才能被反射讀取,也是我們最常用的。
@Target ★★★★★
????????中文翻譯為目標,描述自定義注解的使用范圍,允許自定義注解標注在哪些Java元素上(類、方法、屬性、局部屬性、參數…)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {ElementType[] value();
}
????????value是一個數組,可以有多個取值,說明同一個注解可以同時用于標注在不同的元素上。value的取值如下
值 | 說明 |
---|---|
TYPE | 類、接口、注解、枚舉 |
FIELD | 屬性 |
MEHOD | 方法 |
PARAMETER | 方法參數 |
CONSTRUCTOR | 構造函數 |
LOCAL_VARIABLE | 局部變量(如循環變量、catch參數) |
ANNOTATION_TYPE | 注解 |
PACKAGE | 包 |
TYPE_PARAMETER | 泛型參數 jdk1.8 |
TYPE_USE | 任何元素 jdk1.8 |
????????示例:自定義一個注解@RetentionTest,使用@Target注解定義該注解只能在類和方法上使用,使用在屬性上時會提示錯誤。
@Inherited(/ ?n?her?t?d /)?★★
? ? ? ? 標志是否可以被標注類的子類繼承。被@Inherited修飾的注解是具有繼承性的,在自定義的注解標注到某個類時,該類的子類會繼承這個自定義注解。
????????注意:只有當子類繼承父類的時候,注解才會被繼承。類實現接口,或者接口繼承接口,都是無法獲得父接口上的注解聲明的。
????????正確的示例如下(通過反射獲取注解)
@Repeatable(/ r??pi?t?bl /)?★★
????????是否可以重復標注。這個注解其實是一個語法糖,jdk1.8之前也是有辦法進行重復標注的,就是使用數組屬性(自定義注解會講到)。下面給一個例子,雖然我們標注的是多個@MyAnnotation,其實會給我們返回一個@MyAnnotations,相當于是Java幫我們把重復的注解放入了一個數組屬性中,所以只是一個語法糖而已。
@Documented(/?d?kjument?d /)?★
????????是否在生成的JavaDoc文檔中體現,被標注該注解后,生成的javadoc中,會包含該注解,這里就不做演示了。
5.2 標準注解
標準注解有一下三個
- @Override 標記一個方法是覆寫父類方法
- @Deprecated 標記一個元素為已過期,避免使用
- ? 支持的元素類型為:CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE
- @SuppressWarnings 不輸出對應的編譯警告
@SuppressWarnings(value = {"unused", "rawtypes"})
public class StandardTest extends Parent {@Overridepublic void hello() {System.out.println("StandardTest hello");}@Deprecatedpublic void walk() {}
}
5.3 自定義注解
????????注解定義格式
public @interface 注解名 {修飾符 返回值 屬性名() 默認值;修飾符 返回值 屬性名() 默認值;
}
? ????????首先注解的修飾符一般是public的,定義注解一般都是要給三方使用的,不是public的又有什么意義呢?定義的類型使用@interface,可以猜出來和接口是有一些說不清道不明的關系的,其實注解就是一個接口,在程序運行時,JVM會為其生成對應的代理類。
? ????????然后內部的定義,這個有點四不像,說是方法吧它還有一個默認值,說它是屬性吧它的后面還加了一個括號,我個人還是喜歡稱之為帶默認返回值的接口方法,通過后面的學習我們會進一步認識它的真面目。內部的修飾符只能是public的,即使不寫也默認是public的,因為它本質上就是一個接口,而接口方法的默認訪問權限就是pubilc的。
????????? 注解是不能繼承也不能實現其他類或接口的,本身就是一個元數據了,確實沒什么必要。
返回值支持的類型如下
- 基本類型 int float boolean byte double char logn short
- String
- Class
- Enum
- Annotation
- 以上所有類型的數組類型
????????定義一個簡單的接口示例
// 保留至運行時
@Retention(RetentionPolicy.RUNTIME)
// 可以加在方法或者類上
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface RequestMapping {public String method() default "GET";public String path();public boolean required();
}
????????接下來我們來看下它到底是不是一個接口,首先編譯一下該注解javac RequestMapping.java
生成對應的RequestMapping.class
文件,然后對其進行反編譯,輸出如下
// ...
①public interface RequestMapping extends java.lang.annotation.Annotation//...②public abstract java.lang.String method();descriptor: ()Ljava/lang/String;flags: ACC_PUBLIC, ACC_ABSTRACTAnnotationDefault:③default_value: s#7public abstract java.lang.String path();descriptor: ()Ljava/lang/String;flags: ACC_PUBLIC, ACC_ABSTRACTpublic abstract boolean required();descriptor: ()Zflags: ACC_PUBLIC, ACC_ABSTRACT
}
//...
① 從這里可以看到,注解的本質就是一個接口,并且繼承了java.lang.annotation.Annotation
② ③這里驗證了上面所說的,內部的定義其實就是一個帶默認值的方法
六、使用反射操作注解
? ????????反射可以獲取到Class對象,進而獲取到Constructor、Field、Method等實例,點開源碼結構發現Class、Constructor、Field、Method等均實現了AnnotatedElement接口,AnnotatedElement接口的方法如下
// 判斷該元素是否包含指定注解,包含則返回true
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
// 返回該元素上對應的注解,如果沒有返回null
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
// 返回該元素上的所有注解,如果沒有任何注解則返回一個空數組
Annotation[] getAnnotations();
// 返回指定類型的注解,如果沒有返回空數組
T[] getAnnotationsByType(Class<T> annotationClass)
// 返回指定類型的注解,如果沒有返回空數組,只包含直接標注的注解,不包含inherited的注解
T getDeclaredAnnotation(Class<T> annotationClass)
// 返回指定類型的注解,如果沒有返回空數組,只包含直接標注的注解,不包含inherited的注解
T[] getDeclaredAnnotationsByType
// 返回該元素上的所有注解,如果沒有任何注解則返回一個空數組,只包含直接標注的注解,不包含inherited的注解
Annotation[] getDeclaredAnnotations();
這就說明以上元素均可以通過反射獲取該元素上標注的注解。
個完整的示例
// package-info.java
@AnyAnnotation(order = 0, desc = "包")
package demo.annotation.reflect;// AnyAnnotation.java
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.PACKAGE, ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD,ElementType.LOCAL_VARIABLE, ElementType.PARAMETER})
public @interface AnyAnnotation {int order() default 0;String desc() default "";
}// ReflectAnnotationDemo.java
@AnyAnnotation(order = 1, desc = "我是類上的注釋")
public class ReflectAnnotationDemo {@AnyAnnotation(order = 2, desc = "我是成員屬性")private String name;@AnyAnnotation(order = 3, desc = "我是構造器")public ReflectAnnotationDemo(@AnyAnnotation(order = 4, desc = "我是構造器參數") String name) {this.name = name;}@AnyAnnotation(order = 45, desc = "我是方法")public void method(@AnyAnnotation(order = 6, desc = "我是方法參數") String msg) {@AnyAnnotation(order = 7, desc = "我是方法內部變量") String prefix = "I am ";System.out.println(prefix + msg);}public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {Class<ReflectAnnotationDemo> clazz = ReflectAnnotationDemo.class;// 獲取包上的注解,聲明在package-info.java文件中Package packagee = Package.getPackage("demo.annotation.reflect");printAnnotation(packagee.getAnnotations());// 獲取類上的注解Annotation[] annotations = clazz.getAnnotations();printAnnotation(annotations);// 獲取成員屬性注解Field name = clazz.getDeclaredField("name");Annotation[] annotations1 = name.getAnnotations();printAnnotation(annotations1);//獲取構造器上的注解Constructor<ReflectAnnotationDemo> constructor = clazz.getConstructor(String.class);AnyAnnotation[] annotationsByType = constructor.getAnnotationsByType(AnyAnnotation.class);printAnnotation(annotationsByType);// 獲取構造器參數上的注解Parameter[] parameters = constructor.getParameters();for (Parameter parameter : parameters) {Annotation[] annotations2 = parameter.getAnnotations();printAnnotation(annotations2);}// 獲取方法上的注解Method method = clazz.getMethod("method", String.class);AnyAnnotation annotation = method.getAnnotation(AnyAnnotation.class);printAnnotation(annotation);// 獲取方法參數上的注解Parameter[] parameters1 = method.getParameters();for (Parameter parameter : parameters1) {printAnnotation(parameter.getAnnotations());}// 獲取局部變量上的注解/*** 查了一些資料,是無法獲取局部變量的注解的,且局部變量的注解僅保留到Class文件中,運行時是沒有的。* 這個更多是給字節碼工具使用的,例如lombok可以嵌入編譯流程,檢測到有對應注解轉換成相應的代碼,* 而反射是無法進行操作的。當然也可以利用asm等工具在編譯器完成你要做的事情*/}public static void printAnnotation(Annotation... annotations) {for (Annotation annotation : annotations) {System.out.println(annotation);}}
}
將
@AnyAnnotation
的Retention中的生命周期改為SOURCE/CLASS試試,這時就獲取不到任何注解信息了哦
七、注解的底層實現-動態代理
首先準備一下測試代碼,如下
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Learn {// 默認值為"default"public String name() default "default";// 必須填寫public int age();
}
@Learn(age = 18)
public class LearnAnnotationReflect {// 獲取LearnAnnotationReflect類的Class對象public static void main(String[] args) {Class<LearnAnnotationReflect> reflectClass = LearnAnnotationReflect.class;// 判斷LearnAnnotationReflect類是否有Learn注解if (!reflectClass.isAnnotationPresent(Learn.class)) {return;}// 獲取LearnAnnotationReflect類的Learn注解Learn learn = reflectClass.getAnnotation(Learn.class);// 輸出Learn注解的name屬性System.out.println(learn.name());// 輸出Learn注解的age屬性System.out.println(learn.age());}
}
????????在System.out.println(learn.name());
打一個斷點,以Debug模式運行,查看learn這個對象到底是什么
? ????????從上面的截圖可以看出,jdk為Learn生成了一個叫$Proxy1的代理對象,并且包含了一個內部成員AnnotationIvocationHandler,接下來就是調用$Proxy1.name()進行獲取name的值,那么我們來看下$Proxy1到底是一個什么樣的對象,在jdk8中可以添加JVM參數-Dsun.misc.ProxyGenerator.saveGeneratedFiles來保存代理類,更高版本可以使用-Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true來保存代理類。在Idea中的設置方法如下
???????重新運行程序,就會發現在項目根目錄多了如下類,其中$Proxy1
就是Learn
注解對應的代理類
當我們調用Learn.name()
時,其實就是調用這個代理類的name方法,如下
public final String name() throws {try {return (String)super.h.invoke(this, m3, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}
代理類的name方法中主要是調用h的invoke
方法傳入當前對象,以及m3
這個方法元素
,m3如下
m3 = Class.forName("demo.annotation.runtime.Learn").getMethod("name");
? ????????在5.3講解的內容時,我們反編譯了注解的class文件,知道在編譯注解時,實際上編譯為了一個接口,接口中定義了想干的屬性的方法。
? 那么基本的流程我們就可以梳理出來了:
- 通過反射我們可以獲取對應元素上的注解@Learn,前面說過注解本質是一個接口,也就是獲取到了Learn接口的代理對象。
- Learn代理對象提供了相應的同名方法,內部聲明了原注解的相應方法Method,如method3
- 之后通過代理對象父類的h成員屬性,也就是AnnotationInvocationHandler去執行invoke方法
- AnnotationInvocationHandler在初始化時,會包含一個memberValues的map,key就是方法名,value就是對應的屬性值,在invoka內部通過Method的name從memberValues中獲取到對應的值并返回
?? 接下來我們來看下AnnotationInvocationHandler
中的invoke方法相關信息,如下
// 當前注解類型
private final Class<? extends Annotation> type;
// 當前注解的相關屬性集合,key是方法名,value是對應的值
private final Map<String, Object> memberValues;// jdk會將對應的屬性信息傳過來
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {this.type = var1;this.memberValues = var2;}public Object invoke(Object var1, Method var2, Object[] var3) {// 方法名String var4 = var2.getName();// 參數類型Class[] var5 = var2.getParameterTypes();// 如果是equals方法,則調用對應方法if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {return this.equalsImpl(var3[0]);// 不是equals方法,卻有參數,說明是錯誤的,注解的方法是不允許有參數的} else if (var5.length != 0) {throw new AssertionError("Too many parameters for an annotation method");} else {// 定義一個變量var7 默認值-1byte var7 = -1;// 不同的方法賦予var7不同的值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:// 從map集合中獲取對應的值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;}}}}
八、總結:注解工作流程
- 通過鍵值對的形式為注解屬性賦值
- 編譯器檢查注解的使用范圍 (將注解信息寫入元素屬性表 attribute)
- 運行時JVM將單個Class的runtime的所有注解屬性取出并最終存入map里
- 創建AnnotationInvocationHandler實例并傳入前面的map
- JVM使用JDK動態代理為注解生成代理類,并初始化處理器
- 調用invoke方法,通過傳入方法名返回注解對應的屬性值