Java建立Annotation

註解是Java很強大的部分,但大多數時候咱們傾向於使用而不是去建立註解。例如,在Java源代碼裏不難找到Java編譯器處理的@Override註解, Spring框架的@Autowired註解, 或 Hibernate框架使用的@Entity 註解,但咱們不多看到自定義註解。雖然自定義註解是Java語言中常常被忽視的一個方面,但在開發可讀性代碼時它多是很是有用的資產,一樣有助於理解常見框架(如Spring或Hibernate)如何簡潔地實現其目標。
在本文中,咱們將介紹註解的基礎知識,包括註解是什麼,它們如何在示例中使用,以及如何處理它們。爲了演示註解在實踐中的工做原理,咱們將建立一個Javascript Object Notation(JSON)序列化程序,用於處理帶註解的對象並生成表示每一個對象的JSON字符串。在此過程當中,咱們將介紹許多常見的註解塊,包括Java反射框架和註解可見性問題。感興趣的讀者能夠在 GitHub找到已完成的JSON序列化程序的源代碼。

什麼是註解?

註解是應用於Java結構的裝飾器,例如將元數據與類,方法或字段相關聯。這些裝飾器是良性的,不會自行執行任何代碼,但運行時,框架或編譯器可使用它們來執行某些操做。更正式地說,Java語言規範(JLS) 第9.7節提供瞭如下定義:
註解是信息與程序結構相關聯的標記,但在運行時沒有任何影響。
請務必注意此定義中的最後一句:註解在運行時對程序沒有影響。這並非說框架不會基於註解的存在而改變其運行時行爲,而是包含註解自己的程序不會改變其運行時行爲。雖然這可能看起來是細微差異,但爲了掌握註解的實用性,理解這一點很是重要。
例如,某個實例的字段添加了@Autowired註解,其自己不會改變程序的運行時行爲:編譯器只是在運行時包含註解,但註解不執行任何代碼或注入任何邏輯來改變程序的正常行爲(忽略註解時的預期行爲)。一旦咱們在運行時引入Spring框架,咱們就能夠在解析程序時得到強大的依賴注入(DI)功能。經過引入註解,咱們已經指示Spring框架向咱們的字段注入適當的依賴項。咱們將很快看到(當咱們建立JSON序列化程序時)註解自己並無完成此操做,而是充當標記,通知Spring框架咱們但願將依賴項注入到帶註解的字段中。

Retention和Target

建立註解須要兩條信息:(1)retention策略和(2)target。保留策略(retention)指定了在程序的生命週期註解應該被保留多長時間。例如,註解能夠在編譯時或運行時期間保留,具體取決於與註解關聯的保留策略。從Java 9開始,有 三種標準保留策略,總結以下:

策略html

描述java

Sourcegit

編譯器會丟棄註解github

Classspring

註解是在編譯器生成的類文件中記錄的,但不須要在運行時處理類文件的Java虛擬機(JVM)保留。編程

Runtimejson

註解由編譯器記錄在類文件中,並由JVM在運行時保留api

正如咱們稍後將看到的,註解保留的運行時選項是最多見的選項之一,由於它容許Java程序反射訪問註解並基於存在的註解執行代碼,以及訪問與註解相關聯的數據。請注意,註解只有一個關聯的保留策略。
註解的目標(target)指定註解能夠應用於哪一個Java結構。例如,某些註解可能僅對方法有效,而其餘註解可能對類和字段都有效。從Java 9開始,有 11個標準註解目標,以下表所示:

目標數組

描述bash

Annotation Type

註解另外一個註解

Constructor

註解構造函數

Field

註解一個字段,例如類的實例變量或枚舉常量

Local variable

註解局部變量

Method

註解類的方法

Module

註解模塊(Java 9中的新增功能)

Package

註解包

Parameter

註解到方法或構造函數的參數

Type

註解一個類型,例如類,接口,註解類型或枚舉聲明

Type Parameter

註解類型參數,例如用做通用參數形式的參數

Type Use

註解類型的使用,例如當使用new關鍵字建立類型的對象時 ,當對象強制轉換爲指定類型時,類實現接口時,或者使用throws關鍵字聲明throwable對象的類型時(有關更多信息,請參閱Type Annotations and Pluggable Type Systems Oracle tutorial)

有關這些目標的更多信息,請參見 JLS的第9.7.4節。要注意,註解能夠關聯一個或多個目標。例如,若是字段和構造函數目標與註解相關聯,則能夠在字段或構造函數上使用註解。另外一方面,若是註解僅關聯方法目標,則將註解應用於除方法以外的任何構造都會在編譯期間致使錯誤。

註解參數

註解也能夠具備參數。這些參數能夠是基本類型(例如int或double),String,類,枚舉,註解或前五種類型中任何一種的數組(參見 JLS的第9.6.1節)。將參數與註解相關聯容許註解提供上下文信息或者能夠參數化註解的處理器。例如,在咱們的JSON序列化程序實現中,咱們將容許一個可選的註解參數,該參數在序列化時指定字段的名稱(若是沒有指定名稱,則默認使用字段的變量名稱)。

如何建立註解?

對於咱們的JSON序列化程序,咱們將建立一個字段註解,容許開發人員在序列化對象時標記要轉換的字段名。例如,若是咱們建立汽車類,咱們可使用咱們的註解來註解汽車的字段(例如品牌和型號)。當咱們序列化汽車對象時,生成的JSON將包括make和model鍵,其中值分別表明make和model字段的值。爲簡單起見,咱們假設此註解僅用於String類型的字段,確保字段的值能夠直接序列化爲字符串。
要建立這樣的字段註解,咱們使用@interface 關鍵字聲明一個新的註解:

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

咱們聲明的核心是public @interface JsonField,聲明帶有public修飾符的註解——容許咱們的註解在任何包中使用(假設在另外一個模塊中正確導入包)。註解聲明一個String類型value的參數,默認值爲空字符串。

請注意,變量名稱value具備特殊含義:它定義單元素註解( JLS的第9.7.3節),並容許咱們的註解用戶向註解提供單個參數,而無需指定參數的名稱。例如,用戶可使用@JsonField("someFieldName")而且不須要將註解聲明爲註解@JsonField(value = "someFieldName"),儘管後者仍然可使用(但不是必需的)。包含默認值空字符串容許省略該值,value若是沒有顯式指定值,則致使值爲空字符串。例如,若是用戶使用表單聲明上述註解@JsonField,則該value參數設置爲空字符串。
註解聲明的保留策略和目標分別使用@Retention和@Target註解指定。保留策略使用 java.lang.annotation.RetentionPolicy枚舉指定,幷包含三個標準保留策略的常量。一樣,指定目標爲 java.lang.annotation.ElementType枚舉,包括11種標準目標類型中每種類型的常量。
總之,咱們建立了一個名爲JsonField的public單元素註解,它在運行時由JVM保留,而且只能應用於字段。此註解只有單個參數,類型String的value,默認值爲空字符串。經過建立註解,咱們如今能夠註解要序列化的字段。

如何使用註解?

使用註解僅須要將註解放在適當的結構(註解的任何有效目標)以前。例如,咱們能夠建立一個Car類:

public class Car {
    @JsonField("manufacturer")
    private final String make;
    @JsonField
    private final String model;
    private final String year;

    public Car(String make, String model, String year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    public String getMake() {
        return make;
    }

    public String getModel() {
        return model;
    }

    public String getYear() {
        return year;
    }

    @Override
    public String toString() {
        return year + " " + make + " " + model;
    }
} 複製代碼

 

該類使用@JsonField註解的兩個主要用途:(1)具備顯式值,(2)具備默認值。咱們也可使用@JsonField(value = "someName")註解一個字段,但這種樣式過於冗長,並無助於代碼的可讀性。所以,除非在單元素註解中包含註解參數名稱能夠增長代碼的可讀性,不然應該省略它。對於具備多個參數的註解,須要顯式指定每一個參數的名稱來區分參數(除非僅提供一個參數,在這種狀況下,若是未顯式提供名稱,則參數將映射到value參數)。

鑑於@JsonField註解的上述用法,咱們但願將Car序列化爲JSON字符串{"manufacturer":"someMake", "model":"someModel"} (注意,咱們稍後將會看到,咱們將忽略鍵manufacturer 和model在此JSON字符串的順序)。在這以前,重要的是要注意添加@JsonField註解不會改變類Car的運行時行爲。若是編譯這個類,包含@JsonField註解不會比省略註解時加強類的行爲。類的類文件中只是簡單地記錄這些註解以及參數的值。改變系統的運行時行爲須要咱們處理這些註解。

如何處理註解?

處理註解是經過 Java反射應用程序編程接口(API)完成的。反射API容許咱們編寫代碼來訪問對象的類、方法、字段等。例如,若是咱們建立一個接受Car對象的方法,咱們能夠檢查該對象的類(即Car),並發現該類有三個字段:(1)make,(2)model和(3)year。此外,咱們能夠檢查這些字段以發現每一個字段是否都使用特定註解進行註解。
這樣,咱們能夠遍歷傳遞給方法的參數對象關聯類的每一個字段,並發現哪些字段使用@JsonField註解。若是該字段使用了@JsonField註解,咱們將記錄該字段的名稱及其值。處理完全部字段後,咱們就可使用這些字段名稱和值建立JSON字符串。
肯定字段的名稱須要比肯定值更復雜的邏輯。若是@JsonField包含value參數的提供值(例如"manufacturer"以前使用的@JsonField("manufacturer")),咱們將使用提供的字段名稱。若是value參數的值是空字符串,咱們知道沒有顯式提供字段名稱(由於這是value參數的默認值),不然,顯式提供了一個空字符串。後面這幾種狀況下,咱們都將使用字段的變量名做爲字段名稱(例如,在private final String model聲明中)。
將此邏輯組合到一個JsonSerializer類中:

public class JsonSerializer {
    public String serialize(Object object) throws JsonSerializeException {
        try {
            Class<?> objectClass = requireNonNull(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));
                }
            }
            System.out.println(toJsonString(jsonElements));
            return toJsonString(jsonElements);
        } catch (IllegalAccessException e) {
            throw new JsonSerializeException(e.getMessage());
        }
    }

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

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

請注意,爲簡潔起見,已將多個功能合併到該類中。有關此序列化程序類的重構版本,請參閱codebase存儲庫中的此分支(https://github.com/albanoj2/dzone-json-serializer/tree/srp_generalization)。咱們還建立了一個異常,用於表示在serialize方法處理對象時是否發生了錯誤:

public class JsonSerializeException extends Exception {
    private static final long serialVersionUID = -8845242379503538623L;

    public JsonSerializeException(String message) {
        super(message);
    }
} 複製代碼

儘管JsonSerializer該類看起來很複雜,但它包含三個主要任務:(1)查找使用@JsonField註解的全部字段,(2)記錄包含@JsonField註解的全部字段的名稱(或顯式提供的字段名稱)和值,以及(3)將所記錄的字段名稱和值的鍵值對轉換成JSON字符串。

requireNonNull(object).getClass()檢查提供的對象不是null (若是是,則拋出一個NullPointerException)並得到與提供的對象關聯的 Class對象。並使用此對象關聯的類來獲取關聯的字段。接下來,咱們建立String到String的Map,存儲字段名和值的鍵值對。
隨着數據結構的創建,接下來遍歷類中聲明的每一個字段。對於每一個字段,咱們配置爲在訪問字段時禁止Java語言訪問檢查。這是很是重要的一步,由於咱們註解的字段是私有的。在標準狀況下,咱們將沒法訪問這些字段,而且嘗試獲取私有字段的值將致使IllegalAccessException拋出。爲了訪問這些私有字段,咱們必須禁止對該字段的標準Java訪問檢查。setAccessible(boolean) 定義以下:
返回值true 表示反射對象應禁止Java語言訪問檢查。false 表示反射對象應強制執行Java語言訪問檢查。
請注意,隨着Java 9中模塊的引入,使用setAccessible 方法要求將包含訪問其私有字段的類的包在其模塊定義中聲明爲open。有關更多信息,請參閱 this explanation by Michał SzewczykAccessing Private State of Java 9 Modules by Gunnar Morling
在得到對該字段的訪問權限以後,咱們檢查該字段是否使用了註解@JsonField。若是是,咱們肯定字段的名稱(經過@JsonField註解中提供的顯式名稱或默認名稱),並在咱們先前構造的map中記錄名稱和字段值。處理完全部字段後,咱們將字段名稱映射轉換爲JSON字符串。
處理完全部記錄後,咱們將全部這些字符串與逗號組合在一塊兒。這會產生一個字符串"<fieldName1>":"<fieldValue1>","<fieldName2>":"<fieldValue2>",...。一旦這個字符串被鏈接起來,咱們用花括號括起來,建立一個有效的JSON字符串。
爲了測試這個序列化器,咱們能夠執行如下代碼:

Car car=new Car("Ford","F150","2018");
JsonSerializer serializer=new JsonSerializer();
serializer.serialize(car); 複製代碼

輸出:

{"model":"F150","manufacturer":"Ford"}複製代碼
正如預期的那樣,Car對象的maker和model字段已經被序列化,使用字段的名稱做爲鍵,字段的值做爲值。請注意,JSON元素的順序可能與上面看到的輸出相反。發生這種狀況是由於對於類的聲明字段數組沒有明確的排序,如 getDeclaredFields文檔中所述:
返回數組中的元素未排序,而且不按任何特定順序排列。
因爲此限制,JSON字符串中元素的順序可能會有所不一樣。爲了使元素的順序具備肯定性,咱們必須本身強加排序。因爲JSON對象被定義爲一組無序的鍵值對,所以根據 JSON標準,不須要強制排序。但請注意,序列化方法的測試用例應該輸出{"model":"F150","manufacturer":"Ford"} 或者{"manufacturer":"Ford","model":"F150"}。

結論

Java註解是Java語言中很是強大的功能,但大多數狀況下,咱們使用標準註解(例如@Override)或通用框架註解(例如@Autowired),而不是開發人員。雖然不該使用註解來代替以面向對象的方式,但它們能夠極大地簡化重複邏輯。例如,咱們能夠註解每一個可序列化字段而不是在接口中的方法建立一個toJsonString以及全部能夠序列化的類實現此接口。它還將序列化邏輯與域邏輯分離,從域邏輯的簡潔性中消除了手動序列化的混亂。
雖然在大多數Java應用程序中不常用自定義註解,可是對於Java語言的任何中級或高級用戶來講,須要瞭解此功能。這個特性的知識不只加強了開發人員的知識儲備,一樣也有助於理解最流行的Java框架中的常見註解。
更多文章歡迎訪問: http://www.apexyun.com
公衆號:銀河系1號
聯繫郵箱:public@space-explore.com
(未經贊成,請勿轉載)   
相關文章
相關標籤/搜索