一沉御、注解
從JDK 5 開始屿讽,Java 新增注解,注解是代碼里的特殊標記吠裆,這些標記可以在編譯伐谈、類加載脏里、運行時被讀取挠日,并執(zhí)行相應(yīng)的處理。
通過使用注解助赞,開發(fā)人員可以在不改變原有邏輯的情況下效斑,在源文件中嵌入一些補充的信息非春。代碼分析工具柱徙、開發(fā)工具和部署工具可以通過這些補充信息進行驗證缓屠、處理或者進行部署。
1.1 注解分類
注解分為標準注解和元注解护侮。
1.1.1 標準注解
標準注解有以下 4 種敌完。
? @Override:對覆蓋超類中的方法進行標記,如果被標記的方法并沒有實際覆蓋超類中的方法羊初,則編譯器會發(fā)出錯誤警告滨溉。
? @Deprecated:對不鼓勵使用或者已過時的方法添加注解什湘,當(dāng)編程人員使用這些方法時,將會在編譯時顯示提示信息晦攒。
? @SuppressWarnings:選擇性地取消特定代碼段中的警告闽撤。
? @SafeVarargs:JDK 7 新增,用來聲明使用了可變長度參數(shù)的方法脯颜,其在與泛型類一起使用時不會出現(xiàn)類型安全問題哟旗。
1.1.2. 元注解
元注解用來注解其他注解,從而創(chuàng)建新的注解栋操。元注解有以下幾種闸餐。
? @Targe:注解所修飾的對象范圍。
? @Inherited:表示注解可以被繼承矾芙。
? @Documented:表示這個注解應(yīng)該被 JavaDoc 工具記錄舍沙。
? @Retention:用來聲明注解的保留策略。
? @Repeatable:JDK 8 新增剔宪,允許一個注解在同一聲明類型(類拂铡、屬性或方法)上多次使用。
其中@Targe 注解取值是一個 ElementType 類型的數(shù)組葱绒,其中有以下幾種取值和媳,對應(yīng)不同的對象范圍。
? ElementType.TYPE:能修飾類哈街、接口或枚舉類型留瞳。
? ElementType.FIELD:能修飾成員變量。
? ElementType.METHOD:能修飾方法骚秦。
? ElementType.PARAMETER:能修飾參數(shù)她倘。
? ElementType.CONSTRUCTOR:能修飾構(gòu)造方法。
? ElementType.LOCAL_VARIABLE:能修飾局部變量作箍。
? ElementType.ANNOTATION_TYPE:能修飾注解硬梁。
? ElementType.PACKAGE:能修飾包。
? ElementType.TYPE_PARAMETER:類型參數(shù)聲明胞得。
? ElementType.TYPE_USE:使用類型荧止。
其中@Retention 注解有 3 種類型,分別表示不同級別的保留策略阶剑。
? RetentionPolicy.SOURCE:源碼級注解跃巡。注解信息只會保留在.java 源碼中,源碼在編譯后牧愁,注解信息被丟棄素邪,不會保留在.class 中。
? RetentionPolicy.CLASS:編譯時注解猪半。注解信息會保留在.java 源碼以及.class 中兔朦。當(dāng)運行 Java 程序時偷线,JVM 會丟棄該注解信息,不會保留在 JVM 中沽甥。
? RetentionPolicy.RUNTIME:運行時注解声邦。當(dāng)運行 Java 程序時,JVM 也會保留該注解信息摆舟,可以通過反射獲取該注解信息翔忽。
1.2 定義注解
1.2.1 基本定義
定義新的注解類型使用@interface 關(guān)鍵字,這與定義一個接口很像盏檐,如下所示:
public @interface AnnotationCreateMan {
}
定義完注解后歇式,就可以在程序中使用該注解:
@AnnotationCreateMan
public class AnnotationTest {
}
1.2.2 定義成員變量
注解只有成員變量,沒有方法胡野。注解的成員變量在注解定義中以“無形參的方法”形式來聲明材失,其“方法名”定義了該成員變量的名字,其返回值定義了該成員變量的類型:
public @interface AnnotationCreateMan {
String name();
int age();
}
上面的代碼定義了兩個成員變量硫豆,這兩個成員變量以方法的形式來定義龙巨。定義了成員變量后,使用該注解時就應(yīng)該為該注解的成員變量指定值:
@AnnotationCreateMan(name = "Tom", age = 35)
public class AnnotationTest {
@AnnotationCreateMan(name = "Jim", age = 22)
public void fighting() {
}
}
也可以在定義注解的成員變量時熊响,使用 default 關(guān)鍵字為其指定默認值旨别,如下所示:
public @interface AnnotationCreateMan {
String name() default "Jack";
int age() default 25;
}
因為注解定義了默認值,所以使用時可以不為這些成員變量指定值汗茄,而是直接使用默認值:
@AnnotationCreateMan(name = "Tom", age = 35)
public class AnnotationTest {
@AnnotationCreateMan
public void fight() {
}
}
1.2.3 定義運行時注解
可以用@Retention 來設(shè)定注解的保留策略秸弛,這 3 個策略的生命周期長度為 SOURCE < CLASS < RUNTIME 。生命周期短的能起作用的地方洪碳,生命周期長的一定也能起作用递览。一般如果需要在運行時去動態(tài)獲取注解信息,那只能用 RetentionPolicy.RUNTIME瞳腌;如果要在編譯時進行一些預(yù)處理操作绞铃,比如生成一些輔助代碼,就用 RetentionPolicy.CLASS嫂侍;如果只是做一些檢查性的操作儿捧,比如 @Override 和 @SuppressWarnings,則可選用 RetentionPolicy.SOURCE挑宠。當(dāng)
設(shè)定為 RetentionPolicy.RUNTIME 時菲盾,這個注解就是運行時注解,如下所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationCreateMan {
String name() default "Jack";
int age() default 25;
}
1.2.4 定義編譯時注解
同樣地痹栖,如果將@Retention 的保留策略設(shè)定為 RetentionPolicy.CLASS亿汞,這個注解就是編譯時注解瞭空,如下所示:
@Retention(RetentionPolicy.CLASS)
public @interface AnnotationCreateMan {
String name() default "Jack";
int age() default 25;
}
1.3 注解處理器
如果沒有處理注解的工具揪阿,那么注解也不會有什么大的作用疗我。對于不同的注解有不同的注解處理器。雖然注解處理器的編寫會千變?nèi)f化南捂,但是其也有處理標準吴裤,比如:針對運行時注解會采用反射機制處理,針對編譯時注解會采用 AbstractProcessor 來處理溺健。
1.3.1 運行時注解處理器
處理運行時注解需要用到反射機制麦牺。首先我們要定義運行時注解,如下所示:
@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
String value() default ""鞭缭;
}
上面的代碼是 Retrofit 中定義的@GET 注解剖膳。其定義了@Target(METHOD),這等效于@Target(ElementType.METHOD)岭辣,意味著 GET 注解應(yīng)用于方法吱晒。接下來應(yīng)用該注解,如下所示:
public class AnnotationTest {
@GET(value = "http://ip.taobao.com/59.108.54.3")
public String getIpMsg() {
return "";
}
@GET(value = "http://ip.taobao.com/")
public String getIp() {
return "";
}
}
上面的代碼為@GET 的成員變量賦值沦童。接下來寫一個簡單的注解處理器仑濒,如下所示:
public class AnnotationProcessor {
private static final String TAG = "AnnotationProcessor";
public static void initAnnotation() {
Method[] methods = AnnotationTest.class.getDeclaredMethods();
for (Method m : methods) {
GET get = m.getAnnotation(GET.class);
Log.v(TAG, get.value());
}
}
}
上面的代碼用到了兩個反射方法:getDeclaredMethods 和 getAnnotation,它們都屬于AnnotatedElement 接口偷遗,Class墩瞳、Method 和 Filed 等類都實現(xiàn)了該接口。調(diào)用 getAnnotation 方法返回指定類型的注解對象氏豌,也就是 GET喉酌。最后調(diào)用 GET 的 value 方法返回從 GET 對象中提取元素的值。
調(diào)用注解處理器:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnotationProcessor.initAnnotation();
}
}
輸出結(jié)果為:
1.3.2 編譯時注解處理器
處理編譯時注解的步驟稍微有點多泵喘,首先要定義注解瞭吃。
(1)定義注解
這里首先在項目中新建一個 Java Library 來專門存放注解,這個 Library 名為 annotations涣旨。接下來定義注解歪架,如下所示:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value() default 1;
}
上面代碼中定義的注解類似于 ButterKnife 的@BindView 注解。
(2)編寫注解處理器
我們在項目中再新建一個 Java Library 來存放注解處理器霹陡,這個 Library 名為 processor和蚪。我們來配置 processor 庫的 build.gradle:
apply plugin: 'java'
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':annotations')
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
接下來編寫注解處理器 ClassProcessor,它繼承 AbstractProcessor烹棉,如下所示:
public class ClassProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//TODO 處理注解
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotations = new LinkedHashSet<String>();
annotations.add(BindView.class.getCanonicalName());
return annotations;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
process 方法的實現(xiàn)會在后文講到攒霹,這里首先分別介紹這 4 個方法的作用。
? init:被注解處理工具調(diào)用浆洗,并輸入 ProcessingEnviroment 參數(shù)催束。ProcessingEnviroment提供很多有用的工具類,比如 Elements伏社、TypesFiler 和 Messager 等抠刺。
? process:相當(dāng)于每個處理器的主函數(shù) main()塔淤,在這里寫你的掃描、評估和處理注解的代碼速妖,以及生成 Java 文件高蜂。輸入?yún)?shù)RoundEnviroment,可以讓你查詢出包含特定注解的被注解元素罕容。
? getSupportedAnnotationTypes:這是必須指定的方法备恤,指定這個注解處理器是注冊給哪個注解的。注意锦秒,它的返回值是一個字符串的集合露泊,包含本處理器想要處理的注解類型的合法全稱。
? getSupportedSourceVersion :用來指定你使用的 Java 版本旅择,通常這里返回SourceVersion.latestSupported()滤淳。 在Java 7 以后,也可以使用注解來代替getSupportedAnnotationTypes 方法getSupportedSourceVersion方法砌左,如下所示:
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.lzw.annotations.BindView")
public class ClassProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
但是考慮到 Android 兼容性的問題脖咐,這里不建議采用這種注解的方式。接下來編寫還未實現(xiàn)的 process 方法汇歹,如下所示:
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Messager messager = processingEnv.getMessager();
for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
if (element.getKind() == ElementKind.FIELD) {
messager.printMessage(Diagnostic.Kind.NOTE, "printMessage:"
+ element.toString());
}
}
return true;
}
這里用到 Messager 的 printMessage 方法來打印出注解修飾的成員變量的名稱屁擅,這個名稱會在 Android Studio 的 Gradle Console 窗口中打印出來。
(3)注冊注解處理器
為了能使用注解處理器产弹,需要用一個服務(wù)文件來注冊它∨筛瑁現(xiàn)在我們就來創(chuàng)建這個服務(wù)文件。
- 首先在 processor 庫的 main 目錄下新建 resources 資源文件夾
- 接下來在 resources 中再建立META-INF/services 目錄文件夾痰哨。
- 最后在 META-INF/services 中創(chuàng)建 javax.annotation.processing.Processor 文件胶果,這個文件中的內(nèi)容是注解處理器的名稱。
這里我們的 javax.annotation.processing.Processor 文件的內(nèi)容為 com.lzw.annotationprocessor.ClassProcessor斤斧。
整個項目的目錄結(jié)構(gòu)如圖 9-1 所示
如果你覺得前面創(chuàng)建服務(wù)文件的步驟比較麻煩早抠,也可以使用 Google 開源的 AutoService,它用來自動生成 METAINF/services/javax.annotation.processing.Processor 文件撬讽。首先我們添加該開源庫蕊连,可以在 File→Project Structure 搜索“auto-service”查找該庫并添加,如下圖所示游昼。也可以在 processor 的 build.gradle 中直接添加如下代碼:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(":annotations")
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
}
最后在注解處理器 ClassProcessor 中添加@AutoService(Processor.class)就可以了:
@AutoService(Processor.class)
public class ClassProcessor extends AbstractProcessor {
...
}
(4)應(yīng)用注解
接下來在我們的主工程項目(app)中引用注解甘苍。首先要在主工程項目的 build.graldle 中引用 annotations 和 processor 這兩個庫:
dependencies {
...
implementation project(':annotations')
annotationProcessor project(':annotationProcessor')
}
接下來在 MainActivity 中應(yīng)用注解,如下所示:
public class MainActivity extends AppCompatActivity {
//編譯時注解
@BindView(value = 111)
TextView tv_show_name;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//運行時注解
AnnotationProcessor.initAnnotation();
}
}
最后烘豌,我們先 Clean Project 再 Make Project载庭,在 Gradle Console 窗口中打印的結(jié)果如圖所示:
可以發(fā)現(xiàn)編譯時會打印出@BindView 注解修飾的成員變量名:tv_show_name。
(5)使用 android-apt 插件(這個問題目前還未解決)
我們的主工程項目(app)中引用了 processor 庫,但注解處理器只在編譯處理期間需要用到囚聚,編譯處理完后就沒有實際作用了靖榕,而主工程項目添加了這個庫會引入很多不必要的文件。為了處理這個問題我們需要引入插件 android-apt靡挥。它主要有兩個作用:
? 僅僅在編譯時期去依賴注解處理器所在的函數(shù)庫并進行工作序矩,但不會打包到 APK 中鸯绿。
? 為注解處理器生成的代碼設(shè)置好路徑跋破,以便 Android Studio 能夠找到它。
接下來介紹如何使用它瓶蝴。首先需要在整個工程(Progect)的 build.gradle 中添加如下語句:
buildscript {
...
dependencies {
...
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
} }
接下來在主工程項目(app)的 build.gradle 中以 apt 的方式引入注解處理器 processor毒返,如下所示:
...
apply plugin: 'com.neenbedankt.android-apt'
...
dependencies {
...
//compile project(':processor')
apt project(':processor')
}