java进阶之注解篇
文章目錄
- 注解
- 基本語(yǔ)法
- 定義注解
- 元注解
- 編寫注解處理器
- 注解元素
- 默認(rèn)值限制
- 生成外部文件
- 替代方案
- 注解不支持繼承
- 實(shí)現(xiàn)處理器
注解
注解(也被稱為元數(shù)據(jù))為我們?cè)诖a中添加信息提供了一種形式化的方式,使我們可以在稍后的某個(gè)時(shí)刻更容易的使用這些數(shù)據(jù)。
注解在一定程度上是把元數(shù)據(jù)和源代碼文件結(jié)合在一起的趨勢(shì)所激發(fā)的,而不是保存在外部文檔。這同樣是對(duì)像C# 語(yǔ)言對(duì)于Java 語(yǔ)言特性壓力的一種回應(yīng)。
注解是Java 5 所引入的眾多語(yǔ)言變化之一。它們提供了Java 無法表達(dá)的但是你需要完整表述程序所需的信息。因此,注解使得我們可以以編譯器驗(yàn)證的格式存儲(chǔ)程序的額外信息。注解可以生成描述符文件,甚至是新的類定義,并且有助于減輕編寫“樣板”代碼的負(fù)擔(dān)。通過使用注解,你可以將元數(shù)據(jù)保存在Java 源代碼中。并擁有如下優(yōu)勢(shì):
簡(jiǎn)單易讀的代碼,編譯器類型檢查,使用annotation API 為自己的注解構(gòu)造處理工具。
即使Java 定義了一些類型的元數(shù)據(jù),但是一般來說注解類型的添加和如何使用完全取決于你。
注解的語(yǔ)法十分簡(jiǎn)單,主要是在現(xiàn)有語(yǔ)法中添加@ 符號(hào)。Java 5 引入了前三種定義在java.lang 包中的注解:
? @Override:表示當(dāng)前的方法定義將覆蓋基類的方法。如果你不小心拼寫錯(cuò)誤,或者方法簽名被錯(cuò)誤拼寫的時(shí)候,編譯器就會(huì)發(fā)出錯(cuò)誤提示。
? @Deprecated:如果使用該注解的元素被調(diào)用,編譯器就會(huì)發(fā)出警告信息。
? @SuppressWarnings:關(guān)閉不當(dāng)?shù)木幾g器警告信息。
? @SafeVarargs:在Java 7 中加入用于禁止對(duì)具有泛型varargs 參數(shù)的方法或構(gòu)造函數(shù)的調(diào)用方發(fā)出警告。
? @FunctionalInterface:Java 8 中加入用于表示類型聲明為函數(shù)式接口。
還有5 種額外的注解類型用于創(chuàng)造新的注解。你將會(huì)在這一章學(xué)習(xí)它們。
每當(dāng)創(chuàng)建涉及重復(fù)工作的類或接口時(shí),你通??梢允褂米⒔鈦碜詣?dòng)化和簡(jiǎn)化流程。
例如在Enterprise JavaBean(EJB)中的許多額外工作就是通過注解來消除的。
注解的出現(xiàn)可以替代一些現(xiàn)有的系統(tǒng),例如XDoclet,它是一種獨(dú)立的文檔化工具,專門設(shè)計(jì)用來生成注解風(fēng)格的文檔。與之相比,注解是真正語(yǔ)言層級(jí)的概念,以前構(gòu)造出來就享有編譯器的類型檢查保護(hù)。注解在源代碼級(jí)別保存所有信息而不是通過注釋文字,這使得代碼更加整潔和便于維護(hù)。通過使用拓展的annotation API 或稍后在本章節(jié)可以看到的外部的字節(jié)碼工具類庫(kù),你會(huì)擁有對(duì)源代碼及字節(jié)碼強(qiáng)大的檢查與操作能力。
基本語(yǔ)法
在下面的例子中,使用@Test 對(duì)testExecute() 進(jìn)行注解。該注解本身不做任何事情,但是編譯器要保證其類路徑上有@Test 注解的定義。你將在本章看到,我們通過注解創(chuàng)建了一個(gè)工具用于運(yùn)行這個(gè)方法:
// annotations/Testable.java package annotations; import onjava.atunit.*; public class Testable {public void execute() {System.out.println("Executing..");}@Testvoid testExecute() { execute(); } }被注解標(biāo)注的方法和其他方法沒有任何區(qū)別。在這個(gè)例子中,注解@Test 可以和任何修飾符共同用于方法,諸如public、static 或void。從語(yǔ)法的角度上看,注解和修飾符的使用方式是一致的。
定義注解
如下是一個(gè)注解的定義。注解的定義看起來很像接口的定義。事實(shí)上,它們和其他Java 接口一樣,也會(huì)被編譯成class 文件。
// onjava/atunit/Test.java // The @Test tag package onjava.atunit; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test {}除了@ 符號(hào)之外,@Test 的定義看起來更像一個(gè)空接口。注解的定義也需要一些元注解(meta-annotation),比如@Target 和@Retention。@Target 定義你的注解可以應(yīng)用在哪里(例如是方法還是字段)。@Retention 定義了注解在哪里可用,在源代碼中
(SOURCE),class 文件(CLASS)中或者是在運(yùn)行時(shí)(RUNTIME)。
注解通常會(huì)包含一些表示特定值的元素。當(dāng)分析處理注解的時(shí)候,程序或工具可以利用這些值。注解的元素看起來就像接口的方法,但是可以為其指定默認(rèn)值。
不包含任何元素的注解稱為標(biāo)記注解(marker annotation),例如上例中的@Test就是標(biāo)記注解。
下面是一個(gè)簡(jiǎn)單的注解,我們可以用它來追蹤項(xiàng)目中的用例。程序員可以使用該注解來標(biāo)注滿足特定用例的一個(gè)方法或者一組方法。于是,項(xiàng)目經(jīng)理可以通過統(tǒng)計(jì)已經(jīng)實(shí)現(xiàn)的用例來掌控項(xiàng)目的進(jìn)展,而開發(fā)者在維護(hù)項(xiàng)目時(shí)可以輕松的找到用例用于更新,或者他們可以調(diào)試系統(tǒng)中業(yè)務(wù)邏輯。
注意id 和description 與方法定義類似。由于編譯器會(huì)對(duì)id 進(jìn)行類型檢查,因此將跟蹤數(shù)據(jù)庫(kù)與用例文檔和源代碼相關(guān)聯(lián)是可靠的方式。description 元素?fù)碛幸粋€(gè)default 值,如果在注解某個(gè)方法時(shí)沒有給出description 的值。則該注解的處理器會(huì)使用此元素的默認(rèn)值。
在下面的類中,有三個(gè)方法被注解為用例:
注解的元素在使用時(shí)表現(xiàn)為名-值對(duì)的形式,并且需要放置在@UseCase 聲明之后的括號(hào)內(nèi)。在encryptPassword() 方法的注解中,并沒有給出description 的值,所以在@interface UseCase 的注解處理器分析處理這個(gè)類的時(shí)候會(huì)使用該元素的默認(rèn)值。你應(yīng)該能夠想象到如何使用這套工具來“勾勒” 出將要建造的系統(tǒng),然后在建造的過程中逐漸實(shí)現(xiàn)系統(tǒng)的各項(xiàng)功能。
元注解
Java 語(yǔ)言中目前有5 種標(biāo)準(zhǔn)注解(前面介紹過),以及5 種元注解。元注解用于注解其他的注解
| @Target | 表示注解可以用于哪些地方。可能的ElementType 參數(shù)包括: CONSTRUCTOR:構(gòu)造器的聲明 FIELD:字段聲明(包括enum實(shí)例) LOCAL_VARIABLE:局部變量聲明 METHOD:方法聲明 PACKAGE:包聲明 PARAMETER:參數(shù)聲明 TYPE:類、接口(包括注解類型)或者enum 聲明 |
| @Retention | 表示注解信息保存的時(shí)長(zhǎng)。可選的RetentionPolicy 參數(shù)包括: SOURCE:注解將被編譯器丟棄 CLASS:注解在class 文件中可用,但是會(huì)被VM 丟棄。 RUNTIME:VM 將在運(yùn)行期也保留注解,因此可以通過反射機(jī)制讀取注解的信息。 |
| @Documented | 將此注解保存在Javadoc 中 |
| @Inherited | 允許子類繼承父類的注解 |
| @Repeatable | 允許一個(gè)注解可以被使用一次或者多次(Java 8)。 |
大多數(shù)時(shí)候,程序員定義自己的注解,并編寫自己的處理器來處理他們。
編寫注解處理器
如果沒有用于讀取注解的工具,那么注解不會(huì)比注釋更有用。使用注解中一個(gè)很重要的部分就是,創(chuàng)建與使用注解處理器。Java 拓展了反射機(jī)制的API 用于幫助你創(chuàng)造這類工具。同時(shí)他還提供了javac 編譯器鉤子在編譯時(shí)使用注解。
下面是一個(gè)非常簡(jiǎn)單的注解處理器,我們用它來讀取被注解的PasswordUtils 類,并且使用反射機(jī)制來尋找@UseCase 標(biāo)記。給定一組id 值,然后列出在PasswordUtils中找到的用例,以及缺失的用例。
輸出為:
Found Use Case 48
no description
Found Use Case 47
Passwords must contain at least one numeric
Found Use Case 49
New passwords can’t equal previously used ones
Missing use case 50
這個(gè)程序用了兩個(gè)反射的方法:getDeclaredMethods() 和getAnnotation(),它們都屬于AnnotatedElement 接口(Class,Method 與Field 類都實(shí)現(xiàn)了該接口)。
getAnnotation() 方法返回指定類型的注解對(duì)象,在本例中就是“UseCase”。如果被注解的方法上沒有該類型的注解,返回值就為null。我們通過調(diào)用id() 和description()方法來提取元素值。注意encryptPassword() 方法在注解的時(shí)候沒有指定description的值,因此處理器在處理它對(duì)應(yīng)的注解時(shí),通過description() 取得的是默認(rèn)值“no description”。
注解元素
在UseCase.java 中定義的@UseCase 的標(biāo)簽包含int 元素id 和String 元素description。注解元素可用的類型如下所示:
? 所有基本類型(int、float、boolean 等)
? String
? Class
? enum
? Annotation
? 以上類型的數(shù)組
如果你使用了其他類型,編譯器就會(huì)報(bào)錯(cuò)。注意,也不允許使用任何包裝類型,但是由于自動(dòng)裝箱的存在,這不算是什么限制。注解也可以作為元素的類型。稍后你會(huì)看到,注解嵌套是一個(gè)非常有用的技巧。
默認(rèn)值限制
編譯器對(duì)于元素的默認(rèn)值有些過于挑剔。首先,元素不能有不確定的值。也就是說,元素要么有默認(rèn)值,要么就在使用注解時(shí)提供元素的值。
這里有另外一個(gè)限制:任何非基本類型的元素,無論是在源代碼聲明時(shí)還是在注解接口中定義默認(rèn)值時(shí),都不能使用null 作為其值。這個(gè)限制使得處理器很難表現(xiàn)一個(gè)元素的存在或者缺失的狀態(tài),因?yàn)樵诿總€(gè)注解的聲明中,所有的元素都存在,并且具有相應(yīng)的值。為了繞開這個(gè)約束,可以自定義一些特殊的值,比如空字符串或者負(fù)數(shù)用于表達(dá)某個(gè)元素不存在。
這是一個(gè)在定義注解的習(xí)慣用法。
生成外部文件
當(dāng)有些框架需要一些額外的信息才能與你的源代碼協(xié)同工作,這種情況下注解就會(huì)變得十分有用。像Enterprise JavaBeans (EJB3 之前)這樣的技術(shù),每一個(gè)Bean 都需要大量的接口和部署描述文件,而這些就是“樣板” 文件。Web Service,自定義標(biāo)簽庫(kù)以及對(duì)象/關(guān)系映射工具(例如Toplink 和Hibernate)通常都需要XML 描述文件,而這些文件脫離于代碼之外。除了定義Java 類,程序員還必須忍受沉悶,重復(fù)的提供某些信息,例如類名和包名等已經(jīng)在原始類中提供過的信息。每當(dāng)你使用外部描述文件時(shí),他就擁有了一個(gè)類的兩個(gè)獨(dú)立信息源,這經(jīng)常導(dǎo)致代碼的同步問題。同時(shí)這也要求了為項(xiàng)目工作的程序員在知道如何編寫Java 程序的同時(shí),也必須知道如何編輯描述文件。假設(shè)你想提供一些基本的對(duì)象/關(guān)系映射功能,能夠自動(dòng)生成數(shù)據(jù)庫(kù)表。你可以使用XML 描述文件來指明類的名字、每個(gè)成員以及數(shù)據(jù)庫(kù)映射的相關(guān)信息。但是,通過使用注解,你可以把所有信息都保存在JavaBean 源文件中。為此你需要一些用于定義數(shù)據(jù)庫(kù)表名稱、數(shù)據(jù)庫(kù)列以及將SQL 類型映射到屬性的注解。
以下是一個(gè)注解的定義,它告訴注解處理器應(yīng)該創(chuàng)建一個(gè)數(shù)據(jù)庫(kù)表:
在@Target 注解中指定的每一個(gè)ElementType 就是一個(gè)約束,它告訴編譯器,這個(gè)自定義的注解只能用于指定的類型。你可以指定enum ElementType 中的一個(gè)值,或者以逗號(hào)分割的形式指定多個(gè)值。如果想要將注解應(yīng)用于所有的ElementType,那么可以省去@Target 注解,但是這并不常見。
注意@DBTable 中有一個(gè)name() 元素,該注解通過這個(gè)元素為處理器創(chuàng)建數(shù)據(jù)庫(kù)時(shí)提供表的名字。
如下是修飾字段的注解:
@Constraints 注解允許處理器提供數(shù)據(jù)庫(kù)表的元數(shù)據(jù)。@Constraints 代表了數(shù)據(jù)庫(kù)通常提供的約束的一小部分,但是它所要表達(dá)的思想已經(jīng)很清楚了。primaryKey(),allowNull() 和unique() 元素明顯的提供了默認(rèn)值,從而使得在大多數(shù)情況下,該注解的使用者不需要輸入太多東西。
另外兩個(gè)@interface 定義的是SQL 類型。如果希望這個(gè)框架更有價(jià)值的話,我們應(yīng)該為每個(gè)SQL 類型都定義相應(yīng)的注解。不過作為示例,兩個(gè)元素足夠了。
這些SQL 類型具有name() 元素和constraints() 元素。后者利用了嵌套注解
的功能,將數(shù)據(jù)庫(kù)列的類型約束信息嵌入其中。注意constraints() 元素的默認(rèn)值是@Constraints。由于在@Constraints 注解類型之后,沒有在括號(hào)中指明@Constraints元素的值,因此,constraints() 的默認(rèn)值為所有元素都為默認(rèn)值的@Constraints注解。如果要使得嵌入的@Constraints 注解中的unique() 元素為true,并作為constraints() 元素的默認(rèn)值,你可以像如下定義:
下面是一個(gè)簡(jiǎn)單的,使用了如上注解的類:
// annotations/database/Member.java package annotations.database; @DBTable(name = "MEMBER") public class Member {@SQLString(30) String firstName;@SQLString(50) String lastName;@SQLInteger Integer age;@SQLString(value = 30,constraints = @Constraints(primaryKey = true))String reference;static int memberCount;public String getReference() { return reference; }public String getFirstName() { return firstName; }public String getLastName() { return lastName; }@Overridepublic String toString() { return reference; }public Integer getAge() { return age; } }類注解@DBTable 注解給定了元素值MEMBER,它將會(huì)作為表的名字。類的屬性firstName 和lastName 都被注解為@SQLString 類型并且給了默認(rèn)元素值分別為30 和50。這些注解都有兩個(gè)有趣的地方:首先,他們都使用了嵌入的@Constraints注解的默認(rèn)值;其次,它們都是用了快捷方式特性。如果你在注解中定義了名為value的元素,并且在使用該注解時(shí),value 為唯一一個(gè)需要賦值的元素,你就不需要使用名—值對(duì)的語(yǔ)法,你只需要在括號(hào)中給出value 元素的值即可。這可以應(yīng)用于任何合法類型的元素。這也限制了你必須將元素命名為value,不過在上面的例子中,這樣的注解語(yǔ)句也更易于理解:
@SQLString(30)
處理器將在創(chuàng)建表的時(shí)候使用該值設(shè)置SQL 列的大小。
默認(rèn)值的語(yǔ)法雖然很靈巧,但是它很快就變的復(fù)雜起來。以reference 字段的注解為例,上面擁有@SQLString 注解,但是這個(gè)字段也將成為表的主鍵,因此在嵌入的@Constraint 注解中設(shè)定primaryKey 元素的值。這時(shí)事情就變的復(fù)雜了。你不得不為這個(gè)嵌入的注解使用很長(zhǎng)的鍵—值對(duì)的形式,來指定元素名稱和@interface 的名稱。同時(shí),由于有特殊命名的value 也不是唯一需要賦值的元素,因此不能再使用快捷方式特性。如你所見,最終結(jié)果不算清晰易懂。
替代方案
可以使用多種不同的方式來定義自己的注解用于上述任務(wù)。例如,你可以使用一個(gè)單一的注解類@TableColumn,它擁有一個(gè)enum 元素,元素值定義了STRING,INTEGER,FLOAT 等類型。這消除了每個(gè)SQL 類型都需要定義一個(gè)@interface的負(fù)擔(dān),不過也使得用額外信息修飾SQL 類型變的不可能,這些額外的信息例如長(zhǎng)度或精度等,都可能是非常有用的。
你也可以使用一個(gè)String 類型的元素來描述實(shí)際的SQL 類型,比如“VARCHAR(30)” 或者“INTEGER”。這使得你可以修飾SQL 類型,但是這也將Java 類型到SQL 類型的映射綁在了一起,這不是一個(gè)好的設(shè)計(jì)。你并不想在數(shù)據(jù)庫(kù)更改之后重新編譯你的代碼;如果我們只需要告訴注解處理器,我們正在使用的是什么“口味(favor)” 的SQL,然后注解處理器來為我們處理SQL 類型的細(xì)節(jié),那將是一個(gè)優(yōu)雅的設(shè)計(jì)。
第三種可行的方案是一起使用兩個(gè)注解,@Constraints 和相應(yīng)的SQL 類型(例如,@SQLInteger)去注解同一個(gè)字段。這可能會(huì)讓代碼有些混亂,但是編譯器允許你對(duì)同一個(gè)目標(biāo)使用多個(gè)注解。在Java 8,在使用多個(gè)注解的時(shí)候,你可以重復(fù)使用同一個(gè)注解。
注解不支持繼承
你不能使用extends 關(guān)鍵字來繼承@interfaces。這真是一個(gè)遺憾,如果可以定義@TableColumn 注解(參考前面的建議),同時(shí)嵌套一個(gè)@SQLType 類型的注解,將成為一個(gè)優(yōu)雅的設(shè)計(jì)。按照這種方式,你可以通過繼承@SQLType 來創(chuàng)造各種SQL類型。例如@SQLInteger 和@SQLString。如果支持繼承,就會(huì)大大減少打字的工作量并且使得語(yǔ)法更整潔。在Java 的未來版本中,似乎沒有任何關(guān)于讓注解支持繼承
的提案,所以在當(dāng)前情況下,上例中的解決方案可能已經(jīng)是最佳方案了。
實(shí)現(xiàn)處理器
下面是一個(gè)注解處理器的例子,他將讀取一個(gè)類文件,檢查上面的數(shù)據(jù)庫(kù)注解,并生成用于創(chuàng)建數(shù)據(jù)庫(kù)的SQL 命令:
// annotations/database/TableCreator.java // Reflection-based annotation processor // {java annotations.database.TableCreator // annotations.database.Member} package annotations.database; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; public class TableCreator { public static void main(String[] args) throws Exception { if (args.length < 1) {System.out.println( "arguments: annotated classes"); System.exit(0); } for (String className : args) { Class<?> cl = Class.forName(className); DBTable dbTable = cl.getAnnotation(DBTable.class); if (dbTable == null) { System.out.println( "No DBTable annotations in class " + className); continue; } String tableName = dbTable.name(); // If the name is empty, use the Class name: if (tableName.length() < 1) tableName = cl.getName().toUpperCase(); List<String> columnDefs = new ArrayList<>(); for (Field field : cl.getDeclaredFields()) { String columnName = null; Annotation[] anns = field.getDeclaredAnnotations(); if (anns.length < 1) continue; // Not a db table column if (anns[0] instanceof SQLInteger) { SQLInteger sInt = (SQLInteger) anns[0]; // Use field name if name not specified if (sInt.name().length() < 1) columnName = field.getName().toUpperCase(); else columnName = sInt.name(); columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints())); } if (anns[0] instanceof SQLString) {SQLString sString = (SQLString) anns[0]; // Use field name if name not specified. if (sString.name().length() < 1) columnName = field.getName().toUpperCase(); else columnName = sString.name(); columnDefs.add(columnName + " VARCHAR(" + sString.value() + ")" + getConstraints(sString.constraints())); } StringBuilder createCommand = new StringBuilder( "CREATE TABLE " + tableName + "("); for (String columnDef : columnDefs) createCommand.append( "\n " + columnDef + ","); // Remove trailing comma String tableCreate = createCommand.substring( 0, createCommand.length() - 1) + ");"; System.out.println("Table Creation SQL for " + className + " is:\n" + tableCreate); } } } private static String getConstraints(Constraints con) { String constraints = ""; if (!con.allowNull()) constraints += " NOT NULL"; if (con.primaryKey()) constraints += " PRIMARY KEY"; if (con.unique()) constraints += " UNIQUE"; return constraints; } }輸出為:
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT);
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
FIRSTNAME VARCHAR(30),
LASTNAME VARCHAR(50),
AGE INT,
REFERENCE VARCHAR(30) PRIMARY KEY);
主方法會(huì)循環(huán)處理命令行傳入的每一個(gè)類名。每一個(gè)類都是用forName()
方法進(jìn)行加載, 并使用getAnnotation(DBTable.class) 來檢查該類是否帶有
@DBTable 注解。如果存在,將表名存儲(chǔ)起來。然后讀取這個(gè)類的所有字段,并使用getDeclaredAnnotations() 進(jìn)行檢查。這個(gè)方法返回一個(gè)包含特定字段上所有注解的數(shù)組。然后使用instanceof 操作符判斷這些注解是否是@SQLInteger 或者@SQLString 類型。如果是的話,在對(duì)應(yīng)的處理塊中將構(gòu)造出相應(yīng)的數(shù)據(jù)庫(kù)列的字符串片段。注意,由于注解沒有繼承機(jī)制,如果要獲取近似多態(tài)的行為,使用getDeclaredAnnotations() 似乎是唯一的方式。
嵌套的@Constraint 注解被傳遞給getConstraints() 方法,并用它來構(gòu)造一個(gè)包含SQL 約束的String 對(duì)象。
需要提醒的是,上面演示的技巧對(duì)于真實(shí)的對(duì)象/映射關(guān)系而言,是十分幼稚的。使用@DBTable 的注解來獲取表的名稱,這使得如果要修改表的名字,則迫使你重新編譯Java 代碼。這種效果并不理想?,F(xiàn)在已經(jīng)有了很多可用的框架,用于將對(duì)象映射到數(shù)據(jù)庫(kù)中,并且越來越多的框架開始使用注解了。
總結(jié)
以上是生活随笔為你收集整理的java进阶之注解篇的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 口蘑的功效与作用、禁忌和食用方法
- 下一篇: 豇豆米的功效与作用、禁忌和食用方法