詳解Java如何創(chuàng)建Annotation
前言
注解是Java很強大的部分,但大多數時候我們傾向于使用而不是去創(chuàng)建注解。例如,在Java源代碼里不難找到Java編譯器處理的@Override注解,Spring框架的@Autowired注解, 或Hibernate框架使用的@Entity 注解,但我們很少看到自定義注解。雖然自定義注解是Java語言中經常被忽視的一個方面,但在開發(fā)可讀性代碼時它可能是非常有用的資產,同樣有助于理解常見框架(如Spring或Hibernate)如何簡潔地實現其目標。
在本文中,我們將介紹注解的基礎知識,包括注解是什么,它們如何在示例中使用,以及如何處理它們。為了演示注解在實踐中的工作原理,我們將創(chuàng)建一個Javascript Object Notation(JSON)序列化程序,用于處理帶注解的對象并生成表示每個對象的JSON字符串。在此過程中,我們將介紹許多常見的注解塊,包括Java反射框架和注解可見性問題。感興趣的讀者可以在GitHub上找到已完成的JSON序列化程序的源代碼。
什么是注解?
注解是應用于Java結構的裝飾器,例如將元數據與類,方法或字段相關聯。這些裝飾器是良性的,不會自行執(zhí)行任何代碼,但運行時,框架或編譯器可以使用它們來執(zhí)行某些操作。更正式地說,Java語言規(guī)范(JLS)第9.7節(jié)提供了以下定義:
注解是信息與程序結構相關聯的標記,但在運行時沒有任何影響。
請務必注意此定義中的最后一句:注解在運行時對程序沒有影響。這并不是說框架不會基于注解的存在而改變其運行時行為,而是包含注解本身的程序不會改變其運行時行為。雖然這可能看起來是細微差別,但為了掌握注解的實用性,理解這一點非常重要。
例如,某個實例的字段添加了@Autowired注解,其本身不會改變程序的運行時行為:編譯器只是在運行時包含注解,但注解不執(zhí)行任何代碼或注入任何邏輯來改變程序的正常行為(忽略注解時的預期行為)。一旦我們在運行時引入Spring框架,我們就可以在解析程序時獲得強大的依賴注入(DI)功能。通過引入注解,我們已經指示Spring框架向我們的字段注入適當的依賴項。我們將很快看到(當我們創(chuàng)建JSON序列化程序時)注解本身并沒有完成此操作,而是充當標記,通知Spring框架我們希望將依賴項注入到帶注解的字段中。
Retention和Target
創(chuàng)建注解需要兩條信息:(1)retention策略和(2)target。保留策略(retention)指定了在程序的生命周期注解應該被保留多長時間。例如,注解可以在編譯時或運行時期間保留,具體取決于與注解關聯的保留策略。從Java 9開始,有三種標準保留策略,總結如下:

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

有關這些目標的更多信息,請參見JLS的第9.7.4節(jié)。要注意,注解可以關聯一個或多個目標。例如,如果字段和構造函數目標與注解相關聯,則可以在字段或構造函數上使用注解。另一方面,如果注解僅關聯方法目標,則將注解應用于除方法之外的任何構造都會在編譯期間導致錯誤。
注解參數
注解也可以具有參數。這些參數可以是基本類型(例如int或double),String,類,枚舉,注解或前五種類型中任何一種的數組(參見JLS的第9.6.1節(jié))。將參數與注解相關聯允許注解提供上下文信息或者可以參數化注解的處理器。例如,在我們的JSON序列化程序實現中,我們將允許一個可選的注解參數,該參數在序列化時指定字段的名稱(如果沒有指定名稱,則默認使用字段的變量名稱)。
如何創(chuàng)建注解?
對于我們的JSON序列化程序,我們將創(chuàng)建一個字段注解,允許開發(fā)人員在序列化對象時標記要轉換的字段名。例如,如果我們創(chuàng)建汽車類,我們可以使用我們的注解來注解汽車的字段(例如品牌和型號)。當我們序列化汽車對象時,生成的JSON將包括make和model鍵,其中值分別代表make和model字段的值。為簡單起見,我們假設此注解僅用于String類型的字段,確保字段的值可以直接序列化為字符串。
要創(chuàng)建這樣的字段注解,我們使用@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節(jié)),并允許我們的注解用戶向注解提供單個參數,而無需指定參數的名稱。例如,用戶可以使用@JsonField("someFieldName")并且不需要將注解聲明為注解@JsonField(value = "someFieldName"),盡管后者仍然可以使用(但不是必需的)。包含默認值空字符串允許省略該值,value如果沒有顯式指定值,則導致值為空字符串。例如,如果用戶使用表單聲明上述注解@JsonField,則該value參數設置為空字符串。
注解聲明的保留策略和目標分別使用@Retention和@Target注解指定。保留策略使用java.lang.annotation.RetentionPolicy枚舉指定,并包含三個標準保留策略的常量。同樣,指定目標為java.lang.annotation.ElementType枚舉,包括11種標準目標類型中每種類型的常量。
總之,我們創(chuàng)建了一個名為JsonField的public單元素注解,它在運行時由JVM保留,并且只能應用于字段。此注解只有單個參數,類型String的value,默認值為空字符串。通過創(chuàng)建注解,我們現在可以注解要序列化的字段。
如何使用注解?
使用注解僅需要將注解放在適當的結構(注解的任何有效目標)之前。例如,我們可以創(chuàng)建一個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")注解一個字段,但這種樣式過于冗長,并沒有助于代碼的可讀性。因此,除非在單元素注解中包含注解參數名稱可以增加代碼的可讀性,否則應該省略它。對于具有多個參數的注解,需要顯式指定每個參數的名稱來區(qū)分參數(除非僅提供一個參數,在這種情況下,如果未顯式提供名稱,則參數將映射到value參數)。
鑒于@JsonField注解的上述用法,我們希望將Car序列化為JSON字符串{"manufacturer":"someMake", "model":"someModel"} (注意,我們稍后將會看到,我們將忽略鍵manufacturer 和model在此JSON字符串的順序)。在這之前,重要的是要注意添加@JsonField注解不會改變類Car的運行時行為。如果編譯這個類,包含@JsonField注解不會比省略注解時增強類的行為。類的類文件中只是簡單地記錄這些注解以及參數的值。改變系統(tǒng)的運行時行為需要我們處理這些注解。
如何處理注解?
處理注解是通過Java反射應用程序編程接口(API)完成的。反射API允許我們編寫代碼來訪問對象的類、方法、字段等。例如,如果我們創(chuàng)建一個接受Car對象的方法,我們可以檢查該對象的類(即Car),并發(fā)現該類有三個字段:(1)make,(2)model和(3)year。此外,我們可以檢查這些字段以發(fā)現每個字段是否都使用特定注解進行注解。
這樣,我們可以遍歷傳遞給方法的參數對象關聯類的每個字段,并發(fā)現哪些字段使用@JsonField注解。如果該字段使用了@JsonField注解,我們將記錄該字段的名稱及其值。處理完所有字段后,我們就可以使用這些字段名稱和值創(chuàng)建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存儲庫中的此分支。我們還創(chuàng)建了一個異常,用于表示在serialize方法處理對象時是否發(fā)生了錯誤:
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對象。并使用此對象關聯的類來獲取關聯的字段。接下來,我們創(chuàng)建String到String的Map,存儲字段名和值的鍵值對。
隨著數據結構的建立,接下來遍歷類中聲明的每個字段。對于每個字段,我們配置為在訪問字段時禁止Java語言訪問檢查。這是非常重要的一步,因為我們注解的字段是私有的。在標準情況下,我們將無法訪問這些字段,并且嘗試獲取私有字段的值將導致IllegalAccessException拋出。為了訪問這些私有字段,我們必須禁止對該字段的標準Java訪問檢查。setAccessible(boolean) 定義如下:
返回值true 表示反射對象應禁止Java語言訪問檢查。false 表示反射對象應強制執(zhí)行Java語言訪問檢查。
請注意,隨著Java 9中模塊的引入,使用setAccessible 方法要求將包含訪問其私有字段的類的包在其模塊定義中聲明為open。有關更多信息,請參閱 this explanation by Michał Szewczyk和Accessing Private State of Java 9 Modules by Gunnar Morling。
在獲得對該字段的訪問權限之后,我們檢查該字段是否使用了注解@JsonField。如果是,我們確定字段的名稱(通過@JsonField注解中提供的顯式名稱或默認名稱),并在我們先前構造的map中記錄名稱和字段值。處理完所有字段后,我們將字段名稱映射轉換為JSON字符串。
處理完所有記錄后,我們將所有這些字符串與逗號組合在一起。這會產生一個字符串"<fieldName1>":"<fieldValue1>","<fieldName2>":"<fieldValue2>",...。一旦這個字符串被連接起來,我們用花括號括起來,創(chuàng)建一個有效的JSON字符串。
為了測試這個序列化器,我們可以執(zhí)行以下代碼:
Car car = new Car("Ford", "F150", "2018");
JsonSerializer serializer = new JsonSerializer();
serializer.serialize(car);
輸出:
{"model":"F150","manufacturer":"Ford"}
正如預期的那樣,Car對象的maker和model字段已經被序列化,使用字段的名稱作為鍵,字段的值作為值。請注意,JSON元素的順序可能與上面看到的輸出相反。發(fā)生這種情況是因為對于類的聲明字段數組沒有明確的排序,如getDeclaredFields文檔中所述:
返回數組中的元素未排序,并且不按任何特定順序排列。
由于此限制,JSON字符串中元素的順序可能會有所不同。為了使元素的順序具有確定性,我們必須自己強加排序。由于JSON對象被定義為一組無序的鍵值對,因此根據JSON標準,不需要強制排序。但請注意,序列化方法的測試用例應該輸出{"model":"F150","manufacturer":"Ford"} 或者{"manufacturer":"Ford","model":"F150"}。
結論
Java注解是Java語言中非常強大的功能,但大多數情況下,我們使用標準注解(例如@Override)或通用框架注解(例如@Autowired),而不是開發(fā)人員。雖然不應使用注解來代替以面向對象的方式,但它們可以極大地簡化重復邏輯。例如,我們可以注解每個可序列化字段而不是在接口中的方法創(chuàng)建一個toJsonString以及所有可以序列化的類實現此接口。它還將序列化邏輯與域邏輯分離,從域邏輯的簡潔性中消除了手動序列化的混亂。
雖然在大多數Java應用程序中不經常使用自定義注解,但是對于Java語言的任何中級或高級用戶來說,需要了解此功能。這個特性的知識不僅增強了開發(fā)人員的知識儲備,同樣也有助于理解最流行的Java框架中的常見注解。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
使用Java自帶的mail?API實現郵件發(fā)送功能全過程
電子郵件的應用非常廣泛,例如在某網站注冊了一個賬戶,自動發(fā)送一封歡迎郵件,通過郵件找回密碼,自動批量發(fā)送活動信息等,下面這篇文章主要給大家介紹了關于如何使用Java自帶的mail?API實現郵件發(fā)送功能的相關資料,需要的朋友可以參考下2023-04-04

