注解的本質
在 java.lang.annotation.Annotation
接口中有這樣的描述:
The common interface extended by all annotation interfaces.
大致意思就是所有的注解接口都繼承自該 Annotaion
接口
假設現在我們編寫了一個新的注解 ReadAuth
,該注解的目的是標記那些讀取數據需要權限的操作,如下所示:
public @interface ReadAuth {
}
現在,編譯這個注解類,然后通過 javap
命令查看反編譯之后的結果:
Compiled from "ReadAuth.java"
public interface com.example.eamples.annotations.ReadAuth extends java.lang.annotation.Annotation {
}
可以看到,注解的本質是一個繼承了 java.lang.annotation.Annotation
接口的接口類
注解是元數據的一種提供形式,提供不屬於程序本身的數據,相當與給某個程序區域打上標簽。
然而,如果使用 Spring 開發項目的話,經常會見到使用注解就能完成許多任務的情況,如:通過 @Controller
定義控制器、@RequestMapping
定義請求 url
等。這些注解本質上也只是一個標記的作用,具體功能的實現是通過 Spring 來解析這些注解來實現
解析注解有兩種方式:一是在編譯階段掃描注解,二是在運行期間通過反射的方式來獲取相關的注解信息。第一種方式要求編譯器能夠檢測到合法的注解,由於編譯器一般情況下沒有辦法修改它們的行為,因此對於用戶或者框架自定義的注解,都需要通過反射的方式來獲取注解的元數據信息
元注解
“元注解” 是 JDK
中內置的幾種用於修飾注解的注解。通常在注解的定義上能夠看到這些注解,如常見的方法重寫注解 @Override
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其中,@Target
和 @Retention
注解就是 JDK
中內置的元注解,表示自定義的注解應該作用的代碼范圍和保留時間段
JDK
中存在以下幾個元注解:
@Retention
:@Retention
注解指定標記的注解的存儲方式,有以下三種存儲方式:RetentionPolicy.SOURCE
:標記的注解僅保留在源代碼級別,並被編譯器忽略RetentionPolicy.CLASS
:標記的注釋在編譯時由編譯器保留,但被 Java 虛擬機忽略(即類加載階段忽略)RetentionPolicy.RUNTIME
:標記的注解由 JVM 保留,因此它可以被運行時環境使用
@Documented
:@Documented
注解表示無論注解的存儲方式如何,這些注解都能夠使用javadoc
工具生成到文檔中(默認情況下,注解將不會被包括到javadoc
生成的文檔中)@Target
:@Target
注解標記另一個注解,以限制該注解可以應用於哪些 Java 元素。@Target
可以指定以下元素類型的一個或多個作為其值:ElementType.ANNOTATION_TYPE
表示該注解的作用范圍為注解ElementType.CONSTRUCTOR
作用於構造函數ElementType.FIELD
作用於字段或者屬性ElementType.LOCAL_VARIABLE
作用於局部變量ElementType.METHOD
作用於方法級別ElementType.PACKAGE
作用於包聲明ElementType.PARAMETER
作用於一個方法的參數ElementType.TYPE
作用於一個類的任意元素(該類可以是一般類、接口或枚舉)
@Inherited
:@Inherited 注解表示注解類型可以繼承自父類(默認情況下不可以繼承)。當用戶查詢注解類型並且類沒有該類型的注解時,查詢該類的父類的注解類型。 該注解僅適用於類聲明。@Repeatable
:@Repeatable
注解,在 Java SE 8 中引入,表示標記的注解可以多次應用於同一個聲明或類型使用。
JDK 預定義注解
在 JDK 1.8 中,預先定義了以下幾種注解:
@Deprecated
:@Deprecated
注解表示標記的元素已被棄用,不應再使用。每當程序使用帶有 @Deprecated 注釋的方法、類或字段時,編譯器都會生成警告。@Override
:@Override
注釋通知編譯器該元素將要重寫在父類中聲明的元素。雖然重寫方法時不需要使用此注釋,但它有助於防止錯誤。 如果標有@Override
的方法未能正確覆蓋其父類之一中的方法,則編譯器會生成錯誤。@SuppressWarnings
:@SuppressWarnings
注釋告訴編譯器抑制它將生成的警告。每個編譯器警告都屬於一個類別。 Java 語言規范列出了兩個類別:棄用和未選中。@SafeVarargs
:@SafeVarargs
注釋,當應用於方法或構造函數時,斷言代碼不會對其 varargs 參數執行潛在的不安全操作。 使用此注釋類型時,與可變參數使用相關的未經檢查的警告將被禁止。@FunctionalInterface
:@FunctionalInterface
注解,在 Java SE 8 中引入,表示類型聲明旨在成為 Java 語言規范所定義的功能接口
注解與反射
在 Java 虛擬機規范中,定義了一系列和注解相關的屬性表,也就是說,無論是字段、方法還是類,如果被注解修飾了,那么就可以寫入到對應的字節碼文件。對應的屬性表有以下幾種:
RuntimeVisibleAnnotations
:運行時可見的注解RuntimeInVisibleAnnotations
:運行時不可見的注解RuntimeVisibleParameterAnnotations
:運行時可見的方法參數注解RuntimeInvisibleParameterAnnotations
:運行時不可見的方法參數注解AnnotationDefault
:注解類元素的默認值
由於在 Class
文件中存在這些屬性,因此對於一個類或者接口來說,相關類的Class
對象能夠提供以下幾種和注解交互的方法:
getAnnotation
:返回指定的注解isAnnotationPresent
:判斷當前的元素是否被指定的注解修飾過getAnnotations
:返回該元素上的所有注解getDeclaredAnnotation
:返回本元素的指定注解getDeclaredAnnotations
:返回本元素的所有注解,不包括從父注解繼承來的注解
接下來,讓我們看看 JDK 是如何獲取到相關的注解的
依舊以前面提到的 @ReadAuth
為例,下面是自定義的 @ReadAuth
的定義:
import java.lang.annotation.*;
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadAuth {
}
然后編寫下面的示例來獲取方法的注解:
import java.lang.reflect.Method;
public class TestReadAuth {
@ReadAuth
static void readTest() {
System.out.println("Read Auth Test");
}
static {
/*
JDK 8 及其i之前的版本需要設置 sun.misc.ProxyGenerator.saveGeneratedFiles 屬性為 true,JDK 8 之后版本
則需要設置 jdk.proxy.ProxyGenerator.saveGeneratedFiles 屬性為 true,具體可以查看 ProxyGenerator 的saveGeneratedFiles 定義的屬性
配置這個屬性的目的在於保存在程序運行過程中生成的 Proxy 對象,
假設獲取注解的過程是通過代理的方式來實現的,通過配置該屬性就能夠保存中間的代理對象
*/
// System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
}
public static void main(String[] args) throws NoSuchMethodException {
Class<?> cls = TestReadAuth.class;
Method method = cls.getDeclaredMethod("readTest"); // 通過反射獲取類的方法
ReadAuth readAuth = method.getAnnotation(ReadAuth.class); // 獲取方法上的注解
}
}
運行這段代碼,會發現在項目的根目錄下看到類似下圖所示的代理類:

如果沒有看到這些,那么請嘗試移除當前項目中的其它依賴(如 Spring),這些依賴項目的存在很有可能會導致相關屬性的配置失效
通過發現這些 Proxy,可以大致推斷注解的獲取極有可能是通過代理的方式來實現的,反編譯查看生成的 Proxy 類,關鍵的 Proxy 是實現 ReadAuth
接口的 Proxy,構造函數部分如下:
關鍵的部分就是使用 InvocationHandler
參數這個構造函數(m1
、m2
、m3
、m4
都是 Annotation
接口定義的方法,因為所有的注解都繼承自 Annotation
)。InvocationHandler
是使用 JDK 動態代理時需要實現的接口,因此可以判斷這里的代理類型為 JDK 動態代理
查看 InvocationHandler
的具體實現,可以發現在 AnnotationInvocationHandler
中有一段這樣的描述:
InvocationHandler for dynamic proxy implementation of Annotation.
大致意思就是:用於注解的動態代理實現的 InvocationHandler
也就是說,生成的代理類的 InvocationHandler
參數的具體實現就是 AnnotationInvocationHandler
按照 JDK 動態代理的基本使用,關鍵的部分是 invoke
方法的實現,具體在 AnnotationInvocationHandler
的實現如下:
public Object invoke(Object proxy, Method method, Object[] args) {
String member = method.getName();
int parameterCount = method.getParameterCount();
// Handle Object and Annotation methods
if (parameterCount == 1 && member == "equals" &&
method.getParameterTypes()[0] == Object.class) {
return equalsImpl(proxy, args[0]);
}
if (parameterCount != 0) {
throw new AssertionError("Too many parameters for an annotation method");
}
// 如果 是 Annotation 中定義的方法,那么則調用 AnnotationInvocationHandler 中的具體實現
if (member == "toString") {
return toStringImpl();
} else if (member == "hashCode") {
return hashCodeImpl();
} else if (member == "annotationType") {
return type;
}
// Handle annotation member accessors
/*
走到這說明是自定義的方法(屬性),嘗試獲取屬性值
這里的 memberValues 在構造 AnnotationInvocationHandler 時就已經完成初始化了,這是一個
Map 字段,存儲的時注解中配置的屬性名 ——> 屬性值的映射
*/
Object result = memberValues.get(member);
if (result == null)
throw new IncompleteAnnotationException(type, member);
if (result instanceof ExceptionProxy)
throw ((ExceptionProxy) result).generateException();
if (result.getClass().isArray() && Array.getLength(result) != 0)
result = cloneArray(result);
return result;
}
總結
- 注解本質上是繼承了
Annotation
接口的接口類,用於提供相關元素的元數據信息 - Java 虛擬機中會按照注解的存儲方法存儲在類的不同時間段,如果保留時間為
RUNTIME
,那么在 Java 虛擬機中將會保存這個注解,同時有相關的屬性表來存儲這些注解,因此通過反射獲取注解在理論上具有可行性 - 實際獲取注解時是通過代理的方式來實現的,
AnnotationInvocationHandler
是實際方法調用所有者。對於注解參數的獲取,AnnotationInvocationHandler
中通過memberValues
的Map
結構來存儲相關的映射關系
參考:
[1] https://juejin.cn/post/6844903636733001741#heading-0
[2] https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.7