Java: 20 分鐘搞定註解

什麼是註解?

Java 註解是在 JDK5 時引入的新特性,註解(也被稱爲元數據)爲我們在代碼中添加信息提供了一種形式化的方法,使我們可以在稍後某個時刻非常方便地使用這些數據。

註解的本質?

新建一個註解類如:

public @interface myAnotation {
}

使用 javac 命令編譯該 java 文件:

javac myAnotation.java

再使用 javap 命令反編譯 calss 文件

javap myAnotation.class

結果如下圖:

通過反編譯這個 class 文件可以很清晰地看到其實註解默認繼承了 Anotation 接口

一個註解最基本的組成部分

@Target(....)// 1
@Retention(...) //2
@Documented // 3
public @interface Annotation_name { // Annotation_name4
    // 註解要實現的功能 5
}
  1. @Target()

指定註解的作用域,括號中的值就代表可用在哪些地方,如下枚舉類

public enum ElementType {
    /**
     * 類、接口(包括註釋類型)或枚舉聲明
     */
    TYPE,

    /**
     * 字段聲明(包括枚舉常量)
     */
    FIELD,

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

篇幅問題只展示以上三個作用域

2. @Retention()

註解的生命週期,參數類型爲

  1. RetentionPolicy.SOURCE

  2. RetentionPolicy.CLASS

  3. RetentionPolicy.RUNTIME

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     編譯時將會被丟棄
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time. This is the default
     * behavior.
     註解將由編譯器記錄在類文件中,但不需要在運行時被VM保留。這是默認值行爲
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     註解將由編譯器記錄在類文件中,並在運行時由VM保留,因此可以反射地讀取它們
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}
3. @Documented

在自定義註解的時候可以使用 @Documented 來進行標註,如果使用 @Documented 標註了,在生成 javadoc 的時候就會把 @Documented 註解給顯示出來,只是用來做標識,沒什麼實際作用,瞭解就好。

註解的屬性返回數據類型

通過上文我們知道註解其實就是一個默認繼承 Anotation 接口的接口,在註解中成員變量和成員方法也稱爲註解的屬性,註解的屬性返回值只能爲八大基本數據類型、數組、String、枚舉及以上類型的數組 如下:

//本註解可以使用在類上
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface myAnotation {
    String name();
    // 定義註解屬性默認值爲12
    int age() default 12;

    // 定義一個數組,使用時必填
    String[] like();

    // 定義一個數組,默認爲空,使用時選填
    String[] boll() default {};

    // 定義一個布爾值類型屬性
    boolean isMan() default true;

    // 定義一個枚舉值
    ElementType TYPE();

    // 錯誤定義示範
    //void show();
}

通過反射解析上文自定義註解 myAnotation 獲取註解的屬性值:

運行結果如圖:

通過上文學習,我們對如何定義一個自定義註解和如何獲取自定義註解屬性值已經有所瞭解,那現在我們繼續往下學習

註解結合 Aop 使用案例

場景:在自定義註解標記的方法執行前獲取到註解屬性值

idea 新建 Spring Initializr 項目,不用添加任何其他依賴(步驟省略)

  1. 根據 Spirng 版本手動新增 aop 依賴如下:
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
        <version>2.4.5</version>
    </dependency>
  1. 自定義註解:這個註解中我們定義了一個 String 類型的屬性,默認值爲 “張三”

  1. 使用註解:在 test 方法中我們使用了自己的定義的註解並且指定屬性值爲 “李四”,並且在類上添加了 @Component 註解,這是的意圖下文會說

  1. 新建切面類,如下:

在切面類我們定義了三個方法切入點、前置通知、後置通知、並在前置通知方法中,通過方法簽名獲取註解屬性。

  1. 在此我們就不啓動整個 SpringBoot 項目了,我們通過註解創建出容器,並把相關的方法通過 @Component 註解註冊近容器中,創建一個容器配置類,聲明容器掃描路徑如下

  1. 創建 main 方法,並且配置如下:

目前的項目目錄如下:

運行 main 方法,控制檯打印如下:

在以上例子中我們不僅學習到了切面和註解的結合使用,還體驗了一把個悲劇註解自己創建 Spring 容器把相關 bean 注入容器中並獲取 bean,執行 bean 的知識知識!!

到這我們學會了如何在切面中獲取自定義註解的知識,但是本篇文章並不會到此就早早收場,我們再通過一個案例來深入理解自定義註解和 AOP 的使用

終極目標,通過自定義註解和 aop 實現數據庫動態數據源的切換

  1. 添加如下依賴:

  1. 在 application.yml 文件中配置多數據源,如圖

在上圖中我們配置了 master 和 slave 兩個數據源,分別指定不同的機器上的數據庫

  1. 數據表準備,這裏的表就很簡單了在 133 和 132 數據庫上有相同的數據庫 test 和相同的數據表 user, 但是表中的數據並不相同,這裏也是爲了區分數據源切換是否成功,表如下:

  1. 新建 User 實體類,創建 UserService、UserServiceImpl、controller、mapper、UserMapper.xml 這些常規操作對於大家都很簡單了,這裏不一一講解了直接貼個項目目錄結構圖,如下:

  1. 自定義註解用於方法上根據註解屬性值切換數據源類型

在上圖中我們定義了一個 DataSourceType 的枚舉類,定義了一個 DataSourceAnnotaion 註解,註解的屬性值就是定義的枚舉類,而且默認值爲 MASTER

  1. 每一個線程訪問數據庫操作方法時指定的數據源都必須是私有的,這裏我們用 ThrealLocal 來存儲每個線程訪問數據庫時指定的數據源類型

上圖中我們能定義了一個 ThreadLocal 變量,提供了從 ThreadLocal 存、取、刪這三個方法。

  1. 定義數據源切換的路由類,新建類繼承 AbstractRoutingDataSource,重寫 determineCurrentLookupKey() 方法,如圖

  1. 新建數據源配置類,分別根據註解創建數據源對象如圖:

方法中,我們把主從數據源對應的枚舉類型作爲 key, 對應的數據源對象作爲 value 存入 map 中返回創建了 DynamicDataSource 對象默認數據庫爲 master,在 SqlSessionFactoryBean 方法中我們需要重新指定 mapper.xml 文件的路徑,不然這裏運行時報找不到指定 mapper, 我們還添加了 platformTransactionManager 事務管理器。

  1. 新建切面類用於攔截有註解標識的方法,根據註解屬性值切換數據庫如圖:

如上圖所示我們在環繞通知中,根據方法上註解的屬性值將相對應的數據源枚舉類存入到 ThreadLocal 中,return.ponit.procees() 這個方法就是執行我們操作數據庫的方法,而在操作完數據庫中我們在當前線程的 ThreadLocal() 中刪除所以數據源標識。完成的多數據源切換的類代碼結構如下:

我們來捋一下這個數據源操作的執行過程

  1. 在項目啓動時 DruidConfig 類將會首先將讀取配置文件中的數據源配置創建出 master 和 slave 兩個數據源對象。

  2. DruidConfig 類中 dataSource() 將數據源枚舉值和數據源對象存入 map 中,並執行 DynamicDataSource 的構造方法,並把數據源集合和目標數據源傳給 AbstractRoutingDataSource 中的對象

  3. 當我們調用有註解標識的方法時,切面類 DataSourceAspect 獲取方法上的註解屬性類型,存入到當前線程的 ThrealLocal 對象中,此時將會調用 AbstractRoutingDataSource 類的 determineCurrentLookupKey() 方法,這個方法將對應的數據源對象返回,之後切面類環繞通知方法調用 point.proceed(); 在上文中我們說過調用這個方法其實就是 aop 執行我們操作數據庫的方法,在執行完數據庫方法後在當前線程 ThreadLocal 中把標識清除。

  4. 在 controller 中創建訪問數據庫的接口如圖:

在上圖中我們寫了三個方法,默認方法不加註解,添加了 master 屬性的方法,和添加了 slaves 屬性的方法,現在我們分別測試這三個方法訪問數據表的情況:

調用默認接口, 這個接口方法中我們並沒有只從任何的數據源,所以默認爲 master 數據源

在 determineCurrentLookupKey() 方法中監聽到的日誌如下

調用指定 master 數據源的接口

在 determineCurrentLookupKey() 方法中監聽到的日誌如下

調用指定 slave 數據源的接口

在 determineCurrentLookupKey() 方法中監聽到的日誌如下

從以上圖中可以看到我們所配置的多數據源切換是成功的,到此測試結束。

總結:在本篇文章中我們可以很直接的認識到 aop 和註解的強大功能,學好這些知識對於我們自身的技能也是有所提高的,而在 mybatis-plus 動態數據源切換中也是用到了 aop 的和註解的知識,通過以上知識我們也能對它的實現原理有了一個大概的認識,本篇文章只是作爲一個引子還有很多可擴展的地方,比如我們定義有 master 註解的方法執行數據庫寫操作,而有 slave 註解的數據庫操作爲讀數據庫的操作,這樣我們也是能粗略的模仿出數據庫讀寫分離的框架了,當然讀寫分離不僅僅是代碼多數據源上的切換,還要配合 mysql 端的主從複製,基礎知識很重要,也許你不能創造出一個新的框架,但是這些知識將會在你接觸新框架時讓你很快上手。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8OBio0LnE2QZf4jiYhtaTA