面試官:註解五問你怕了嗎?

山禾說發表於2020-11-03

1. 註解是什麼

首先,我們先來康康註解在百度百科上的解釋

而在 Java 中,簡單通俗的講,就是一個標籤,對類、方法、變數的一個解釋說明,在早些年,我們通常使用 xml 去對我們的程式碼進行增強的解釋,但是格式繁雜,程式碼可讀性差,維護起來很困難,在 Java SE 5.0 以後,註解的出現為這種情況得到了改善,越來越多的開源專案開始使用註解,拋棄了 xml 。
xml 就像一段程式碼的補充解釋和說明,是一段單獨的文件,比如我們 Spring 專案中使用 xml 配置 Bean 的作用域,而註解是寫在程式碼旁邊,對程式碼進行標記和進行進一步的解釋。

  • xml 配置 Bean
<bean name="user" class="shanhe.show.User" scope="prototype"
</bean>
  • 註解配置 Bean
@Bean
public class User{}

2. 註解該怎麼用

我們使用註解的方法非常的簡單,可以分別這樣去用

@Data
public class User{}

方法

@Override
public String print(){}

變數

@Notnull
private String id;

引數

String getIdByName(@Param("name") String name);

其使用的簡單和方便其實也是我們選擇使用的註解的最大原因:)

3. 自定義註解

要想真正的理解註解的實現原理,首先我們要學會自己去實現一個自定義的註解,當我們得到一個自己的註解之後,相信可以對註解的理解有更為深刻的認識。
自定義註解之前,我們先來認識幾個定義註解的註解——元註解
@Target
@Retention
@Docuemented
Inherited
通過這四個元註解,我們就可以去自定義一個我們想要的註解,首先來分別解釋一下,這四個元註解在構建自定義註解的時候起到的作用
@Target正如它的名字那樣,它是用於限定這個自定義註解能夠應用的 Java 元素,在這個註解中維護著一個列舉類:

public enum ElementType {
    /** 類,介面(包括註解型別)或列舉的宣告 */
    TYPE,

    /** 屬性的宣告 */
    FIELD,

    /** 方法的宣告 */
    METHOD,

    /** 方法形式引數宣告 */
    PARAMETER,

    /** 構造方法的宣告 */
    CONSTRUCTOR,

    /** 區域性變數宣告 */
    LOCAL_VARIABLE,

    /** 註解型別宣告 */
    ANNOTATION_TYPE,

    /** 包的宣告 */
    PACKAGE
}

其使用的方法如下

@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface AnnotionDemo {
    String value();
}

@Retention註解是對自定義註解的生命週期進行限定,分為了原始檔、編譯期、執行期這三類,同樣的,它也有一個好搭檔——列舉類去維護這三個階段。

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

    /**
     * 註解將被編譯器記錄在class檔案中,但在執行時不會被虛擬機器保留,這是一個預設的行為
     */
    CLASS,

    /**
     * 註解將被編譯器記錄在class檔案中,而且在執行時會被虛擬機器保留,因此它們能通過反射被讀取到
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

我們實際開發中使用自定義註解的時候,最常使用的還是RUNTIME型別,我們可以通過另外一個神器——反射去獲取到我們自定義註解的相關內容,從而對這些不同的內容進行不同的判斷,後面專案實戰部分,會舉例說明實際使用的方法

@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotionDemo {
    String value();
}

@Document註解相對來說就比較簡單了,它只是用來指定自定義註解能否跟隨著它被使用的 Java 檔案一起生成到 JavaDoc 中,就目前來看,這個元註解對於我們的作用並不是很大。
@Inherited的使用則是有條件限制,只有當ElementTypeTYPE的時候才會生效,而它的作用就是將父類的作用域擴充到子類中,是子類可以去繼承原本處於父類上的註解。
所以綜上所述,我們就可以運用元註解去自定義出一個屬於我們自己的註解:

@Document
@Inherited
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotionDemo {
    String value();
}

4. 註解實現原理

首先,在 Java 的文件中,我找到了這樣的一句話:

The direct superinterface of every annotation type is java.lang.annotation.Annotation
意思就說,我們不管是自定義的註解也好,JDK中原生的註解也好,都是繼承自Annotation這個介面的,也就是說我們上面自定義的註解經過了編譯器編譯後,大概是這個樣子的

@Document
@Inherited
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public interface AnnotionDemo extends Annotation {
    String value();
}

那麼我們是使用註解的時候,怎麼去給註解中的value賦值呢?
我們使用該註解後,通過反編譯後的程式碼,我們可以找到(這裡就不貼出反編譯後的程式碼了,浪費空間,大家知道怎麼回事就好~)在堆記憶體中有一個代理物件,大概長下面這樣

public final class com.sun.proxy.$Proxy1 extends java.lang.reflect.Proxy implements java.lang.annotation.Annotation

然後在這個代理類中去完成了對value的賦值,而執行這一系列動作的是一個叫做AnnotationInvocationHandler的東西,它完成了在執行期間生成動態代理物件的操作,整體的流程大概是這樣的

5.在專案中我們如何去使用註解?

下面,我們通過一個實際應用的一個小?去看一下

我們在使用系統的時候,通常會有許可權的控制,在專案中,我們會在 gateway 中去設定過濾器,通過過濾請求之中的token,獲取對應的使用者資訊,從而拿到使用者的許可權,完成對許可權的控制,但是有一些介面是處於非登入狀態(即沒有token的時候)也需要去訪問的,而這些介面並非固定一成不變的,這個時候,我們就需要一個標誌,也就是註解去註明,哪些介面的訪問是無需進行許可權的,相當於頒發了一個綠牌,可以跳過許可權的控制。

自定義@Pass註解

/**
 * 既可以作用於方法上,也可以作用於類上,作用於類上時,該類下的所有介面均可跳過
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Pass {
    boolean required() default true;
}

處理方式

// 如果不是對映到方法直接通過
if (!(object instanceof HandlerMethod)) {
    return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//檢查是否有pass註釋,有則跳過認證
if (method.isAnnotationPresent(Pass.class)) {
    Pass pass = method.getAnnotation(Pass.class);
    if (pass.required()) {
        return true;
    }
}

在攔截器中獲取Method方法,通過反射去獲取註解中的值,這樣就可以跳過過濾直接去訪問介面啦,具體使用方法如下:

@Pass
@GetMapping("hello")
public String hello(){
    return "hello";
}

相信我聰明的讀者一定可以舉一反三,使用註解去巧妙的實現更多的功能,本次註解的相關內容就到此為止了~

如果你有學到,請給我點贊+關注,原創不易,且看且珍惜。

相關文章