1. 程式人生 > >不吹牛逼,擼個註解有什麼難的

不吹牛逼,擼個註解有什麼難的

註解是 Java 中非常重要的一部分,但經常被忽視也是真的。之所以這麼說是因為我們更傾向成為一名註解的使用者而不是建立者。@Override 註解用過吧?@Service 註解用過吧?但你知道怎麼自定義一個註解嗎?

恐怕你會搖搖頭,擺擺手,不好意思地承認自己的確沒有自定義過。

01、註解是什麼

註解(Annotation)是在 Java 1.5 時引入的概念,同 class 和 interface 一樣,也屬於一種型別。註解提供了一系列資料用來裝飾程式程式碼(類、方法、欄位等),但是註解並不是所裝飾程式碼的一部分,它對程式碼的執行效果沒有直接影響(這句話怎麼理解呢?),由編譯器決定該執行哪些操作。

來看一段程式碼,我隨便寫的,除了列印到控制檯的那句宣傳語,其他都不重要,嘻嘻。

public class AutowiredTest {
    @Autowired
    private String name;

    public static void main(String[] args) {
        System.out.println("沉默王二,一枚有趣的程式設計師");
    }
}

注意到 @Autowired 這個註解了吧?它本來是為 Spring 容器注入 Bean 的,現在被我無情地扔在了成員變數 name 的身上,但這段程式碼所在的專案中並沒有啟用 Spring,意味著 @Autowired 註解此時只是一個擺設。

我之所以舉這個無聊的例子就是為了證明一個觀點:註解對程式碼的執行效果沒有直接影響,明白我的用意了吧?

02、註解的生命週期

註解的生命週期有 3 種策略,定義在 RetentionPolicy 列舉中。

1)SOURCE:在原始檔中有效,被編譯器丟棄。

2)CLASS:在編譯器生成的位元組碼檔案中有效,但在執行時會被處理類檔案的 JVM 丟棄。

3)RUNTIME:在執行時有效。這也是註解生命週期中最常用的一種策略,它允許程式通過反射的方式訪問註解,並根據註解的定義執行相應的程式碼。

03、註解裝飾的目標

註解的目標定義了註解將適用於哪一種級別的 Java 程式碼上,有些註解只適用於方法,有些只適用於成員變數,有些只適用於類,有些則都適用。

截止到 Java 9,註解的型別一共有 11 種,定義在 ElementType 列舉中。

1)TYPE:用於類、介面、註解、列舉

2)FIELD:用於欄位(類的成員變數),或者列舉常量

3)METHOD:用於方法

4)PARAMETER:用於普通方法或者構造方法的引數

5)CONSTRUCTOR:用於構造方法

6)LOCAL_VARIABLE:用於變數

7)ANNOTATION_TYPE:用於註解

8)PACKAGE:用於包

9)TYPE_PARAMETER:用於泛型引數

10)TYPE_USE:用於宣告語句、泛型或者強制轉換語句中的型別

11)MODULE:用於模組

04、開始擼註解

說再多,都不如擼個註解來得讓人心動。擼個什麼樣的註解呢?一個欄位註解吧,它用來標記物件在序列化成 JSON 的時候要不要包含這個欄位。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}

1)JsonField 註解的生命週期是 RUNTIME,也就是執行時有效。

2)JsonField 註解裝飾的目標是 FIELD,也就是針對欄位的。

3)建立註解需要用到 @interface 關鍵字。

4)JsonField 註解有一個引數,名字為 value,型別為 String,預設值為一個空字串。

為什麼引數名要為 value 呢?有什麼特殊的含義嗎?

當然是有的,value 允許註解的使用者提供一個無需指定名字的引數。舉個例子,我們可以在一個欄位上使用 @JsonField(value = "沉默王二"),也可以把 value = 省略,變成 @JsonField("沉默王二")

default "" 有什麼特殊含義嗎?

當然也是有的,它允許我們在一個欄位上直接使用 @JsonField,而無需指定引數的名和值。

05、使用註解

是騾子是馬拉出來遛遛,對吧?現在 @JsonField 註解已經擼好了,接下來就到了怎麼使用它的環節。

假設有一個作者類,他有 3 個欄位,分別是 age、name 和 bookName,後 2 個是必須序列化的欄位。

public class Writer {
    private int age;

    @JsonField("writerName")
    private String name;

    @JsonField
    private String bookName;

    public Writer(int age, String name, String bookName) {
        this.age = age;
        this.name = name;
        this.bookName = bookName;
    }

    // getter / setter

    @Override
    public String toString() {
        return "Writer{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", bookName='" + bookName + '\'' +
                '}';
    }
}

1)name 上的 @JsonField 註解提供了顯式的字串值。

2)bookName 上的 @JsonField 註解使用了預設項。

接下來,我們來編寫序列化類 JsonSerializer,內容如下:

public class JsonSerializer {
    public static String serialize(Object object) throws IllegalAccessException {
        Class<?> objectClass = object.getClass();
        Map<String, String> jsonElements = new HashMap<>();
        for (Field field : objectClass.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(JsonField.class)) {
                jsonElements.put(getSerializedKey(field), (String) field.get(object));
            }
        }
        return toJsonString(jsonElements);
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }

    private static String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet()
                .stream()
                .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
                .collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }
}

JsonSerializer 類的內容看起來似乎有點多,但不要怕,我一點點來解釋,直到你搞明白為止。

1)serialize() 方法是用來序列化物件的,它接收一個 Object 型別的引數。objectClass.getDeclaredFields() 通過反射的方式獲取物件宣告的所有欄位,然後進行 for 迴圈遍歷。在 for 迴圈中,先通過 field.setAccessible(true) 將反射物件的可訪問性設定為 true,供序列化使用(如果沒有這個步驟的話,private 欄位是無法獲取的,會丟擲 IllegalAccessException 異常);再通過 isAnnotationPresent() 判斷欄位是否裝飾了 JsonField 註解,如果是的話,呼叫 getSerializedKey() 方法,以及獲取該物件上由此欄位表示的值,並放入 jsonElements 中。

2)getSerializedKey() 方法用來獲取欄位上註解的值,如果註解的值是空的,則返回欄位名。

3)toJsonString() 方法藉助 Stream 流的方式返回格式化後的 JSON 字串。如果對 Stream 流比較陌生的話,請查閱我之前寫的 Stream 流入門。

看完我的解釋,是不是豁然開朗了?

接下來,我們來寫一個測試類 JsonFieldTest,內容如下:

public class JsonFieldTest {
    public static void main(String[] args) throws IllegalAccessException {
        Writer cmower = new Writer(18,"沉默王二","Web全棧開發進階之路");
        System.out.println(JsonSerializer.serialize(cmower));
    }
}

程式輸出結果如下:

{"bookName":"Web全棧開發進階之路","writerName":"沉默王二"}

從結果上來看:

1)Writer 類的 age 欄位沒有裝飾 @JsonField 註解,所以沒有序列化。

2)Writer 類的 name 欄位裝飾了 @JsonField 註解,並且顯示指定了字串“writerName”,所以序列化後變成了 writerName。

3)Writer 類的 bookName 欄位裝飾了 @JsonField 註解,但沒有顯式指定值,所以序列化後仍然是 bookName。

如果覺得文章對你有點幫助,請微信搜尋「 沉默王二 」第一時間閱讀,回覆【666】更有我為你精心準備的 500G 高清教學視訊(已分門別類)。本文 GitHub 已經收錄,有大廠面試完整考點,歡迎 Star。