關於注解的一點點思考
簡介
Java注解是在JDK1.5被引入的技術,配合反射可以在運行期間處理注解,配合apt tool可以在編譯器處理注解,在JDK1.6之后,apt tool被整合到了javac里面。
什么是注解
注解其實就是一種標記,常常用於代替冗余復雜的配置(XML、properties)又或者是編譯器進行一些檢查如JDK自帶的Override、Deprecated
等,但是它本身並不起任何作用,可以說有它沒它都不影響程序的正常運行,注解的作用在於注解的處理程序,注解處理程序通過捕獲被注解標記的代碼然后進行一些處理,這就是注解工作的方式。
在java中,自定義一個注解非常簡單,通過@interface
就能定義一個注解,實現如下
public @interface PrintMsg {
}
寫個測試類給他加上我們寫的這個注解吧
@PrintMsg
public 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_PARAMETER | 1.8之后,泛型 |
TYPE_USE | 1.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());
}
}
輸出如下:
2020
my 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: 2
Constant 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<String, Object> memberValues;
他是一個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.java
G:\ideaIU\ideaProjects\cookcode\src\main\java>javac com\hustdj\jdkStudy\annotation\PrintNameProcessor.java
G:\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.AnnotationTest
1 個警告
參考資料
https://blog.csdn.net/lylwo317/article/details/52163304
https://www.zhihu.com/question/24401191
https://juejin.cn/post/6844903879524483085#heading-4
《深入理解Java虛擬機》