內(nèi)容:
- 注解的定義
- 注解的語法
- 源碼級別的注解的使用
- 運(yùn)行時注解的使用
- 編譯時注解的使用
- Android 預(yù)置的注解
一 注解的定義
- 注解(Annotation)邓线,也叫元數(shù)據(jù)。
- 一種代碼級別的說明羡洛。它是 JDK 1.5 以后版本引入的一個特性峻呛,與類糟袁、接口、枚舉是在同一個層次伶棒。
- 它可以聲明在包旺垒、類、字段肤无、方法袖牙、局部變量、方法參數(shù)等元素上舅锄。
- 它提供數(shù)據(jù)用來解釋程序代碼鞭达,但是注解并非是所解釋的代碼本身的一部分。
- 注解對于代碼的運(yùn)行效果沒有直接影響皇忿。
注解有許多用處畴蹭,主要如下:
提供信息給編譯器: 編譯器可以利用注解來探測錯誤和警告信息
編譯階段時的處理: 軟件工具可以用來利用注解信息來生成代碼、Html 文檔或者做其它相應(yīng)處理鳍烁。
運(yùn)行時的處理: 某些注解可以在程序運(yùn)行的時候接受代碼的提取
如我們所熟知的依賴注入框架 ButterKnife 就是在編譯階段來生成 findViewById 的代碼(文件)的
而我們所見過的 @Deprecated 就是提供信息給編輯器的RetentionPolicy.SOURCE類型注解叨襟,
在自定義了一個編譯或者運(yùn)行階段的注解后,需要一個開發(fā)者編寫相應(yīng)的代碼來解釋這些注解幔荒,從而來發(fā)揮注解的作用糊闽。
這些用來解釋注解的代碼被統(tǒng)稱為是 APT(Annotation Processing Tool)。換句話說注解其實(shí)是給 APT 或者編輯器來使用的爹梁,而對于非框架開發(fā)人員的我們我們只需要關(guān)注注解的使用右犹,并遵守規(guī)則即可,從而我們節(jié)省了很多代碼提高了效率姚垃。
但是凡事如果只滿足于用上念链,就不算是一個合 (tong) 格 (guo) 程 (mian)序 (shi) 員 (de)! 但是不要慌,當(dāng)你打開這篇文章的時候你已經(jīng)離 offer 又進(jìn)了一步。
二掂墓、 注解的語法
2.1 注解的聲明
注解的聲明和聲明一個接口十分類似谦纱,沒錯只是名字很類似~ 我們使用@interface
來聲明一個注解,如我們最常見的Override 注解的聲明
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
注解聲明的修飾符,可以是 private,public, protected 或者默認(rèn) default 這一點(diǎn)跟定義一個類或者接口相同君编。
在聲明一個注解的時候我們常常需要一些其他注解來修飾和限制該定義注解的使用和運(yùn)行方式跨嘉。上述的 @Target 和 @Retention 就是如此,我們稱之為元注解吃嘿,詳細(xì)的元注解在下邊說明祠乃。
2.2 注解成員
- 注解跟一個類相似,它們并不是都是像上面的 @Override一樣只有聲明唠椭。
- 一個類大概可以包含構(gòu)造函數(shù),成員變量忍饰,成員函數(shù)等贪嫂,而一個注解只能包含注解成員,注解成員的聲明格式為:
類型 參數(shù)名() default 默認(rèn)值;
注解成員可以是:
- 基本類型 byte,short,int,long,float,double,boolean 八種基本類型及這些類型的數(shù)組艾蓝, 注意這里沒對應(yīng)基本數(shù)據(jù)類型的包裝類力崇。
- String,Enum,Class,annotations 及這些類型的數(shù)組
- 注解的成員修飾符只能是 public 或默認(rèn)(default)
- 注解元素必須有確定的值,可以在注解中定義默認(rèn)值,也可以使用注解時指定。即我們在定義注解的時候聲明的成員赢织,可以不賦值亮靴,但是就跟抽象函數(shù)一樣,在使用的時候就必須指定于置。
public @interface TestAnnotation {
String value() default "";
String[] values();
int id() default -1;
int[] ids();
// 錯誤的不能使用包裝類 以及自定義類型
// Integer idInt();
// Apple apple();
enum Color {BULE, RED, GREEN}
Color testEnum() default Color.BULE;
Color[] testEnums();
//注解類型成員 注解元素必須有確定的值,可以在注解中定義默認(rèn)值,也可以使用注解時指定
FruitName fruitName() default @FruitName("apple");
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface FruitName {
String value();
String alias() default "no alias";
}
三茧吊、元注解
- 我們在 Override 注解的聲明中可以看到還有注解修飾著如@Target(ElementType.METHOD),我們講元注解理解為修飾注解定義的注解八毯。
- 換句話說元注解為 JDK 提供給我們的一些基本注解搓侄,我們使用元注解來定義一個注解是如何工作的。
JDK 1.8 中存在的元注解有以下 5 種:
@Target, @Retention话速、@Documented讶踪、@Inherited、@Repeatable
下面我們依次來說明這幾種類型的注解是如何使用的泊交。
3.1 @Target 元注解
@Target 指定了被修飾的注解運(yùn)用的地方乳讥,這些 "地方" 定義在 ElementType 類中,包括:
- ElementType.ANNOTATION_TYPE 可以給一個注解進(jìn)行注解
- ElementType.CONSTRUCTOR 可以給構(gòu)造方法進(jìn)行注解
- ElementType.FIELD 可以給屬性進(jìn)行注解
- ElementType.LOCAL_VARIABLE 可以給局部變量進(jìn)行注解
- ElementType.METHOD 可以給方法進(jìn)行注解
- ElementType.PACKAGE 可以給一個包進(jìn)行注解
- ElementType.PARAMETER 可以給一個方法內(nèi)的參數(shù)進(jìn)行注解
- ElementType.TYPE 可以給一個類型進(jìn)行注解廓俭,比如類云石、接口、枚舉
其中 METHOD研乒、PARAMETER留晚、FIELD 最為常見,如 Override 注解被 @Target(ElementType.METHOD) 修飾,如果我們想要標(biāo)記一個參數(shù)不能為空則可以使用 @NonNull 去修飾一個 param错维, FIELD 用來指定注解只能用來修飾成員變量如我們經(jīng)常使用的 @BindView奖地。
值得注意的是 @Target 元注解定義如下,
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
它內(nèi)部的成員為ElementType[] 數(shù)組也就是說赋焕,我們可以同時指定一個注解可以用于很多地方参歹。如 @ColorRes 的注解的元注解為@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})。
3.2 Retention 元注解
Retention 翻譯過來是保留期的意思隆判。當(dāng)@Retention 用于修飾一個注解上的時候犬庇,它規(guī)定了了被修飾的注解應(yīng)用的時期,或者存活的時期
它可以有如下 3 種取值:
- RetentionPolicy.SOURCE 注解只在源碼階段保留侨嘀,在編譯器進(jìn)行編譯時它將被丟棄臭挽。
- RetentionPolicy.RUNTIME 注解可以保留到程序運(yùn)行的時候,它會被加載進(jìn)入到 JVM 中咬腕,所以在程序運(yùn)行時通過反射獲取到它們欢峰,并解釋他們。
- RetentionPolicy.CLASS 注解只被保留到編譯進(jìn)行的時候涨共,它并不會被加載到 JVM 中纽帖。
3.2.1 源碼級別注解 RetentionPolicy.SOURCE
- 對于第一種 RetentionPolicy.SOURCE 注解只在源碼階段保留镰吵,更多的效果時做一些編譯檢查
- 在 Android 中有個為 @IntDef 的注解煎娇,他可以和常量組合一起 代替枚舉 enum 做參數(shù)限制作用毙芜,來優(yōu)化內(nèi)存使用盛撑。
這里說只是替代了參數(shù)限制作用灌灾,而 JDK 1.5 為我們帶來的 enum 的作用不只是簡單的參數(shù)限制作用作用限寞,對于 Enum 更多優(yōu)雅使用可以參考 《Effective Java》泼橘。
如 @IntDef 的注解定義如下:
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
long[] value() default {};
boolean flag() default false;
}
如我們常用的設(shè)置一個 View 的可見屬性就使用了 @IntDef 注解來保證使用者傳入的參數(shù)是對的秘噪,如下:
@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}
@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}
//設(shè)置一個 View 的屬性:
...
toolbar.setVisibility(View.VISIBLE);// it is Ok
//toolbar.setVisibility(1000);// 如果我們隨便寫一個數(shù)值 那么編輯器將會報錯
3.2.2 運(yùn)行期時的注解 RetentionPolicy.RUNTIME
- 源碼級別的注解對我們的編碼約束
- 運(yùn)行期注解與之不同的是魁索,如果要是讓該注解生效波俄,我們必須要編寫一定的代碼去將定義好的注解,在運(yùn)行中"注入"應(yīng)用中蛾默,看到運(yùn)行時注入就可以應(yīng)該能想得起Java反射懦铺,是的注入這個操作就是需要開發(fā)人員自己編寫的。
- 另外支鸡,我們也都了解冬念,在運(yùn)行反射的時候效率是無法保證的。因?yàn)榉瓷鋵⒈闅v對應(yīng)類的 Class 文件來獲取相應(yīng)的信息牧挣。所以運(yùn)行時注解急前,并不是那么廣泛被運(yùn)用
- 而編譯期注解則不會對程序的運(yùn)行造成效率的影響,因此應(yīng)用更廣泛一些瀑构。
我們來試著寫一個 Dota 英雄名稱的運(yùn)行期注解來了解下他的運(yùn)作方式:
/**
* 定義一個注解表示英雄的名字
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
private @interface HeroName {
String value();
String alias();
}
/**
* 定義一個類包含英雄名稱的屬性
*/
public class Hero {
// 定義注解的時候沒有 deflaut 屬性名稱所以在使用的時候必須賦值
@HeroName(value = "Spirit Walker", alias = "NB")
private String heroName;
public void setHeroName(String heroName) {
this.heroName = heroName;
}
public String getHeroName() {
return heroName;
}
}
ok 聲明就是這么簡單裆针,那么如何讓一個屬性生效呢刨摩,這時候我們就需要一個注解處理方法。為了方便觀察運(yùn)行注解的結(jié)果世吨,所以我們這個處理方法選擇傳遞一個 Hero 對象澡刹,不過你為了更通用也可以不用這么做。
/** 運(yùn)行時注解處理方法*/
public static void getHeroNameInfo(Hero hero) {
try {
Class<? extends Hero> clazz = hero.getClass();
Field field = clazz.getDeclaredField("heroName");
// Field isAnnotationPresent 判斷一個屬性是否被對應(yīng)的注解修飾
if (field.isAnnotationPresent(HeroName.class)) {
//field.getAnnotation 獲取屬性的注解
HeroName fruitNameAnno = field.getAnnotation(HeroName.class);
hero.setHeroName("name = " +fruitNameAnno.value() +" alias = " + fruitNameAnno.alias());
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
下面我們來運(yùn)行下程序測試下:
public static void main(String[] args) {
Hero hero = new Hero();
getHeroNameInfo(hero);
System.out.println("hero = " + hero);
}
運(yùn)行結(jié)果:
hero = Hero{heroName='name = Spirit Walker alias = NB'}
3.2.3 編譯時期的注解 RetentionPolicy.CLASS
- 接下來到了編譯時注解耘婚,這個注解類型罢浇,便是眾多工具庫中應(yīng)用的注解類型,它不會影響運(yùn)行時的效率問題沐祷,而是在編譯期嚷闭,或者打包過程中就生成了對應(yīng)的代碼,在運(yùn)行時將會生效赖临。
- 如我們常見的 ButterKnife 和 EventBus胞锰。
- 編譯時注解與運(yùn)行時注解不同,編譯時注解主要是幫助我們在編譯器編譯期使用注解處理器生成相應(yīng)的代碼兢榨,幫我們解放勞動力嗅榕。
我們知道運(yùn)行時注解是通過反射來解釋對應(yīng)注解并使注解生效的,那么編譯時如何解釋對應(yīng)的注解呢色乾?這里就需要用到注解處理器的知識了誊册。
注解處理器(Annotation Processor)是javac的一個工具领突,它用來在編譯時掃描和處理注解(Annotation)暖璧。你可以自定義注解,并注冊相應(yīng)的注解處理器(自定義的注解處理器需繼承自AbstractProcessor)君旦。
- Java 中提供給我們了注解處理器實(shí)現(xiàn)方法澎办,主要是通過實(shí)現(xiàn)一個名為
AbstractProcessor
的注解處理器基類。 - 該抽象類要求我們
必須實(shí)現(xiàn) process
方法來定義處理邏輯金砍。
public class NameProcessor extends AbstractProcessor {
//會被注解處理工具調(diào)用局蚀,并輸入ProcessingEnviroment參數(shù)。ProcessingEnviroment提供很多有用的工具類如Elements, Types和Filer等
@Override
public synchronized void init(ProcessingEnvironment env){ }
//返回最高所支持的java版本, 如返回 SourceVersion.latestSupported();
@Override
public SourceVersion getSupportedSourceVersion() { }
//一個注解處理器可能會處理多個注解邏輯恕稠,這個方法將返回待處理的注解類型集合琅绅,返回值作為參數(shù)傳遞給 process 方法。
@Override
public Set<String> getSupportedAnnotationTypes() { }
//process 函數(shù)就是我們處理待處理注解的地方了鹅巍,我們需要在這里編寫生成 java 文件的具體邏輯千扶。 方法返回布爾值類型,表示注解是否已經(jīng)處理完成骆捧。一般情況下我們返回 true 即可澎羞。
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
注解處理器的處理步驟主要有以下:
- 編譯器開始執(zhí)行注解處理器
- process 方法中循環(huán)處理注解元素(Element),找到被該注解所修飾的類敛苇,方法妆绞,或者屬性
- 拿上一步得到的備注注解修飾的類或者屬性,方法,生成對應(yīng)的輔助類括饶,并寫入 java 文件
- 生成 java 文件后就可以在運(yùn)行時株茶,在程序中獲取并調(diào)用對應(yīng)的輔助方法,如 ButterKnife.bind(this); 方法就是獲取對應(yīng) Activity 的注解處理器生成的java 文件巷帝,并執(zhí)行了構(gòu)造函數(shù)
四 自定義一個編譯時注解
- 自定義編譯時注解要比運(yùn)行時注解要繁瑣一些忌卤。下面我們來舉一個簡單的例子,意在說明編譯時注解是如何工作的楞泼。
在 Android 中為了實(shí)現(xiàn)一個編譯時注解我們一般需要借助兩個三方庫:
- com.google.auto.service:auto-service:1.0-rc2 這是谷歌官方提供的一個注解處理注冊插件可以幫助我們更方便的注冊注解處理器驰徊,只需要在自定義的 Processor 類上方添加@AutoService(Processor.class)即可,不用自己動手執(zhí)行注解處理器的注冊工作(即編寫 resource/META-INF/services/javax.annotation.processing.Processor文件)堕阔。
- 為了更方便的在 process 文件中生成 Java 類棍厂,需要依賴一個 Square 公司開源的 javapoet 庫,com.squareup:javapoet:1.9.0 這個庫中包裝提供了一些好用的 API 幫助我們更快更準(zhǔn)確的構(gòu)建 .java 文件超陆。當(dāng)然你也可以自己手寫拼接字符串然后寫入文件(如果你能保證正確)牺弹。
仿照 ButterKnife 的實(shí)現(xiàn),我們建立一個新的 Android project 时呀,然后創(chuàng)建兩個 Java Moudle张漂,其中 processor 用來存放注解處理器,processor-lib 用來存放對應(yīng)的注解谨娜,如下圖所示:
在注解處理器存在的lib的 build.gradle 中添加依賴關(guān)系:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile project(':processor-lib')
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
}
主 moudle 中也需要添加對 processor 和 processor -lib 的依賴:
dependencies {
....
implementation project(':processor-lib')
// 注意這里的注解處理器的依賴方式
annotationProcessor project(':processor')
}
好了經(jīng)過上述的準(zhǔn)備我們終于能夠編寫我們的編譯時注解了:
- 在 processor-lib 定義一個 Name 注解如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Name {
String name();
String alias();
}
- 編寫兩個類使用我們定義的注解:
public class SBHero {
@Name(name = "Spirit Walker", alias = "SB")
private String heroName;
}
public class PAHero {
@Name(name = "Phantom Assassin", alias = "PA")
private String heroName;
}
- 在 processor 注解處理lib 下定義一個 NamePorcessor
// @AutoService(Processor.class) 幫助我們生成對應(yīng)的注解處理器配置
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.wangshijia.www.processor.Name")
public class NamePorcessor extends AbstractProcessor {
//文件寫入工具類
private Filer filer;
//可以幫助我們在 gradle 控制臺打印信息的類
private Messager messager;
// 元素操作的輔助類
private Elements elementUtils;
//自定義文件名的后綴
private static final String SUFFIX = "AutoGenerate";
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* @return 你所需要處理的所有注解航攒,該方法的返回值會被 process()方法所接收, 這里其實(shí)只有Name 注解趴梢,
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> set = new HashSet<>();
set.add(Name.class.getCanonicalName());
return set;
}
...
}
- 最后我們要編輯我們的 process 方法了,process 方法中一共進(jìn)行了下面這幾件事:
- 遍歷程序中所有被該注解修飾器處理注解修飾的元素 存放進(jìn)創(chuàng)建的Map集合
- 依次取出map 中的元素構(gòu)建對應(yīng)的類和方法
- 構(gòu)建對應(yīng)的方法內(nèi)容
- 生成.java 文件 位置在 ~/app/build/generated/source/apt 目錄下
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
String packageName= "";
// 獲得被該注解聲明的元素
Set<? extends Element> elememts = roundEnv.getElementsAnnotatedWith(Name.class);
// 聲明一個存放成員變量的列表
List<VariableElement> fields;
//key 對應(yīng)包含注解修飾元素的類的全類名 vaule 代表所有被注解修飾的變量
Map<String, List<VariableElement>> maps = new HashMap<>();
// 遍歷程序中所有被該注解修飾器處理注解修飾的元素
for (Element ele : elememts) {
// ele.getKind() 獲取注解修飾的成員的類型漠畜,判斷該元素是否為成員變量
if (ele.getKind() == ElementKind.FIELD) {
VariableElement varELe = (VariableElement) ele;
// 獲取該元素封裝類型
TypeElement enclosingElement = (TypeElement) varELe.getEnclosingElement();
// 拿到包含 enclosingElement 元素的類的名稱 樣式如 com.wangshijia.www.annotationapplication.Hero
String key = enclosingElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
fields = maps.get(key);
if (fields == null) {
maps.put(key, fields = new ArrayList<>());
}
fields.add(varELe);
}
}
/*
* maps 包含有所有被 @Name 修飾的類
*/
for (String key : maps.keySet()) {
List<VariableElement> elementFileds = maps.get(key);
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + elementFileds);
String className = key.substring(key.lastIndexOf(".") + 1);
className += SUFFIX;
// 創(chuàng)建 className 類
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
// 創(chuàng)建方法
MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("printNameAnnotation")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class);
//創(chuàng)建方法中的打印語句
for (VariableElement e : elementFileds) {
Name annotation = e.getAnnotation(Name.class);
// 創(chuàng)建 printNameAnnotation 方法
methodBuild
.addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.name())
.addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.alias());
}
//將方法中添加到類中
MethodSpec printNameMethodSpec = methodBuild.build();
TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();
try {
//構(gòu)造的 java 文件 參數(shù)一 包名,參數(shù)二 上述構(gòu)建的類描述 TypeSpec
JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
javaFile.writeTo(filer);
} catch (IOException exception) {
exception.printStackTrace();
}
}
上述注釋寫的很詳細(xì)了坞靶,這里希望不熟悉的朋友憔狞,自己動手實(shí)現(xiàn)下,才能更好的理解是如何構(gòu)建對應(yīng)的文件的彰阴。生成的文件位于指定目錄下:
- 使用我們定義好的注解生成文件
- 使用注解生成器生成的 java 文件和普通的類沒什么區(qū)別
- 通過編譯后就放在上述文件夾中瘾敢,我們可以正常調(diào)用我們構(gòu)造類的方法,ButterKnife.bind(this) 實(shí)際上就是調(diào)用生成類的方法的過程尿这。
- 我們是一個簡單的 demo 就不這么復(fù)雜的調(diào)用了簇抵。直接在 App 目錄下的任意文件調(diào)用,如在一個 Activity 中:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PAHeroAutoGenerate.printNameAnnotation();
SBHeroAutoGenerate.printNameAnnotation();
}
}
五 Android 預(yù)置的注解
- 日常開發(fā)中妻味,注解能夠幫助我們寫出更好更優(yōu)秀的代碼正压,為了更好地支持 Android 開發(fā),在已有的 android.annotation 基礎(chǔ)上责球,Google 開發(fā)了 android.support.annotation 擴(kuò)展包焦履,共計(jì)50個注解拓劝,幫助開發(fā)者們寫出更優(yōu)秀的程序,這五十多種注解得以應(yīng)用場景各不相同嘉裤,常見的如 @IntDef @ColorInt @Nullable郑临。