曾水新 黃日勝
摘要:Java的注解機(jī)制在JDK5就已推出,后續(xù)發(fā)布的新版不斷完善,目前注解機(jī)制的應(yīng)用已經(jīng)很廣泛,如用于輔助開(kāi)發(fā)的工具Lombok、AutoValue、Immutables等,主流的開(kāi)發(fā)框架如Spring、MyBatis也大量以注解替代了XML配置文件。大多數(shù)開(kāi)發(fā)者僅會(huì)使用注解,但不了解其工作原理,該文詳細(xì)介紹了JDK內(nèi)置的注解、元注解的作用和用法,分析了注解的工作原理,并以案例演示了如何編寫自定義注解,包括聲明注解、處理注解、使用注解三個(gè)流程,最后介紹了注解的應(yīng)用場(chǎng)景。
關(guān)鍵詞:Java;注解;反射技術(shù);框架技術(shù);編譯器
中圖分類號(hào):TP311.1? ? ? 文獻(xiàn)標(biāo)識(shí)碼:A
文章編號(hào):1009-3044(2022)34-0035-04
1 引言
Java或Android的開(kāi)發(fā)者對(duì)注解(Annotation) 機(jī)制一定不會(huì)陌生,在項(xiàng)目開(kāi)發(fā)過(guò)程中,開(kāi)發(fā)者會(huì)接觸到很多注解,如@Override、@Deprecated、@SuppressWarnings等,如果使用框架,可能會(huì)使用到注解@Controller、@Param、@Select等。目前關(guān)于注解原理的資料相對(duì)比較貧乏,很多開(kāi)發(fā)者會(huì)使用注解,但不了解注解的工作原理、運(yùn)行機(jī)制,也不清楚如何編寫注解。
2 Java注解機(jī)制介紹
2.1 注解的概念
注解是JDK5.0引入的一種標(biāo)注機(jī)制,Oracle官方定義為:“Annotations, a form of metadata, provide data about a program that is not part of the program itself.”[1]。即注解是元數(shù)據(jù)的一種形式,注解提供了程序的信息但不屬于程序的一部分。注解可以對(duì)代碼添加附加信息,但不會(huì)侵入業(yè)務(wù)代碼,也不會(huì)影響代碼的具體執(zhí)行過(guò)程[2]。開(kāi)發(fā)者可以對(duì)包、類、接口、字段、方法參數(shù)、局部變量等進(jìn)行注解,添加特殊標(biāo)記,在編譯期或運(yùn)行期可以對(duì)這些被標(biāo)記的類、變量、方法或方法參數(shù)進(jìn)行一些特殊的操作。
注解與Javadoc注釋容易混淆,兩者貌似相同,實(shí)則區(qū)別很大:
Javadoc是Sun公司提供的一種工具,它可以從程序源代碼中抽取類、方法、成員等注釋,然后形成一個(gè)和源代碼配套的API幫助文檔,相當(dāng)于產(chǎn)品說(shuō)明書,是為了便于開(kāi)發(fā)者調(diào)用時(shí)了解類、方法和屬性的作用、用法而誕生的,Javadoc是特殊的、格式化的注釋,本質(zhì)上還是注釋。注釋是給開(kāi)發(fā)者看的。
注解則不一樣,開(kāi)發(fā)者通過(guò)配置,使其在編譯時(shí)、類加載時(shí)、運(yùn)行時(shí)可見(jiàn),還可通過(guò)Java的反射機(jī)制獲取注解內(nèi)容,加入自定義的處理邏輯。注解是非侵入性的,不會(huì)干涉代碼本身的處理流程,而是通過(guò)低耦合的“貼標(biāo)簽”形式,向原有代碼附加信息,編碼輔助工具、部署工具、IDE都可以讀取注解,為開(kāi)發(fā)者提供檢測(cè)代碼、自動(dòng)編碼、驗(yàn)證部署的服務(wù),注解是給機(jī)器看的。
2.2 Java內(nèi)置的標(biāo)準(zhǔn)注解
1) @Override:該注解是使用頻率較高的一個(gè),標(biāo)注在方法上,表示該方法是重寫父類方法。編譯器會(huì)進(jìn)行檢測(cè)是否符合重寫規(guī)則,如果不符合,比如父類(包括接口)沒(méi)有這個(gè)方法,則會(huì)提示錯(cuò)誤。不寫@Override其實(shí)也不會(huì)影響程序運(yùn)行,這個(gè)注解是否就沒(méi)有存在的意義?不是的,編寫代碼時(shí),通常開(kāi)發(fā)者很清楚他要重寫一個(gè)方法,但是可能單詞拼寫錯(cuò)誤,如果沒(méi)加注解,編譯器就發(fā)現(xiàn)不了錯(cuò)誤。
2) @Deprecated:用于標(biāo)記類、成員變量、成員方法或者構(gòu)造方法已廢棄,不推薦開(kāi)發(fā)者使用,如果開(kāi)發(fā)者調(diào)用了被標(biāo)記了@Deprecated的方法,編譯時(shí)會(huì)有警告信息,但仍能強(qiáng)制編譯。
3) @SuppressWarnings:該注解的作用是指示編譯器對(duì)被注解的代碼元素內(nèi)部的某些警告保持靜默,支持在類、屬性、方法、參數(shù)、構(gòu)造方法、本地變量上使用。標(biāo)注了“@SuppressWarnings”不等于消除了警告內(nèi)容,只是編譯器不顯示而已,除非開(kāi)發(fā)者確定該警告的隱患不會(huì)影響程序的正常運(yùn)行,否則不建議使用該注解。
4) @SafeVarargs:JDK7加入的注解,用于取消編譯器產(chǎn)生的unchecked警告。在聲明一個(gè)有泛型的可變參數(shù)的構(gòu)造函數(shù)或者方法時(shí),編譯器會(huì)提示unchecked警告,如果開(kāi)發(fā)者確定該構(gòu)造函數(shù)或方法不會(huì)造成不安全的操作時(shí),可使用@SafeVarargs進(jìn)行修飾,編譯器就會(huì)忽略u(píng)nchecked警告。例如定義了一個(gè)靜態(tài)方法如下,添加上@SafeVarargs后,編譯器警告消失:
@SafeVarargs
public static
//函數(shù)主體
}
5) @FunctionalInterface:JDK8新增了函數(shù)式編程[3],相應(yīng)地加入了函數(shù)式接口注解,所謂函數(shù)式接口實(shí)際上是一個(gè)Lambda表達(dá)式,它本質(zhì)上是接口,比普通接口多了一個(gè)約束:有且僅有一個(gè)抽象方法。標(biāo)注了@FunctionalInterface注解,即指示編譯器檢查開(kāi)發(fā)者編寫的接口是否符合函數(shù)式接口的約束條件,如不符合,編譯器會(huì)給出錯(cuò)誤提示。
2.3 Java內(nèi)置的元注解
元注解是注解的注解,是JDK的基礎(chǔ)注解,它作用在其他注解上面,用于標(biāo)記和描述注解的基本信息。編寫一個(gè)注解需要指明其保留的時(shí)間和生效的上下文等最基本的信息,JDK原生的元注解則提供了可以用于標(biāo)注并描述這些信息的注解。JDK提供的元注解有:
1) @Retention:用于設(shè)定注解的生命周期,可以取值為:①RetentionPolicy.SOURCE,注解只在源碼階段保留,源碼被編譯之后就不存在了。②RetentionPolicy.CLASS,注解內(nèi)容被編譯到字節(jié)碼文件(.class) 里,但JVM讀取字節(jié)碼文件時(shí),并不將其加載,這是@Retention的默認(rèn)取值。③RetentionPolicy.RUNTIME,注解的生命周期貫穿于源碼、.class文件、JVM三個(gè)階段,因此程序在運(yùn)行時(shí)可以獲取到它們。
2) @Documented:注解后Javadoc工具可從源代碼中抽取出類、方法、成員等注釋形成一個(gè)配套的API幫助文檔,默認(rèn)情況下,類和方法的注解內(nèi)容是不會(huì)出現(xiàn)在Javadoc中的,使用@Documented修飾后,該注解即可被Javadoc工具提取到API文檔。要使@Documented注解生效的前提是:@Retention的值需設(shè)置為RetentionPolicy.RUNTIME。
3) @Target:用于指定注解的放置目標(biāo)。注解可用于修飾包、接口、類、方法、變量等類型,@Target注解確定該注解可以出現(xiàn)在哪個(gè)位置,取值范圍定義在ElementType枚舉里:①TYPE:表示可用于標(biāo)注類、接口、注解、枚舉;②FIELD:表示可用于標(biāo)注成員變量;③METHOD:表示可用于標(biāo)注成員方法;④ PARAMETER:表示可用于標(biāo)注參數(shù);⑤CONSTRUCTOR:表示可用于標(biāo)注構(gòu)造器;⑥LOCAL_VARIABLE:表示可用于標(biāo)注本地變量;⑦ANNOTATION_TYPE:表示可用于標(biāo)注注解(即元注解);⑧PACKAGE:表示可用于標(biāo)注包;⑨TYPE_PARAMETER:這是JDK8新增的,表示可用于標(biāo)注自定義類型參數(shù);⑩TYPE_USE:JDK8新增的,表示可用于標(biāo)注除class外的任意類型。
@Target可使用單個(gè)枚舉值,如設(shè)定為元注解,代碼為:
@Target(ElementType. ANNOTATION_TYPE)
也可使用多個(gè)枚舉值,需將多個(gè)枚舉值用大括號(hào)“{}”包圍,如設(shè)定注解可添加到成員方法和成員變量上,代碼為:
@Target({ElementType.METHOD, ElementType.FIELD})
4) @Inherited:用于指明父類注解會(huì)被子類繼承。@Inherited僅針對(duì)@Target(ElementType.TYPE)類型的注解有效,并且僅針對(duì)class的繼承,對(duì)interface的繼承無(wú)效。
5) @Native:JDK8新增的注解,表示被修飾的成員變量可以被本地代碼引用,常被代碼生成工具使用。
6) @Repeatable:JDK8新增的注解。JDK8之前,同一程序元素前最多只能有一個(gè)相同類型的注解。@Repeatable允許在相同的程序元素中重復(fù)注解。
3 自定義注解
內(nèi)置的注解并不多,開(kāi)發(fā)者可以編寫自定義注解,主要有三個(gè)步驟:一是聲明注解,二是處理注解,三是使用注解。
3.1 自定義注解的聲明
3.1.1 自定義注解的語(yǔ)法
[[public] @interface 注解名稱{
[數(shù)據(jù)類型 變量名稱();]
} ]
關(guān)鍵字“@interface”與標(biāo)準(zhǔn)的接口關(guān)鍵字interface是不一樣的,從反編譯的代碼中,可以看到類似“public interface AnnoDemo extends Annotation {}”的代碼,意味著注解繼承了Annotation接口(在java.lang.annotation包中),即該注解就是一個(gè)Annotation,因此注解本質(zhì)上是一個(gè)特殊的接口(interface) ,接口里可以定義什么,注解里同樣也可以定義,它的修飾符與接口一樣,也是默認(rèn)被public abstract修飾。它和普通的接口不一樣的地方:
1) 定義普通接口使用interface修飾,但定義注解使用的是@interface。
2) 普通接口使用implements關(guān)鍵字實(shí)現(xiàn)接口,注解的實(shí)現(xiàn)是由編譯器完成。
3) 普通接口可以繼承多個(gè)接口,注解不能繼承其他的注解或接口。
4) 在定義注解時(shí)可以定義屬性,但是屬性必須使用括號(hào)“()”,形式上是一個(gè)方法。
一個(gè)典型的注解聲明代碼如下:
[@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE,ElementType.TYPE})
public @interface AnnoDemo {
String role() ;
} ]
3.1.2 注解的屬性
如前所述,注解的屬性與普通類屬性不一樣,它的屬性是抽象方法,注解的屬性規(guī)則有:
1) 返回值類型必須是以下幾種:基本數(shù)據(jù)類型、String類型、枚舉類型、注解、以上類型的數(shù)組。
2) 使用時(shí)需給屬性賦值,如下面代碼SomeClass類前面添加了@AnnoDemo注解,并為其role屬性賦值為admin。
[@AnnoDemo(role = "admin")
class SomeClass{
} ]
3) 可以使用default關(guān)鍵字設(shè)置屬性的默認(rèn)值,有默認(rèn)值的屬性,在使用時(shí)可不給屬性賦值。
[public @interface AnnoDemo {
String role() default "user";
}
@AnnoDemo
class SomeClass{
} ]
4) 注解如有多個(gè)屬性,賦值時(shí)可以在注解括號(hào)中用“,”號(hào)隔開(kāi)分別給對(duì)應(yīng)的屬性賦值。
5) 如注解只有一個(gè)屬性,可將其命名為value,賦值時(shí)可省略value直接定義值。
6) 給數(shù)組屬性賦值時(shí),如數(shù)組中只有一個(gè)值,則可以省略“{}”數(shù)組符號(hào)。
3.2 處理注解
注解的處理是自定義注解的核心,生命周期為SORCE、CLASS的注解,需編寫注解處理器處理注解,生命周期為RUNTIME的注解,可通過(guò)反射獲取注解內(nèi)容,再進(jìn)行業(yè)務(wù)處理。
3.2.1 注解處理器
注解處理器(Annotation Processor) 是javac的一個(gè)工具,用于編譯時(shí)掃描和處理注解。開(kāi)發(fā)者如想在編譯期處理注解,需要編寫一個(gè)注解處理器。編寫注解處理器需要繼承JDK自帶的抽象處理器javax.annotation.processing.AbstractProcessor類,AbstractProcessor繼承于Processor接口,提供了以下方法:
1) init(ProcessingEnvironment processingEnv):初始化時(shí)處理工具會(huì)調(diào)用init()方法。ProcessingEnviroment對(duì)象提供很多有用的工具類Elements, Types和Filer。
2) process(Set<? extends TypeElement> annos, RoundEnvironment roundEnvironment):這是開(kāi)發(fā)者需要實(shí)現(xiàn)的方法,需要編寫該注解的業(yè)務(wù)邏輯。RoundEnviroment參數(shù)可讓你查詢包含特定注解的被注解元素。
3) getSupportedAnnotationTypes():獲取支持的注解類型,方法的返回值是字符串集合,如果沒(méi)有支持的類型,則返回空集。該方法也可以用@SupportedAnnotationTypes注解替代。
4) getSupportedSourceVersion():獲取支持的源代碼版本,一般情況下返回SourceVersion.latestSupported(),如果返回的版本小于當(dāng)前編譯器版本,會(huì)有警告提示。
3.2.2 插入式注解處理器原理
插入式注解處理器是JDK6之后提供了一種可以在編譯期進(jìn)行注解讀取和處理的能力,開(kāi)發(fā)者可通過(guò)實(shí)現(xiàn)JDK的API自定義注解處理器實(shí)現(xiàn)干涉編譯器的行為。編譯期的注解,需要插入式注解處理器進(jìn)行解析,注解處理器在編譯過(guò)程中是如何工作的?可以從Javac的編譯流程去分析,如圖1所示:
1) 插入式注解處理器初始化。
2) 解析與填充符號(hào)表,包括詞法、語(yǔ)法分析;將源代碼的字符流轉(zhuǎn)換為標(biāo)記集合,構(gòu)造出抽象語(yǔ)法樹(shù);填充符號(hào)表,產(chǎn)生符號(hào)地址和符號(hào)信息。
3) 插入式注解處理器對(duì)注解的處理,執(zhí)行后如果產(chǎn)生了新的符號(hào),則返回上一步驟,重新處理這些新符號(hào)。
4) 分析與生成字節(jié)碼。
JDK編譯字節(jié)碼前,會(huì)先掃描源代碼中的注解,如果注解有對(duì)應(yīng)的注解處理器,則會(huì)調(diào)用process() 方法處理,因此可能會(huì)產(chǎn)生新的源代碼、修改原有代碼,因此需要進(jìn)行多輪的注解處理。
3.2.3 處理編譯期的注解
生命周期為SOURCE的注解,被編譯為.class文件時(shí)被抹去,生命周期為CLASS的注解,會(huì)被編譯到.class里,但運(yùn)行程序時(shí),不會(huì)被加載到JVM中,在運(yùn)行時(shí)是獲取不到它的信息的,因此這兩類的注解,需要在編譯期處理,步驟為:
1) 開(kāi)發(fā)者編寫一個(gè)繼承于AbstractProcessor類的注解處理器,如MyProcessor,在MyProcessor里重寫初始化方法init()、重寫注解的邏輯實(shí)現(xiàn)process()方法,例如我們的注解的功能是為類添加getter、setter方法,就可以在process()先獲取被注解的類,然后在類中插入getter、setter。
2) 在編譯的參數(shù)中指定MyProcessor,如javac -processer com.zsx.MyProcessor,編譯時(shí)會(huì)自動(dòng)調(diào)用MyProcessor的process()方法。很多IDE如IntelliJ IDEA也支持注解處理器,只需在工具中配置注解處理器的路徑即可。
3.2.4 處理運(yùn)行時(shí)的注解
生命周期為RUNTIME的注解,虛擬機(jī)加載字節(jié)碼文件后,依然能讀取注解的信息,此類注解可通過(guò)Java反射機(jī)制獲取注解內(nèi)容,再進(jìn)行業(yè)務(wù)邏輯處理。
獲取注解的關(guān)鍵是java.lang.reflect.AnnotatedElement接口,它的對(duì)象代表了一個(gè)被注解的元素,AccessibleObject、Class、Constructor、Executable、Field、Method、Package、Parameter類都實(shí)現(xiàn)了這個(gè)接口,可獲取到AnnotatedElement對(duì)象,然后可調(diào)用該對(duì)象提供以下方法訪問(wèn)注解:
1) isAnnotationPresent(Class<?extends Annotation> annoClass):該方法功能是判斷指定類型的注解是否存在,返回一個(gè)布爾類型的值,如存在返回true,反之為false。
2) getDeclaredAnnotations():該方法獲取此元素上的所有注解,但不包括繼承的父類的注解,返回Annotation類型數(shù)組,如果該元素不存在注解,則返回長(zhǎng)度為0的數(shù)組。
3) getAnnotation(Class
4) Annotation[] getAnnotations():該方法獲取此元素上的所有注解,并且包括繼承的父類的注解,這是與getDeclaredAnnotations()的區(qū)別的地方。
例如,我們準(zhǔn)備利用注解機(jī)制實(shí)現(xiàn)日志工具,步驟為:
1) 聲明注解
[@Retention(RetentionPolicy.RUNTIME)//聲明周期為運(yùn)行時(shí)
@Target(ElementType.METHOD)//允許加在方法上
public @interface LogTool {
String action() default "默認(rèn)操作";
String description() default "無(wú)說(shuō)明";
} ]
聲明一個(gè)LogTool注解,設(shè)置元素類型為方法、生命周期為運(yùn)行時(shí),聲明兩個(gè)屬性分別為:action記錄操作類型、description作為備注。
2) 使用注解
定義一個(gè)Dao類,模擬數(shù)據(jù)庫(kù)操作類,在其各個(gè)方法前加上LogTool注解。
[public class Dao {
@LogTool(action="新增",description="新增一行數(shù)據(jù)")
public void addUser(){
}
@LogTool(action = "更新")
public void updateUser(){
}
@LogTool(action="刪除")
public void delUser(){
}
@LogTool
public void initData(){
}
} ]
3) 處理注解
定義一個(gè)注解處理方法parse,傳入被注解的類,對(duì)該類的所有方法進(jìn)行遍歷,使用isAnnotationPresent()方法判斷該方法是否被注解,如果是被注解的方法,通過(guò)getDeclaredAnnotation()方法獲取注解類的實(shí)例,即可獲取注解的兩個(gè)屬性“操作類型”及“備注”的值,然后對(duì)其進(jìn)行其他的業(yè)務(wù)操作。
public class AnnoParser {
public static void parse(Class annoClass) throws Exception{
Method[] array = annoClass.getMethods();
for(Method method : array){
if(method.isAnnotationPresent(LogTool.class)){
String methodName=method.getName();
LogTool methodLog = method.getDeclaredAnnotation(LogTool.class);
String action = String.valueOf(methodLog.action());
String description = String.valueOf(methodLog.description());
System.out.println("方法:"+methodName + " - " + action+"("+description+")");
}
}
}
public static void main(String[] args){
try {
AnnoParser.parse(Dao.class);
}catch(Exception e){
e.printStackTrace();
}
}
}
4 注解的應(yīng)用場(chǎng)景
注解目前在生產(chǎn)環(huán)境已經(jīng)得到了廣泛的應(yīng)用,給開(kāi)發(fā)者帶來(lái)了效率的提升:
1) 檢測(cè)代碼。例如內(nèi)置的@Deprecated、@Override可以幫助開(kāi)發(fā)者減少開(kāi)發(fā)錯(cuò)誤、規(guī)范代碼,企業(yè)也可以根據(jù)內(nèi)部的代碼規(guī)范,編寫自定義注解,檢測(cè)代碼的合法性,提高編碼質(zhì)量。
2) 輔助編碼??梢詭椭_(kāi)發(fā)者自動(dòng)生成部分煩瑣的代碼,提高編碼效率,如第三方庫(kù)Lombok、AutoValue等,可以自動(dòng)插入到編輯器和構(gòu)建工具中,通過(guò)注解生成諸如getter、setter或equals方法等,提高了開(kāi)發(fā)效率。
3) 替代配置文件。例如Servlet現(xiàn)在可以使用注解替代原來(lái)的web.xml部署文件,越來(lái)越多的框架如Spring、Mybatis等使用了注解進(jìn)行開(kāi)發(fā)。
4) 測(cè)試。例如Junit單元測(cè)試框架使用了大量的注解[4]。
5) 面向切面編程應(yīng)用。在需要非侵入式業(yè)務(wù)邏輯的面向切面編程(AOP) [5],如權(quán)限控制、日志、監(jiān)控等場(chǎng)景,注解是較好的解決方案。
5 結(jié)束語(yǔ)
經(jīng)過(guò)多年的迭代優(yōu)化后,目前JDK對(duì)注解已有了較完備的支持,包括內(nèi)置注解、元注解、注解處理器、自定義注解四部分。注解機(jī)制是非常巧妙的、簡(jiǎn)約而強(qiáng)大的設(shè)計(jì),一方面,它簡(jiǎn)約:API簡(jiǎn)潔,對(duì)使用者友好、耦合度低;另一方面,它強(qiáng)大:開(kāi)放、擴(kuò)展性強(qiáng),應(yīng)用面廣,極大地推動(dòng)了Java生態(tài)如開(kāi)發(fā)框架、分析工具、開(kāi)發(fā)工具、部署工具等的發(fā)展。
參考文獻(xiàn):
[1] Lesson:Annotations[EB/OL].[2021-03-20].https://docs.oracle.com/javase/tutorial/java/annotations.
[2] 劉學(xué)玉.JAVA編程語(yǔ)言在計(jì)算機(jī)軟件開(kāi)發(fā)中的應(yīng)用[J].電子技術(shù)與軟件工程,2022(1):57-60.
[3] 趙榮彪.JDK1.8新特性與編程性能[J].信息技術(shù)與信息化,2021(5):145-146,150.
[4] 劉彥楠.JUnit參數(shù)化測(cè)試的應(yīng)用研究[J].信息與電腦(理論版),2021,33(14):30-32.
[5] 遲慧智,孔德智.Java方法增強(qiáng)技術(shù)研究[J].電子產(chǎn)品可靠性與環(huán)境試驗(yàn),2022,40(3):75-80.
【通聯(lián)編輯:謝媛媛】