簡單易懂講註解

Cyrus_sss發表於2021-10-28

註解是什麼

簡單的說,註解就是一種將後設資料資訊從 xml 剝離開來,然後儲存在 java 原始碼中,這將使得程式碼更加清晰易懂,無需維護兩個地方: java 原始碼以及 xml 配置檔案。

典型的場景就是 spring 框架,我們都知道,spring 框架將一個 bean 儲存在容器裡有兩種方式,一種是採用配置檔案的方式生成 bean 並且儲存在容器中,使用的時候通過 bean 工廠拿對應的 bean 例項即可。這種方式很繁瑣,不僅需要維護 java 原始碼,還需要在 xml 配置裡再維護一遍。另一種方式是採用註解的方式,在類名上使用 @Component或者@Service(當然還有其他方式,但不是本篇文章的重點)。然後在使用的時候採用 @Autowired 形式注入即可。這樣就無需繁瑣的 xml 配置。(例子)

當然,採用傳統 xml 維護元素據還是使用註解,各有優劣,需要根據實際場景進行評估。

如何使用註解

接下來我們先從一個簡單的註解定義開始,然後介紹一些註解的關鍵屬性

定義註解

如下例子,Test 註解看起來很像介面的定義,註解和其他介面和類一樣,都會被編譯成 class 檔案。像這種不含任何元素的被稱為標記註解,如 java 8 新加入的用於宣告一個介面時函式式介面的註解:@FunctionalInterface。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    
}

當然,註解也是可以定義一些屬性的。如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    int value();
    String name() default "-- default --";
}

其中 value() 以及 name() 就是該註解的屬性,其中 value() 沒有預設值,那麼在使用該註解的時候,必須指定 value 屬性,name 有個預設值,使用的時候可以不需要指定預設值。

如下例子,就是該註解的使用方式,在 1 處,由於沒有指定 value 屬性,所以編譯失敗。

public class AnnotationDemo {

    @Test(1)
    private int value;
    
    @Test() // 1 編譯失敗
    private int withoutValue;

    @Test(2)
    private String withoutName;

    @Test(value = 3, name = "name")
    private String name;
}

這個註解現在來說是沒有一絲絲意義的,因為我們還沒有為其編寫註釋處理器,註釋處理器在後面會介紹。

@Test 註解中使用到的 @Target 註解、@Retention 註解以及他們的引數列舉,會在下文的元註解中介紹。

常見註解

常見的註解這裡主要介紹 jdk 的註解

  • @Override:表示當前的方法定義將覆蓋基類的方法。如果你不小心拼寫錯誤,或者方法簽名被錯誤拼寫的時候,編譯器就會發出錯誤提示。
  • @Deprecated:表示當前類 or 方法 or 欄位被棄用了,不應該再使用了,使用會產生告警
  • @SuppressWarnings:關閉不當的編譯器警告資訊。
  • @FunctionalInterface:Java 8 中加入用於表示型別宣告為函式式介面

元註解

上文的 @Test 註解中,我們使用到了 @Target 註解、@Retention 註解,這兩個註解為元註解,

目前一共有 5 個元註解:

註解 解釋
@Target 表示註解可以用於哪些地方。可能的 ElementType 引數包括:CONSTRUCTOR:構造器的宣告 FIELD:欄位宣告(包括 enum 例項) LOCAL_VARIABLE:區域性變數宣告 METHOD:方法宣告 PACKAGE:包宣告 PARAMETER:引數宣告 TYPE:類、介面(包括註解型別)或者 enum 宣告
@Retention 表示註解資訊儲存的時長。可選的 RetentionPolicy 引數包括: SOURCE:註解將被編譯器丟棄 CLASS:註解在 class 檔案中可用,但是會被 VM 丟棄。 RUNTIME:VM 將在執行期也保留註解,因此可以通過反射機制讀取註解的資訊。
@Documented 將此註解儲存在 Javadoc 中
@Inherited 允許子類繼承父類的註解
@Repeatable 允許一個註解可以被使用一次或者多次(Java 8)

使用最多的其實就是 @Target 以及 @Retention。

@Targe:註解中指定的每一個 ElementType 就是一個約束,它告訴編譯器,這個自定義的註解只能用於指定的型別。你可以指定 enum ElementType 中的一個值,或者以逗號分割的形式指定多個值。如果想要將註解應用於所有的 ElementType,那麼可以省去 @Target 註解,但是這並不常見。

@Retention:表明註解存在的時長,使用最多的是 RUNTIME,使用 RUNTIME 的時候,註解在執行期也保留著,這時就可以通過反射機制讀取註解資訊,如果使用 SOURCE,CLASS,那麼就無法通過反射獲取。

註解處理器

單獨定義一個註釋是沒什麼意義的,我們要給一個註釋賦予意義,那麼就得 coding,給這個註釋編寫一個註解處理器。這裡我僅演示最簡單的註解處理器。

這個列子很簡單,定義了一個註解 @Test,該註解可以在方法上使用,可以被帶入到執行時。AnnotationDemo 類實現了 Interface 介面,demo1()、demo2()、demo3()使用了註解,其中 demo3() 使用預設值,demo4() 沒有引入註解。這裡實現介面的原因是為了使用動態代理來呼叫方法,處理註解的邏輯寫在動態代理裡。動態代理類 InvokeClass,可以看到 invoke 方法裡拿到 obj 對應的方法(這裡不直接用入參的 method 欄位是因為該欄位代表介面方法,介面方法沒有加註解,獲取到的 Test annotation 會為空),這裡拿到了方法上的註解資訊後可以編寫自己想要的處理邏輯,我這邊就簡單把 @Test 註解的 value() 值列印出來。

動態代理文章可以看:簡單易懂將反射

(這裡字串判空寫的有點醜了,是因為我沒引入對應工具類)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value() default "--- default ---";
}
interface Interface {

    void demo1();

    void demo2();

    void demo3();

    void demo4();
}


public class AnnotationDemo implements Interface {

    @Test("demo1")
    @Override
    public void demo1() {

    }

    @Test("demo2")
    @Override
    public void demo2() {

    }

    @Test
    @Override
    public void demo3() {

    }

    @Override
    public void demo4() {

    }
}
class InvokeClass implements InvocationHandler {

    Object obj;

    public InvokeClass(Object obj) {
        this.obj = obj;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method objectMethod = obj.getClass().getMethod(method.getName());
        Test annotation = objectMethod.getAnnotation(Test.class);
        if (Objects.nonNull(annotation) && !"".equals(annotation.value())) {
            System.out.println(annotation.value());
        }
        return method.invoke(obj, args);
    }
}


public class Main {

    public static void main(String[] args) {

        Interface anInterface = (Interface) Proxy.newProxyInstance(Main.class.getClassLoader(), new Class[]{Interface.class}, new InvokeClass(new AnnotationDemo()));
        anInterface.demo1();
        anInterface.demo2();
        anInterface.demo3();
        anInterface.demo4();
    }

}

輸出:

demo1
demo2
--- default ---

Spring 如何自定義註解

在 spring 中使用自定義註解一般是配合 aop 使用的。

如下,還是註解 @Test ,有個 AnnotationDemo 類,在方法上使用了註解,並且將自身注入 spring 容器 (@Service),並且通過實現 BeanFactoryAware 介面,在初始化的時候呼叫 setBeanFactory 方法,這裡通過傳入的 bean 工廠獲取到 bean 並且呼叫方法。

定義一個切面 AspectDemo,切點 pointcut 為我們自定義的註解類,增強 advice 是列印了 @Test 註解的 value() 資訊。這樣當呼叫了使用了 @Test 的註解的方法的時候,就是會列印對應的 value() 資訊。啟動專案,由於在 setBeanFactory 方法中呼叫了 AnnotationDemo 類的幾個方法,因此列印出了對應的註解的 value 資訊。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value() default "--- default ---";
}
@Service
public class AnnotationDemo implements BeanFactoryAware {

    @Test("demo1")
    public void demo1() {

    }

    @Test("demo2")
    public void demo2() {

    }

    @Test
    public void demo3() {

    }

    public void demo4() {

    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        AnnotationDemo demo = beanFactory.getBean(AnnotationDemo.class);
        demo.demo1();
        demo.demo2();
        demo.demo3();
        demo.demo4();
    }
}
@Component
@Aspect
public class AspectDemo {

    @Pointcut("@annotation(com.example.spring_project.Test)")
    private void pointcut() {}

    @Before("pointcut() && @annotation(test)")
    public void advice(Test test) {
        System.out.println(test.value());
    }

}

輸出:

demo1
demo2
--- default ---

文章為本人學習過程中的一些個人見解,漏洞是必不可少的,希望各位大佬多多指教,幫忙修復修復漏洞!!!

進入本人語雀文件體驗更好哦
https://www.yuque.com/docs/share/d1d3f7bb-0918-4844-a870-fc50eb0da707?# 《註解》

參考資料:java 程式設計思想

相關文章