溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java創(chuàng)建Annotation的方法

發(fā)布時間:2021-02-05 10:55:37 來源:億速云 閱讀:224 作者:小新 欄目:編程語言

這篇文章將為大家詳細講解有關(guān)Java創(chuàng)建Annotation的方法,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

前言

注解是Java很強大的部分,但大多數(shù)時候我們傾向于使用而不是去創(chuàng)建注解。例如,在Java源代碼里不難找到Java編譯器處理的@Override注解,Spring框架的@Autowired注解, 或Hibernate框架使用的@Entity 注解,但我們很少看到自定義注解。雖然自定義注解是Java語言中經(jīng)常被忽視的一個方面,但在開發(fā)可讀性代碼時它可能是非常有用的資產(chǎn),同樣有助于理解常見框架(如Spring或Hibernate)如何簡潔地實現(xiàn)其目標(biāo)。

在本文中,我們將介紹注解的基礎(chǔ)知識,包括注解是什么,它們?nèi)绾卧谑纠惺褂?,以及如何處理它們。為了演示注解在實踐中的工作原理,我們將創(chuàng)建一個Javascript Object Notation(JSON)序列化程序,用于處理帶注解的對象并生成表示每個對象的JSON字符串。在此過程中,我們將介紹許多常見的注解塊,包括Java反射框架和注解可見性問題。感興趣的讀者可以在GitHub上找到已完成的JSON序列化程序的源代碼。

什么是注解?

注解是應(yīng)用于Java結(jié)構(gòu)的裝飾器,例如將元數(shù)據(jù)與類,方法或字段相關(guān)聯(lián)。這些裝飾器是良性的,不會自行執(zhí)行任何代碼,但運行時,框架或編譯器可以使用它們來執(zhí)行某些操作。更正式地說,Java語言規(guī)范(JLS)第9.7節(jié)提供了以下定義:

注解是信息與程序結(jié)構(gòu)相關(guān)聯(lián)的標(biāo)記,但在運行時沒有任何影響。

請務(wù)必注意此定義中的最后一句:注解在運行時對程序沒有影響。這并不是說框架不會基于注解的存在而改變其運行時行為,而是包含注解本身的程序不會改變其運行時行為。雖然這可能看起來是細微差別,但為了掌握注解的實用性,理解這一點非常重要。

例如,某個實例的字段添加了@Autowired注解,其本身不會改變程序的運行時行為:編譯器只是在運行時包含注解,但注解不執(zhí)行任何代碼或注入任何邏輯來改變程序的正常行為(忽略注解時的預(yù)期行為)。一旦我們在運行時引入Spring框架,我們就可以在解析程序時獲得強大的依賴注入(DI)功能。通過引入注解,我們已經(jīng)指示Spring框架向我們的字段注入適當(dāng)?shù)囊蕾図?。我們將很快看到(?dāng)我們創(chuàng)建JSON序列化程序時)注解本身并沒有完成此操作,而是充當(dāng)標(biāo)記,通知Spring框架我們希望將依賴項注入到帶注解的字段中。

Retention和Target

創(chuàng)建注解需要兩條信息:(1)retention策略和(2)target。保留策略(retention)指定了在程序的生命周期注解應(yīng)該被保留多長時間。例如,注解可以在編譯時或運行時期間保留,具體取決于與注解關(guān)聯(lián)的保留策略。從Java 9開始,有三種標(biāo)準(zhǔn)保留策略,總結(jié)如下:

Java創(chuàng)建Annotation的方法

正如我們稍后將看到的,注解保留的運行時選項是最常見的選項之一,因為它允許Java程序反射訪問注解并基于存在的注解執(zhí)行代碼,以及訪問與注解相關(guān)聯(lián)的數(shù)據(jù)。請注意,注解只有一個關(guān)聯(lián)的保留策略。

注解的目標(biāo)(target)指定注解可以應(yīng)用于哪個Java結(jié)構(gòu)。例如,某些注解可能僅對方法有效,而其他注解可能對類和字段都有效。從Java 9開始,有11個標(biāo)準(zhǔn)注解目標(biāo),如下表所示:

Java創(chuàng)建Annotation的方法

有關(guān)這些目標(biāo)的更多信息,請參見JLS的第9.7.4節(jié)。要注意,注解可以關(guān)聯(lián)一個或多個目標(biāo)。例如,如果字段和構(gòu)造函數(shù)目標(biāo)與注解相關(guān)聯(lián),則可以在字段或構(gòu)造函數(shù)上使用注解。另一方面,如果注解僅關(guān)聯(lián)方法目標(biāo),則將注解應(yīng)用于除方法之外的任何構(gòu)造都會在編譯期間導(dǎo)致錯誤。

注解參數(shù)

注解也可以具有參數(shù)。這些參數(shù)可以是基本類型(例如int或double),String,類,枚舉,注解或前五種類型中任何一種的數(shù)組(參見JLS的第9.6.1節(jié))。將參數(shù)與注解相關(guān)聯(lián)允許注解提供上下文信息或者可以參數(shù)化注解的處理器。例如,在我們的JSON序列化程序?qū)崿F(xiàn)中,我們將允許一個可選的注解參數(shù),該參數(shù)在序列化時指定字段的名稱(如果沒有指定名稱,則默認使用字段的變量名稱)。

如何創(chuàng)建注解?

對于我們的JSON序列化程序,我們將創(chuàng)建一個字段注解,允許開發(fā)人員在序列化對象時標(biāo)記要轉(zhuǎn)換的字段名。例如,如果我們創(chuàng)建汽車類,我們可以使用我們的注解來注解汽車的字段(例如品牌和型號)。當(dāng)我們序列化汽車對象時,生成的JSON將包括make和model鍵,其中值分別代表make和model字段的值。為簡單起見,我們假設(shè)此注解僅用于String類型的字段,確保字段的值可以直接序列化為字符串。

要創(chuàng)建這樣的字段注解,我們使用@interface 關(guān)鍵字聲明一個新的注解:

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

我們聲明的核心是public @interface JsonField,聲明帶有public修飾符的注解——允許我們的注解在任何包中使用(假設(shè)在另一個模塊中正確導(dǎo)入包)。注解聲明一個String類型value的參數(shù),默認值為空字符串。

請注意,變量名稱value具有特殊含義:它定義單元素注解(JLS的第9.7.3節(jié)),并允許我們的注解用戶向注解提供單個參數(shù),而無需指定參數(shù)的名稱。例如,用戶可以使用@JsonField("someFieldName")并且不需要將注解聲明為注解@JsonField(value = "someFieldName"),盡管后者仍然可以使用(但不是必需的)。包含默認值空字符串允許省略該值,value如果沒有顯式指定值,則導(dǎo)致值為空字符串。例如,如果用戶使用表單聲明上述注解@JsonField,則該value參數(shù)設(shè)置為空字符串。

注解聲明的保留策略和目標(biāo)分別使用@Retention和@Target注解指定。保留策略使用java.lang.annotation.RetentionPolicy枚舉指定,并包含三個標(biāo)準(zhǔn)保留策略的常量。同樣,指定目標(biāo)為java.lang.annotation.ElementType枚舉,包括11種標(biāo)準(zhǔn)目標(biāo)類型中每種類型的常量。

總之,我們創(chuàng)建了一個名為JsonField的public單元素注解,它在運行時由JVM保留,并且只能應(yīng)用于字段。此注解只有單個參數(shù),類型String的value,默認值為空字符串。通過創(chuàng)建注解,我們現(xiàn)在可以注解要序列化的字段。

如何使用注解?

使用注解僅需要將注解放在適當(dāng)?shù)慕Y(jié)構(gòu)(注解的任何有效目標(biāo))之前。例如,我們可以創(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")注解一個字段,但這種樣式過于冗長,并沒有助于代碼的可讀性。因此,除非在單元素注解中包含注解參數(shù)名稱可以增加代碼的可讀性,否則應(yīng)該省略它。對于具有多個參數(shù)的注解,需要顯式指定每個參數(shù)的名稱來區(qū)分參數(shù)(除非僅提供一個參數(shù),在這種情況下,如果未顯式提供名稱,則參數(shù)將映射到value參數(shù))。

鑒于@JsonField注解的上述用法,我們希望將Car序列化為JSON字符串{"manufacturer":"someMake", "model":"someModel"} (注意,我們稍后將會看到,我們將忽略鍵manufacturer 和model在此JSON字符串的順序)。在這之前,重要的是要注意添加@JsonField注解不會改變類Car的運行時行為。如果編譯這個類,包含@JsonField注解不會比省略注解時增強類的行為。類的類文件中只是簡單地記錄這些注解以及參數(shù)的值。改變系統(tǒng)的運行時行為需要我們處理這些注解。

如何處理注解?

處理注解是通過Java反射應(yīng)用程序編程接口(API)完成的。反射API允許我們編寫代碼來訪問對象的類、方法、字段等。例如,如果我們創(chuàng)建一個接受Car對象的方法,我們可以檢查該對象的類(即Car),并發(fā)現(xiàn)該類有三個字段:(1)make,(2)model和(3)year。此外,我們可以檢查這些字段以發(fā)現(xiàn)每個字段是否都使用特定注解進行注解。

這樣,我們可以遍歷傳遞給方法的參數(shù)對象關(guān)聯(lián)類的每個字段,并發(fā)現(xiàn)哪些字段使用@JsonField注解。如果該字段使用了@JsonField注解,我們將記錄該字段的名稱及其值。處理完所有字段后,我們就可以使用這些字段名稱和值創(chuàng)建JSON字符串。

確定字段的名稱需要比確定值更復(fù)雜的邏輯。如果@JsonField包含value參數(shù)的提供值(例如"manufacturer"之前使用的@JsonField("manufacturer")),我們將使用提供的字段名稱。如果value參數(shù)的值是空字符串,我們知道沒有顯式提供字段名稱(因為這是value參數(shù)的默認值),否則,顯式提供了一個空字符串。后面這幾種情況下,我們都將使用字段的變量名作為字段名稱(例如,在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; } }}

請注意,為簡潔起見,已將多個功能合并到該類中。有關(guān)此序列化程序類的重構(gòu)版本,請參閱codebase存儲庫中的此分支。我們還創(chuàng)建了一個異常,用于表示在serialize方法處理對象時是否發(fā)生了錯誤:

public class JsonSerializeException extends Exception {
	private static final long serialVersionUID = -8845242379503538623L;
	public JsonSerializeException(String message) {
		super(message);
	}
}

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

requireNonNull(object).getClass()檢查提供的對象不是null (如果是,則拋出一個NullPointerException)并獲得與提供的對象關(guān)聯(lián)的Class對象。并使用此對象關(guān)聯(lián)的類來獲取關(guān)聯(lián)的字段。接下來,我們創(chuàng)建String到String的Map,存儲字段名和值的鍵值對。

隨著數(shù)據(jù)結(jié)構(gòu)的建立,接下來遍歷類中聲明的每個字段。對于每個字段,我們配置為在訪問字段時禁止Java語言訪問檢查。這是非常重要的一步,因為我們注解的字段是私有的。在標(biāo)準(zhǔn)情況下,我們將無法訪問這些字段,并且嘗試獲取私有字段的值將導(dǎo)致IllegalAccessException拋出。為了訪問這些私有字段,我們必須禁止對該字段的標(biāo)準(zhǔn)Java訪問檢查。setAccessible(boolean) 定義如下:

返回值true 表示反射對象應(yīng)禁止Java語言訪問檢查。false 表示反射對象應(yīng)強制執(zhí)行Java語言訪問檢查。

請注意,隨著Java 9中模塊的引入,使用setAccessible 方法要求將包含訪問其私有字段的類的包在其模塊定義中聲明為open。有關(guān)更多信息,請參閱 this explanation by Micha? Szewczyk和Accessing Private State of Java 9 Modules by Gunnar Morling。

在獲得對該字段的訪問權(quán)限之后,我們檢查該字段是否使用了注解@JsonField。如果是,我們確定字段的名稱(通過@JsonField注解中提供的顯式名稱或默認名稱),并在我們先前構(gòu)造的map中記錄名稱和字段值。處理完所有字段后,我們將字段名稱映射轉(zhuǎn)換為JSON字符串。

處理完所有記錄后,我們將所有這些字符串與逗號組合在一起。這會產(chǎn)生一個字符串"<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"}

正如預(yù)期的那樣,Car對象的maker和model字段已經(jīng)被序列化,使用字段的名稱作為鍵,字段的值作為值。請注意,JSON元素的順序可能與上面看到的輸出相反。發(fā)生這種情況是因為對于類的聲明字段數(shù)組沒有明確的排序,如getDeclaredFields文檔中所述:

返回數(shù)組中的元素未排序,并且不按任何特定順序排列。

由于此限制,JSON字符串中元素的順序可能會有所不同。為了使元素的順序具有確定性,我們必須自己強加排序。由于JSON對象被定義為一組無序的鍵值對,因此根據(jù)JSON標(biāo)準(zhǔn),不需要強制排序。但請注意,序列化方法的測試用例應(yīng)該輸出{"model":"F150","manufacturer":"Ford"} 或者{"manufacturer":"Ford","model":"F150"}。

結(jié)論

Java注解是Java語言中非常強大的功能,但大多數(shù)情況下,我們使用標(biāo)準(zhǔn)注解(例如@Override)或通用框架注解(例如@Autowired),而不是開發(fā)人員。雖然不應(yīng)使用注解來代替以面向?qū)ο蟮姆绞剑鼈兛梢詷O大地簡化重復(fù)邏輯。例如,我們可以注解每個可序列化字段而不是在接口中的方法創(chuàng)建一個toJsonString以及所有可以序列化的類實現(xiàn)此接口。它還將序列化邏輯與域邏輯分離,從域邏輯的簡潔性中消除了手動序列化的混亂。

雖然在大多數(shù)Java應(yīng)用程序中不經(jīng)常使用自定義注解,但是對于Java語言的任何中級或高級用戶來說,需要了解此功能。這個特性的知識不僅增強了開發(fā)人員的知識儲備,同樣也有助于理解最流行的Java框架中的常見注解。

關(guān)于“Java創(chuàng)建Annotation的方法”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學(xué)到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI