天天看點

一文讀懂Annotation

作者:老周聊架構
歡迎大家關注我的微信公衆号【老周聊架構】,Java後端主流技術棧的原理、源碼分析、架構以及各種網際網路高并發、高性能、高可用的解決方案。

一、什麼是注解

根據wikipedia中介紹:

In the Java computer programming language, an annotation is a form of syntactic metadata that can be added to Java source code. Classes, methods, variables, parameters and Java packages may be annotated. Like Javadoc tags, Java annotations can be read from source files. Unlike Javadoc tags, Java annotations can also be embedded in and read from Java class files generated by the Java compiler. This allows annotations to be retained by the Java virtual machine at run-time and read via reflection. It is possible to create meta-annotations out of the existing ones in Java.

翻譯中文則是:

Java 注解又稱 Java 标注,是 JDK5.0 版本開始支援加入源代碼的特殊文法中繼資料 。

Java 語言中的類、方法、變量、參數和包等都可以被标注。和 Javadoc 不同,Java 标注可以通過反射擷取标注内容。在編譯器生成類檔案時,标注可以被嵌入到位元組碼中。Java 虛拟機可以保留标注内容,在運作時可以擷取到标注内容。 當然它也支援自定義 Java 标注。

這定義已經夠清晰了,但你可能有個疑問,老周啊,通過反射擷取标注内容,反射在哪展現啊。别問,一問又是要看底層源碼了。

既然這樣,那我們就來看下 JDK java.lang.annotation 包的結構:

一文讀懂Annotation

看見帶 @ 辨別的沒,一共有 6 個,是以 JDK 源碼裡定義了 6 個注解。

  • @Document
  • @Target
  • @Retention
  • @Inherited
  • @Native
  • @Repeatable

其中前 4 個是元注解。

等一等,元注解又是什麼?不急,我們來看下一節。

二、元注解

元注解的作用就是負責注解其它注解,它們被用來提供對其它 annotation 類型作說明。

我們拿 @Document 元注解來說吧。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
           

不管是這裡的 Documented 還是我們自定義的注解,都需要使用 @interface 辨別,使用了這個辨別,會自動繼承 java.lang.annotation.Annotation 接口,由編譯程式自動完成其它細節。你又可能會說了,為啥是這樣的辨別就會自動繼承這個接口呀。額,這是人家的事先約定的規範,你如果你是 JDK 源碼的開發人員,也可以自己定義其它辨別,亦或是寫了一套比注解還好用的規範呢。

在定義注解時,不能繼承其它的注解或接口。@interface 用來聲明一個注解,其中的每一個方法實際上是聲明了一個配置參數。方法的名稱就是參數的名稱,傳回值類型就是參數的類型(傳回值類型隻能是基本類型、Class、String、enum)。可以通過 default 來聲明參數的預設值。

1、Annotation 類型裡面的參數該如何設定

  • 隻能用 public 或預設(default)這兩個修飾通路權限。例如 String value(); 這裡把方法設為 defaul 預設類型。
  • 參數成員隻能用【char、byte、short、int、long、float、double、boolean】八種基本資料類型和 String、Enum、Class 和 annotations 等資料類型,以及這一些類型的數組。例如 String value(); 這裡的參數成員就為 String。
  • 如果隻有一個參數成員,最好把參數名稱設為 "value",後加小括号。

2、元注解的用途

在詳細說這四個中繼資料的含義之前,先來看一個在工作中會經常使用到的 @Autowired 注解,此注解中使用到了 @Target、@Retention、@Documented 這三個元注解 。

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}
           

2.1 @Target 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}
           

@Target 注解,是專門用來限定某個自定義注解能夠被應用在哪些 Java 元素上面的,标明作用範圍;取值在 java.lang.annotation.ElementType 進行定義的。

public enum ElementType {
    /** 類,接口(包括注解類型)或枚舉的聲明 */
    TYPE,

    /** 屬性的聲明 */
    FIELD,

    /** 方法的聲明 */
    METHOD,

    /** 方法形式參數聲明 */
    PARAMETER,

    /** 構造方法的聲明 */
    CONSTRUCTOR,

    /** 局部變量聲明 */
    LOCAL_VARIABLE,

    /** 注解類型聲明 */
    ANNOTATION_TYPE,

    /** 包的聲明 */
    PACKAGE,

    /** 作用于類型參數(泛型參數)聲明 */
    TYPE_PARAMETER,

    /** 作用于使用類型的任意語句(不包括class) */
    TYPE_USE
}
           

根據此處可以知道 @Autowired 注解的作用範圍:

// 可以作用在 構造方法、方法、方法形參、屬性、注解類型 上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
           

2.2 @Retention 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}
           

@Retention 注解,翻譯為持久力、保持力。即用來修飾自定義注解的生命周期。

注解的生命周期有三個階段:

  • Java 源檔案階段
  • 編譯到 class 檔案階段
  • 運作期階段

同樣使用了 RetentionPolicy 枚舉類型對這三個階段進行了定義:

public enum RetentionPolicy {
    /**
     * 注解将被編譯器忽略掉
     */
    SOURCE,

    /**
     * 注解将被編譯器記錄在class檔案中,但在運作時不會被虛拟機保留,這是一個預設的行為
     */
    CLASS,

    /**
     * 注解将被編譯器記錄在class檔案中,而且在運作時會被虛拟機保留,是以它們能通過反射被讀取到
     */
    RUNTIME
}
           

再較長的描述下這三個階段:

  • 如果被定義為 RetentionPolicy.SOURCE,則它将被限定在 Java 源檔案中,那麼這個注解即不會參與編譯也不會在運作期起任何作用,這個注解就和一個注釋是一樣的效果,隻能被閱讀 Java 檔案的人看到;
  • 如果被定義為 RetentionPolicy.CLASS,則它将被編譯到 Class 檔案中,那麼編譯器可以在編譯時根據注解做一些處理動作,但是運作時 JVM(Java虛拟機)會忽略它,并且在運作期也不能讀取到;
  • 如果被定義為 RetentionPolicy.RUNTIME,那麼這個注解可以在運作期的加載階段被加載到 Class 對象中。那麼在程式運作階段,可以通過反射得到這個注解,并通過判斷是否有這個注解或這個注解中屬性的值,進而執行不同的程式代碼段。

注意:實際開發中的自定義注解幾乎都是使用的 RetentionPolicy.RUNTIME 。

2.3 @Documented 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
           

@Documented 注解,是被用來指定自定義注解是否能随着被定義的 java 檔案生成到 JavaDoc 文檔當中。

2.4 @Inherited 元注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
           

@Inherited 注解,是指定某個自定義注解如果寫在了父類的聲明部分,那麼子類的聲明部分也能自動擁有該注解。

@Inherited 注解隻對那些 @Target 被定義為 ElementType.TYPE 的自定義注解起作用。

三、如何自定義注解

上面把注解與元注解說完了,那得實戰一下吧。其實很多人在工作中已經用到過了或者自己沒用到過但項目中有用到過。但你有沒有想過自定義注解是怎麼關聯到目标方法的,是的沒錯,就是我們開頭講的定義,通過反射。

這裡我就拿一個我們項目中自定義注解的例子來說:

1、标記日志列印的自定義注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrintLog {
}
           

2、定義一個切面,在切面中對使用了 @PrintLog 自定義注解的方法進行環繞增強通知

@Component
@Aspect
@Slf4j
public class PrintLogAspect {
    @Around(value = "@annotation(com.riemann.core.annotation.PrintLog)")
    public Object handlerPrintLog(ProceedingJoinPoint joinPoint) throws Throwable {
        String clazzName = joinPoint.getSignature().getDeclaringTypeName();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();

        Map<String, Object> nameAndArgs = getFieldsName(this.getClass(), clazzName, methodName, args);
        log.info("Enter class[{}] method[{}] params[{}]", clazzName, methodName, nameAndArgs);

        Object object = null;
        try {
            object = joinPoint.proceed();
        } catch (Throwable throwable) {
            log.error("Process class[{}] method[{}] error", clazzName, methodName, throwable);
        }
        log.info("End class[{}] method[{}]", clazzName, methodName);
        return object;
    }

    private Map<String, Object> getFieldsName(Class clazz, String clazzName, String methodName, Object[] args) throws NotFoundException {
        Map<String, Object > map = new HashMap<>();
        ClassPool pool = ClassPool.getDefault();
        ClassClassPath classPath = new ClassClassPath(clazz);
        pool.insertClassPath(classPath);

        CtClass cc = pool.get(clazzName);
        CtMethod cm = cc.getDeclaredMethod(methodName);
        MethodInfo methodInfo = cm.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        if (attr == null) {
            // exception
        }
        int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
        for (int i = 0; i < cm.getParameterTypes().length; i++) {
            map.put( attr.variableName(i + pos), args[i]);
        }

        return map;
    }
}
           

3、最後,在 Controller 中的方法上使用 @PrintLog 自定義注解即可;當某個方法上使用了自定義注解,那麼這個方法就相當于一個切點,那麼就會對這個方法做環繞(方法執行前和方法執行後)增強處理。

@RestController
public class Controller {
    @PrintLog
    @GetMapping(value = "/user/findUserNameById/{id}", produces = "application/json;charset=utf-8")
    public String findUserNameById(@PathVariable("id") int id) {
        // 模拟根據id查詢使用者名
        String userName = "公衆号【老周聊架構】";
        return userName;
    }
}
           

4、在浏覽器中輸入網址: http://127.0.0.1:8080/api/user/findUserNameById/666 回車後觸發方法執行,發現控制台列印了日志

Enter class[Controller] method[findUserNameById] params[{id=666}]
End class[Controller] method[findUserNameById]
           

這樣的話,項目中的 Controller 類的請求日志我們不必每個方法都列印一遍了,而且收集日志到日志中心請求的參數也有具體統一的格式,排查問題也友善了不少。使用自定義注解 + AOP 實作日志的列印,有木有如絲滑般順暢的感覺,哈哈,這樣代碼看着也優雅了不少。

這裡要說一下,寫這篇文章的初衷是我一個好哥們在群裡提了這麼個問題,找不到自定義注解和相關方法的關聯。我第一時間想到的是反射,然後再想想自己項目中的場景,雖說是 AOP 實作,但注解是如何通過反射擷取值的呢?AOP 切面織入底層是如何實作的呢?有了這兩點疑問,是以老周才下寫下這篇文章,那下來兩篇就會對這個兩個疑點進行揭秘,敬請期待。

歡迎大家關注我的公衆号【老周聊架構】,Java後端主流技術棧的原理、源碼分析、架構以及各種網際網路高并發、高性能、高可用的解決方案。

喜歡的話,一鍵三連走一波。

繼續閱讀