Java 注解的實現原理


注解的本質

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); // 獲取方法上的注解
    }
}

運行這段代碼,會發現在項目的根目錄下看到類似下圖所示的代理類:

2022-02-14 07-40-46 的屏幕截圖.png

如果沒有看到這些,那么請嘗試移除當前項目中的其它依賴(如 Spring),這些依賴項目的存在很有可能會導致相關屬性的配置失效

通過發現這些 Proxy,可以大致推斷注解的獲取極有可能是通過代理的方式來實現的,反編譯查看生成的 Proxy 類,關鍵的 Proxy 是實現 ReadAuth 接口的 Proxy,構造函數部分如下:

2022-02-14 07-47-33 的屏幕截圖.png

關鍵的部分就是使用 InvocationHandler 參數這個構造函數(m1m2m3m4 都是 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 中通過 memberValuesMap 結構來存儲相關的映射關系

參考:

[1] https://juejin.cn/post/6844903636733001741#heading-0

[2] https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.7


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM