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

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";
}

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

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