Java 注解是在 JDK5 時引入的新特性,注解(也被稱為元數據)為我們在代碼中添加信息提供了一種形式化的方法,使我們可以在稍后某個時刻非常方便地使用這些數據。注解類型定義指定了一種新的類型,一種特殊的接口類型。 在關鍵詞 interface 前加 @ 符號也就是用 @interface 來區分注解的定義和普通的接口聲明。目前大部分框架(如 Spring Boot 等)都通過使用注解簡化了代碼並提高的編碼效率。
二、作用
-
提供信息給編譯器: 編譯器可以利用注解來探測錯誤和警告信息,如 @Override、@Deprecated。
-
編譯階段時的處理: 軟件工具可以用來利用注解信息來生成代碼、Html 文檔或者做其它相應處理,如 @Param、@Return、@See、@Author 用於生成 Javadoc 文檔。
-
運行時的處理: 某些注解可以在程序運行的時候接受代碼的提取,值得注意的是,注解不是代碼本身的一部分。如Spring 2.5 開始注解配置,減少了配置。
三、定義
2.1 注解的本質
所有的注解本質上都是繼承自 Annotation 接口。但是,手動定義一個接口繼承 Annotation 接口無效的,需要通過 @interface 聲明注解,Annotation 接口本身也不定義注解類型,只是一個普通的接口。
public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); /** *獲取注解類型 */ Class<? extends Annotation> annotationType(); }
來對比下 @interface 定義注解和繼承 Annotation 接口
public @interface TestAnnotation1 { } public interface TestAnnotation2 extends Annotation { }
通過使用 javap 指令對比兩個文件的字節碼,發現通過 @interface 定義注解,本質上就是繼承 Annotation 接口。
// javap -c TestAnnotation1.class Compiled from "TestAnnotation1.java" public interface com.hncboy.corejava.annotation.TestAnnotation1 extends java.lang.annotation.Annotation {} // javap -c TestAnnotation2.class Compiled from "TestAnnotation2.java" public interface com.hncboy.corejava.annotation.TestAnnotation2 extends java.lang.annotation.Annotation {}
雖然本質上都是繼承 Annotation 接口,但即使接口可以實現多繼承,注解的定義仍然無法使用繼承關鍵字來實現。
通過 @interface 定義注解后,該注解也不能繼承其他的注解或接口,注解是不支持繼承的,如下代碼就會報錯。
public @interface TestAnnotation1 { } /** 錯誤的定義,注解不能繼承注解 */ @interface TestAnnotation2 extends TestAnnotation1 { } /** 錯誤的定義,注解不能繼承接口 */ @interface TestAnnotation3 extends Annotation { }
雖然注解不支持繼承其他注解或接口,但可以使用組合注解的方式來解決這個問題。如 @SpringBootApplication 就采用了組合注解的方式。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { }
2.2 注解的架構
注解的基本架構如圖所示,先簡單了解下該架構,后面會詳細講解。
該架構的左半部分為基本注解的組成,一個基本的注解包含了 @interface 以及 ElementType 和 RententionPolicy 這兩個枚舉類。
-
Annotation 和 ElementType 是一對多的關系
-
Annotation 和 RetentionPolicy 是一對一的關系
該架構的右半部分為 JDK 部分內置的標准注解及元注解。
-
標准注解:@Override、@Deprecated 等
-
元注解:@Documented、@Retention、@Target、@Inherited 等
2.3 注解的屬性
注解的屬性也稱為成員變量,注解只有成員變量,沒有方法。注解的成員變量在注解的定義中以“無形參的方法”形式來聲明,其方法名定義了該成員變量的名字,其返回值定義了該成員變量的類型。
注解內的可使用的數據類型是有限制的,類型如下:
-
所有的基本類型(int,float,boolean 等)
-
String
-
Class
-
enum(@Retention 中屬性的類型為枚舉)
-
Annotation
-
以上類型的數組(@Target 中屬性類型為枚舉類型的數組)
編譯器對屬性的默認值也有約束。首先,屬性不能有不確定的的值。也就是說,屬性要么具有默認值,要么在使用注解時提供屬性的值。對於非基本類型的屬性,無論是在源代碼中聲明時,或是在注解接口中定義默認值時,都不能使用 null 為其值。因此,為了繞開這個約束,我們需要自己定義一些特殊的值,例如空字符串或負數,來表示某個屬性不存在。
通過一個案例來演示下注解可使用的數據類型及默認值。
@interface Reference { boolean contain() default false; } enum Week { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } public @interface TestAnnotation { /** * int 基本數據類型 * @return */ int type() default -1; /** * boolean 基本數據類型 * @return */ boolean status() default false; /** * String 類型 * @return */ String name() default ""; /** * Class 類型 * @return */ Class<?> loadClass() default String.class; /** * 枚舉類型 * @return */ Week today() default Week.Sunday; /** * 注解類型 * @return */ Reference reference() default @Reference(contain = true); /** * 枚舉數組類型 * @return */ Week[] value(); }
四、組成
我們已經了解了注解的架構,先來定義一個簡單的注解。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation { }
3.1 ElementType
ElementType 枚舉類型的常量為 Java 程序中可能出現注解的聲明位置提供了簡單的分類。這些常量用於 @Target 注解中。@Target 用於描述注解適用的范圍,即注解能修飾的對象范圍,通過 ElementType 的枚舉常量表示。
先來看下 ElementType 該枚舉類的代碼。
public enum ElementType { /** * 用於描述類、接口(包括注解類型)、枚舉的定義 */ TYPE, /** * 用於描述成員變量、對象、屬性(包括枚舉常量) */ FIELD, /** * 用戶描述方法 */ METHOD, /** * 用於描述參數 */ PARAMETER, /** * 用於描述構造器 */ CONSTRUCTOR, /** * 用於描述局部變量 */ LOCAL_VARIABLE, /** * 用於描述注解的(元注解) */ ANNOTATION_TYPE, /** * 用於描述包 */ PACKAGE, /* * 表示該注解能寫在類型變量的聲明語句中 * @since 1.8 */ TYPE_PARAMETER, /** * 表示該注解能寫在使用類型的任何語句中(聲明語句、泛型和強制轉換語句中的類型) * @since 1.8 */ TYPE_USE }
JDK8 之前,注解只能用於聲明的地方,JDK8 中添加了 TYPE_PARAMETER 和 TYPE_USE 類型注解,可以應用於所有地方:泛型、父類、接口,異常、局部變量等。舉個例子,定義一個 @AnyWhere 注解,Boy 接口和 Test 類。
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface AnyWhere { } public interface Boy { } public class Test<@AnyWhere T> extends @AnyWhere Object implements @AnyWhere Boy { private @AnyWhere T test1(@AnyWhere T t) throws @AnyWhere Exception { return t; } private void test2() { Test<Integer> test = new @AnyWhere Test<>(); @AnyWhere List<@AnyWhere Integer> list = new ArrayList<>(); } }
3.2 RetentionPolicy
RetentionPolicy 枚舉類型的常量用於保留注解的各種策略,即該注解的有效期。它們與 @Retention 注解類型一起使用,以指定保留注解的時間。RetentionPolicy 枚舉的代碼如下。
public enum RetentionPolicy { /** * 表示該注解只存在於源碼階段, */ SOURCE, /** * 表示該注解存在於源碼階段和編譯后的字節碼文件里 */ CLASS, /** * 表示該注解存在於源碼階段、編譯后的字節碼文件和運行時期,且注解的內容將會被 JVM 解釋執行 * 該范圍的注解可通過反射獲取到 */ RUNTIME }
Annotation 和 RetentionPolicy 是一對一的關系,即每個注解只能有一種保留策略。
這三個枚舉值是有等級關系的,SOURCE < CLASS < RUNTIME,即 RUNTIME 的有效范圍是最大的,其次的是 CLASS,最小的范圍是 SOURCE,默認的保留范圍為 CLASS。
-
RUNTIME 范圍使用於在運行期間通過反射的方式去獲取注解。
-
CLASS 適用於編譯時進行一些預處理操作。
-
SOURCE 適用於一些檢查性的工作,或者生成一些輔助的代碼,如 @Override 檢查重寫的方法,Lombok 中的 @Date、@Getter、@Setter 注解。
3.3 注解與反射
通過前面我們了解到,注解本質上繼承 Annotation 接口,也就是說,Annotation 接口是所有注解的父接口。@Retention 的保留策略為 RetentionPolicy.RUNTIME 的情況下,我們可以通過反射獲取注解的相關信息。Java 在 java.lang.reflect 包下也提供了對注解支持的接口。
主要來了解下 AnnotationElement 這個接口,其他接口都為該接口的子接口。該接口的對象代表 JVM 運行期間使用注解的類型(Class,Method,Field 等)。該包下的 Constructor 類、Method 類、Package 類和 Class 類等都實現了該接口。簡單了解下該接口的部分函數。
public interface AnnotatedElement { /** * default 方法是 Java8 新增的 * 如果指定類型的注解存在該類型上,則返回 true,否則返回 false。此方法的主要目的是方便訪問一些已知的注解 * * @param annotationClass 該泛型參數表示所有繼承了Annotation 接口的接口,也就是注解 * @return 返回該類型上是否有指定的注解 */ default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) { return getAnnotation(annotationClass) != null; } /** * 根據注解的 Class 查詢注解 */ <T extends Annotation> T getAnnotation(Class<T> annotationClass); /** * 返回該類型上的所有注解,包含繼承的 */ Annotation[] getAnnotations(); /** * 返回該類型上的所有注解,不包含繼承的 */ Annotation[] getDeclaredAnnotations(); }
我們使用代碼來測試下反射獲取注解。定義兩個注解,一個保留策略為 RetentionPolicy.RUNTIME,另一個為 RetentionPolicy.CLASS。創建 TestAnnotation 類測試注解,該類上使用了這兩個注解。
@Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation1 { String status() default "hncboy"; } @Retention(RetentionPolicy.CLASS) public @interface TestAnnotation2 { String value() default "hncboy"; } @TestAnnotation1(status = "hncboy2") @TestAnnotation2("hncboy2") public class TestAnnotation { public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz = Class.forName("com.hncboy.corejava.annotation.TestAnnotation"); // 獲取該類的所有注解 Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation.annotationType()); System.out.println(annotation.toString()); } } }
輸出結果如下,可見 TestAnnotation2 注解沒有輸出,因為 TestAnnotation2 注解類型是 RetentionPolicy.CLASS 的,所以用反射方法獲取不到。這里還涉及到了注解的一個快捷方法,就是當注解里的屬性名字定義為 value 時,可以在使用該注解時不指定屬性名,上面的 @Target 注解和 @Retention 注解都屬於這種情況,不過當注解里有多個屬性時,那就必須指定屬性名了。
interface com.hncboy.corejava.annotation.TestAnnotation1 @com.hncboy.corejava.annotation.TestAnnotation1()(status=hncboy2)
五、元注解
元注解即注解的注解且只能作用於注解上的注解,也就是說元注解負責其他注解的注解,而且只能用在注解上面。
JDK8 以前內置的元注解有 @Documented、@Retention、@Target、@Inherited 這四個,JDK 8 引入了 @Repeatable, 前面已經了解過了 @Target 和 @Retention,下面做一些簡單的補充。
元注解的 @Target 都為 ElementType.ANNOTATION_TYPE,因為元注解只能應用於注解的注解。元注解在定義該注解的同時也可以直接使用該注解。
5.1 @Target
該注解用於定義注解能使用的范圍,取值為 ElementType 枚舉。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * 返回可以應用注解類型的各種范圍的枚舉數組 * 名字為 value 時可以省略屬性名 * @return */ ElementType[] value(); }
使用方式:
@Target(ElementType.METHOD) @Target(value = ElementType.METHOD) @Target({ElementType.METHOD, ElementType.TYPE}) @Target(value = {ElementType.METHOD, ElementType.TYPE})
5.2 @Retention
該注解定義注解的保留策略或者說定義注解的有效期,取值范圍為 RetationPolicy 枚舉。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * 返回保留策略 * @return */ RetentionPolicy value(); }
使用方式:
@Retention(RetentionPolicy.RUNTIME)
@Retention(value = RetentionPolicy.RUNTIME)
5.3 @Documented
該注解的使用表示是否包含在生成的 javadoc 文檔中。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Documented { }
舉個例子,定義一個 @TestAnnotation 注解和 Test 類。
@Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface TestAnnotation { } @TestAnnotation public class Test { }
通過 javadoc -d doc *.java 命令將該目錄下的這兩個類生成文檔並放在 doc 目錄下。生成的文件如下,點擊 index.html。
看到如圖所示的樣子,Test 類中包含 @TestAnnotation。
我們再把 @TestAnnotation 注解上的 @Documenet 注解注釋掉再來生成下文檔。此時發現 Test 類中沒有 @TestAnnotation 注解了。
5.4 @Inherited
該注解表示注解是否具有繼承的特性。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { }
舉個例子來測試下。新建 TestAnnotation 注解,Father 類,Son 類,Father 類使用了該注解,Son 類繼承 Father 類。
@Retention(RetentionPolicy.RUNTIME) @Inherited public @interface TestAnnotation { } @TestAnnotation public class Father { } public class Son extends Father { }
新建一個測試類,測試 Father 和 Son 這兩個類是否包這兩個注解。
public class Test { public static void main(String[] args) { System.out.println(Father.class.isAnnotationPresent(TestAnnotation.class)); System.out.println(Son.class.isAnnotationPresent(TestAnnotation.class)); } }
輸出為 true true,當把 @TestAnnotation 注解上的 @Inherited 注解注釋掉時,輸出 true false,如此可見該注解的作用。
5.5 @Repeatable
JDK8 以前是不支持重復注解的,同一個地方只能使用同一個注解一次。 該注解從 JDK8 引入,該注解類型用於表示其聲明注解的注解類型為可重復時。 value() 的值表示可重復注解的類型,包含注解類型。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { /** * 指可重復注解的類型,包含注解類型 * @return */ Class<? extends Annotation> value(); }
舉個例子,定義 @Activity 和 @Activities 注解,定義 Hncboy 類測試重復注解。@Activity 注解被 @Repeatable(Activities.class) 注解,@Activities 相當於一個容器注解,屬性為 Activity 類型的數組,通過這樣的方式,使得 @Activity 注解可以被重復使用。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Activities { Activity[] value(); } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Repeatable(Activities.class) public @interface Activity { String value(); } @Activity("打代碼") @Activity("吃飯") @Activity("睡覺") public class Hncboy { } @Activities({@Activity("打代碼"), @Activity("吃飯"), @Activity("睡覺")}) public class Hncboy { }
六、標准注解
JDK 內置的注解有 @Deprecated、@Override、@SuppressWarnnings、@SafeVarargs(JDK 7 引入)、@FunctionalInterface(JDK 引入)等。接下來介紹下 3 中常用的內置注解。
6.1 @Deprecated
注解為 @Deprecated 的類型是不鼓勵程序員使用的元素,通常是因為這樣做很危險,或者是因為存在更好的替代方法。當在不推薦使用的代碼中使用或覆蓋不推薦使用的程序元素時,編譯器會發出警告。該注解可以用來修飾構造器、字段、局部變量、方法等類型。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
舉個例子,使用 @Deprecated 修飾的元素是不推薦使用的,編譯器會幫我們將這些類和方法用刪除線標記。直接聲明在包上會報 Package annotations should be in file package-info.java 錯誤。
@Deprecated public class TestDeprecated { @Deprecated String s = "hncboy"; @Deprecated public void test() { } }
6.2 @Override
@Override 注解我們經常用到,提示子類需要重寫父類的方法。方法重寫或實現了在父類中聲明的方法時需要加上該注解,該注解用於編譯器檢查重寫的操作是否正確,保留策略為 RetentionPolicy.SOURCE。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
6.3 @SuppressWarnings
用來關閉編譯器生成警告信息,可以用來修飾類、方法、成員變量等,在使用該注解時,應采用就近原則,如方法產生警告是,應該針對方法聲明該注解,而不是對類聲明,有利於發現該類的其他警告信息。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { /** * 帶有注解的元素中的編譯器將禁止的警告集。 * 使用 unchecked 忽略無法識別的警告 */ String[] value(); }
舉個例子,rawtypes 用於使用泛型時忽略沒有指定相應的類型,unused 用於沒有使用過的代碼。
public class Test { @SuppressWarnings({"rawtypes", "unused"}) private List test() { return new ArrayList(); } }
七、自定義注解
自定義注解實現 Spring IOC Bean 實例創建,自定義簡單的注解: @Component、@Bean 和 @ComponentScan。
通過什么是反射?這篇文章我們已經學習到通過反射實現 Spring IOC Bean 實例的三種創建方式,不清楚的可以去看下那篇文章。
7.1 新建 @MyComponent、@MyBean、 @MyComponentScan
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface MyBean { String value() default ""; } @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyComponent { String value() default ""; } @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyComponentScan { String value() default ""; }
7.2 新建 A、B、C 三個類
@MyComponent("a")
public class A {
public A() {
System.out.println("調用 A 的無參構造器");
}
@MyBean("b")
public static B createBInstance() {
System.out.println("調用 A 的靜態方法 createBInstance");
return new B();
}
@MyBean("c")
public C createCInstance() {
System.out.println("調用 A 的實例方法 createCInstance");
return new C();
}
}
class B {}
class C {}
7.3 新建 IOCContainer 類
/** * 定義 map 存放 bean */ public class IOCContainer { private static HashMap<String, Object> container = new HashMap<>(); public static void putBean(String id, Object object) { container.put(id, object); } public static Object getBean(String id) { return container.get(id); } }
7.4 新建 Test 類
-
先獲取 @MyComponentScan 注解中的包名
-
然后掃描該包下所有類的全限定名
-
遍歷類名,判斷改類是否實現 @MyComponent 注解
-
遍歷方法,判斷該方法是否實現 @MyBean 注解
大致過程是這樣,具體的可以見代碼的注釋。
@MyComponentScan("com.hncboy.corejava.annotation.spring")
public class Test {
public static void main(String[] args) throws Exception {
Test test = new Test();
// 獲取 MyComponentScan 注解中的包名
String scanPackage = test.getScanPackage();
HashSet<String> classPathSet = new HashSet<>();
// 掃描包下的所有類並將類的全限定名放進 classPathSet
test.doScanPackage(classPathSet, scanPackage);
// 遍歷掃描包下的所有類
for (String className : classPathSet) {
// 通過類的全限定名獲取 Class
Class<?> clazz = Class.forName(className);
// 判斷該類是否實現了 MyComponent 注解
if (clazz.isAnnotationPresent(MyComponent.class)) {
// 方式1:通過構造器實例化
IOCContainer.putBean(className, clazz.newInstance());
}
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
// 判斷方法是否有 MyBean 注解
if (method.isAnnotationPresent(MyBean.class)) {
// 獲取 bean 值
String beanName = method.getAnnotation(MyBean.class).value();
// 判斷該方法是否是靜態方法或實例方法
if (Modifier.isStatic(method.getModifiers())) {
// 方式2:通過靜態工廠實例化
IOCContainer.putBean(beanName, method.invoke(null));
} else {
// 方式3:通過實例工廠實例化
// 首先獲取該類的實例對象,再調用實例方法進行實例化
IOCContainer.putBean(beanName, method.invoke(IOCContainer.getBean(className)));
}
}
}
}
}
/**
* 獲取 MyComponentScan 注解中的包名
*
* @return
*/
private String getScanPackage() {
Class<?> clazz = this.getClass();
if (!clazz.isAnnotationPresent(MyComponentScan.class)) {
return "";
}
MyComponentScan scanPackage = clazz.getDeclaredAnnotation(MyComponentScan.class);
return scanPackage.value();
}
/**
* 掃描該包下的類
*
* @param classPathSet
* @param scanPackage
*/
private void doScanPackage(HashSet<String> classPathSet, String scanPackage) {
// 通過正則表達式將包名中的 . 替代為 /,並獲取到該路徑的 class url
URL url = this.getClass().getResource("/" + scanPackage.replaceAll("\\.", "/"));
// 獲取該 url 下的所有 File(目錄/文件)
File classDir = new File(url.getFile());
// 遍歷所有 File
for (File file : classDir.listFiles()) {
// 判斷該 file 如果是目錄的話
if (file.isDirectory()) {
// 拼接該目錄的名字並遞歸遍歷該目錄
doScanPackage(classPathSet, scanPackage + "." + file.getName());
} else {
// 如果文件不是以 .class 結尾
if (!file.getName().endsWith(".class")) {
continue;
}
// 通過 包名+目錄名+除去.class的類名 拼接該類的全限定名
String clazzName = (scanPackage + "." + file.getName().replace(".class", ""));
// 將該類的全限定名放入 classPathSet
classPathSet.add(clazzName);
}
}
}
}
輸出如下:
調用 A 的無參構造器
調用 A 的靜態方法 createBInstance
調用 A 的實例方法 createCInstance
注:APT——這些處理提取和處理 Annotation 的代碼統稱為 APT(Annotation Processing Tool)。
Java 編程思想
