請看前言
上一篇文章我們說到運行時框架是在虛擬機(jī)運行程序時使用反射技術(shù)搭建的框架忘朝;而源碼級框架是在javac編譯源碼時,生成框架代碼或文件。源碼級別框架發(fā)生過程是在編譯期間虐块,并不會過多影響到運行效率。因此,Android等對效率性能要求較高的平臺一般使用源碼級別注解來搭建苍柏。
注解處理器
注解處理器是一個在javac中的,用來編譯時掃描和處理的注解的工具姜贡。
一個注解的注解處理器试吁,以Java代碼(或者編譯過的字節(jié)碼)作為輸入,生成文件(通常是.java文件)作為輸出楼咳。這具體的含義什么呢熄捍?你可以生成Java代碼!
生成的Java代碼是在新的.java文件中母怜,所以你并不能修改已存在的Java類治唤,例如向已有的類中添加方法。并且這些生成的Java文件糙申,會同其他普通的手動編寫的Java源代碼一樣被javac編譯宾添。
寫代碼之前
在開始寫代碼之前,我們需要了解一個叫AbstractProcessor的類柜裸。AbstractProcessor(虛處理器)缕陕,是注解處理器核心API。注解處理器需要繼承于AbstractProcessor疙挺,如下所示:
public class MyProcessor extends AbstractProcessor {
// 這個方法主要是獲取工具類扛邑,有Elements, Types和Filer等。后面會提到
@Override
public synchronized void init(ProcessingEnvironment env){ }
// 相當(dāng)于main(),寫處理的過程
// annotations是getSupportedAnnotationTypes()的子集
// env代表這一輪掃描后的結(jié)果,返回true則表示消費完此次掃描铐然,此輪掃描注解結(jié)束
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
// 在這里定義你的注解處理器注冊到哪些注解上
@Override
public Set<String> getSupportedAnnotationTypes() { }
// 用來指定你使用的Java版本蔬崩。通常這里返回SourceVersion.latestSupported()
@Override
public SourceVersion getSupportedSourceVersion() { }
}
Java 7,我們可以使用注解來代替getSupportedAnnotationTypes()和getSupportedSourceVersion()搀暑。但是因為兼容原因(特別針對Android平臺)沥阳,建議使用重載getSupportedAnnotationTypes()和getSupportedSourceVersion()的方式。
@SupportedSourceVersion(SourceVersion.latestSupported())
@SupportedAnnotationTypes({
// 合法注解全名的集合
})
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
值得一提
注解處理器是運行它自己的虛擬機(jī)JVM中自点。javac啟動一個完整Java虛擬機(jī)來運行注解處理器桐罕。這意味著我們可以使用任何你在其他java應(yīng)用中使用的的東西。可以使用依賴注入工具(Dagger等)功炮,或者其他想要的類庫溅潜。
自定義注解處理器
在自定義前,問一個問題薪伏,怎么注冊Processor到j(luò)avac中滚澜?
我們需要編譯產(chǎn)生一個類似這樣的.jar文件。(注解處理器代碼實現(xiàn)+可擴(kuò)展應(yīng)用程序)
MyProcessor.jar
- com
- example
- MyProcessor.class
- META-INF
- services
- javax.annotation.processing.Processor
打包進(jìn)MyProcessor.jar中的javax.annotation.processing.Processor的內(nèi)容是注解處理器的合法的全名列表嫁怀。(不明白可搜索“可擴(kuò)展應(yīng)用程序”)
com.example.MyProcessor
值得為人稱道的是博秫,Google老大哥提供了auto-service.jar,極大簡化了我們的打包操作眶掌。我們可以用它生成META-INF/services/javax.annotation.processing.Processor文件挡育。是的,我們可以在注解處理器中使用注解朴爬,只需要一個@AutoService即寒。后面會使用到。
不說廢話
場景:我們要幫一個動物學(xué)家的朋友完成觀察動物的報告召噩。于是我們寫了一個Animal接口母赵,然后實現(xiàn)了Bird,Dog具滴,F(xiàn)ish凹嘲。因為目前只觀察到了這幾種。學(xué)過設(shè)計模式的朋友會把它們抽取出來构韵,做一個工廠類周蹭,這種思路是很好的。但是疲恢,假設(shè)我們只會簡單工廠模式凶朗,就if..else..那種。那這時候我們添加一個動物显拳,就需要在工廠自動生成對應(yīng)的if語句來生成相應(yīng)實例棚愤。
于是,我們想到說使用注解處理器的方法來在編譯時自動生成有關(guān)代碼杂数。
我們可以自定義注解如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Factory {
// Animal接口實現(xiàn)類
Class<?> type();
String id();
}
有了工廠注解后宛畦,我們先不急著放在Animal接口實現(xiàn)類上面。我們先實現(xiàn)注解處理器揍移。
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
}
前面我們提到過@AutoService注解次和,加了此注解,可以自動生成services下的文件羊精。因此斯够,我們不需要手動編寫service,只需要關(guān)注注解處理器代碼邏輯即可喧锦。
我們需要一些工具類读规,在代碼中添加并在init()方法對其進(jìn)行初始化。
private Filer filer;
private Messager messager;
private Types typeUtils;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
接著燃少,重載getSupportedAnnotationTypes(),getSupportedSourceVersion()獲取注解支持束亏,以及源碼編譯版本支持。
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(Factory.class.getCanonicalName());
return annotataions;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
Elements和TypeMirrors
需要你對工具類的用途大概有個印象阵具。
工具類 | 用途 |
---|---|
Elements | 用來處理Element的工具類 |
Types | 用來處理TypeMirror的工具類 |
Filer | 使用Filer你可以創(chuàng)建文件 |
需要注意
在注解處理的過程中碍遍,我們掃描所有的Java源文件。源代碼的每一個部分都是一個特定類型的Element阳液。它只是結(jié)構(gòu)化的文本怕敬,不是可運行的,可以像你認(rèn)識Xml一樣的方式認(rèn)識Element帐我。例如說:
package com.example; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo other; // VariableElement
public Foo () {} // ExecuteableElement
public void setA ( // ExecuteableElement
int newA // TypeElement
) {}
}
因此认臊,我們可以像Xml那樣定位到某個元素膝捞。
舉例來說,假如你有一個代表public class Foo類的TypeElement元素虽填,你可以遍歷它的孩子,如下:
TypeElement fooClass = ... ;
for (Element e : fooClass.getEnclosedElements()){ // iterate over children
Element parent = e.getEnclosingElement(); // parent == fooClass
}
可以從上面看出來曹动,Element代表的是源代碼斋日。TypeElement代表的是源代碼中的類型元素,例如類墓陈。然而恶守,TypeElement并不包含類本身的信息。你可以從TypeElement中獲取類的名字贡必,但是你獲取不到類的信息熬的,例如它的父類。類信息需要通過TypeMirror獲取赊级。你可以通過調(diào)用elements.asType()獲取元素的TypeMirror押框。
搜索@Factory注解
開始處理process()的邏輯,在其中添加這樣一段:
for (Element annotatedElement:roundEnv.getElementsAnnotatedWith(Factory.class))
目的是遍歷所有被注解了@Factory的元素理逊,而我們知道TypeElement可能是class橡伞,也可能是其他,那么我們在循環(huán)中需要加以判斷:
if (annotatedElement.getKind() != ElementKind.CLASS) {
throw new Exception(
"Only classes can be annotated with @%s");
}
// 確認(rèn)為ElementKind.CLASS晋被,強(qiáng)制轉(zhuǎn)換為TypeElement
TypeElement typeElement = (TypeElement) annotatedElement;
為什么我們使用.getKind()而不是.instanceof()兑徘?
首先,instanceof是檢查對象和類的關(guān)系的方法羡洛。其次挂脑,接口(interface)類型也是TypeElement。所以不采用.instanceof()的做法。
優(yōu)雅的錯誤處理
可能上面的代碼給你造成一種錯覺崭闲,就是我直接跑出了異常肋联。事實上不是的,上面的代碼我寫在try..catch..塊中刁俭,并且使用
messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
來處理異常橄仍。這是一種優(yōu)雅的錯誤處理方式。
為什么說是一種優(yōu)雅的錯誤處理方式牍戚?
在傳統(tǒng)Java應(yīng)用中我們可能就拋出一個Exception侮繁。如果你在process()中拋出一個異常,那么運行注解處理器的JVM將會崩潰(就像其他Java應(yīng)用一樣)如孝。而使用我們注解處理器的第三方開發(fā)者將會從javac中得到非常難懂的出錯信息宪哩,因為它包含自定義注解處理器的堆棧跟蹤(Stacktace)信息。因此第晰,注解處理器就有一個Messager類斋射,它能夠打印非常優(yōu)美的錯誤信息。除此之外但荤,你還可以鏈接到出錯的元素罗岖。在像IntelliJ這種現(xiàn)代的IDE(集成開發(fā)環(huán)境)中,第三方開發(fā)者可以直接點擊錯誤信息腹躁,IDE將會直接跳轉(zhuǎn)到第三方開發(fā)者項目的出錯的源文件的相應(yīng)的行桑包。當(dāng)然,這里需要之前初始化好的Messager對象纺非。
接下來哑了,我們會希望有一個類能解析typeElement并保存信息。
public class FactoryAnnotatedClass {
private TypeElement annotatedClassElement;
private String qualifiedSuperClassName;
private String simpleTypeName;
private String id;
public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {
this.annotatedClassElement = classElement;
Factory annotation = classElement.getAnnotation(Factory.class);
id = annotation.id();
if (id == null || "".equals(id)) {
throw new IllegalArgumentException(
String.format(
"id() in @%s for class %s is null or empty! that's not allowed",
Factory.class.getSimpleName(), classElement
.getQualifiedName().toString()));
}
try {
// 這個類已經(jīng)被編譯
Class<?> clazz = annotation.type();
qualifiedSuperClassName = clazz.getCanonicalName();
simpleTypeName = clazz.getSimpleName();
} catch (MirroredTypeException mte) {
// 這個類還沒被編譯
DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
simpleTypeName = classTypeElement.getSimpleName().toString();
}
}
/**
* 獲取在{@link Factory#id()}中指定的id return the id
*/
public String getId() {
return id;
}
/**
* 獲取在{@link Factory#type()}指定的類型合法全名
*
* @return qualified name
*/
public String getQualifiedFactoryGroupName() {
return qualifiedSuperClassName;
}
/**
* 獲取在{@link Factory#type()}{@link Factory#type()}指定的類型的簡單名字
*
* @return qualified name
*/
public String getSimpleFactoryGroupName() {
return simpleTypeName;
}
/**
* 獲取被@Factory注解的原始元素
*/
public TypeElement getTypeElement() {
return annotatedClassElement;
}
}
解析并保存信息烧颖,接著需要有一個生成工廠類的類弱左。
public class FactoryGroupedClasses {
/**
* 將被添加到生成的工廠類的名字中
*/
private static final String SUFFIX = "Factory";
private String qualifiedClassName;
private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<String, FactoryAnnotatedClass>();
public FactoryGroupedClasses(String qualifiedClassName) {
this.qualifiedClassName = qualifiedClassName;
}
public void add(FactoryAnnotatedClass toInsert) throws Exception {
FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());
if (existing != null) {
throw new Exception(existing + " is existed.");
}
itemsMap.put(toInsert.getId(), toInsert);
}
public void generateCode(Elements elementUtils, Filer filer) throws IOException {
TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
String factoryClassName = superClassName.getSimpleName() + SUFFIX;
// 通過filer生成.java文件
JavaFileObject jfo = filer.createSourceFile(qualifiedClassName + SUFFIX);
Writer writer = jfo.openWriter();
JavaWriter jw = new JavaWriter(writer);
// 寫包名
PackageElement pkg = elementUtils.getPackageOf(superClassName);
if (!pkg.isUnnamed()) {
jw.emitPackage(pkg.getQualifiedName().toString());
// 空一行
jw.emitEmptyLine();
} else {
jw.emitPackage("");
}
// public class factoryClassName
jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));
jw.emitEmptyLine();
// public qualifiedClassName create(String id)
jw.beginMethod(qualifiedClassName, "create",EnumSet.of(Modifier.PUBLIC), "String", "id");
jw.beginControlFlow("if (id == null)");
jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");
jw.endControlFlow();
for (FactoryAnnotatedClass item : itemsMap.values()) {
jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());
jw.emitStatement("return new %s()", item.getTypeElement().getQualifiedName().toString());
jw.endControlFlow();
jw.emitEmptyLine();
}
jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");
jw.endMethod();
jw.endType();
jw.close();
}
}
基本的思路整理清晰后,我們回到process()方法的循環(huán)中炕淮。
FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement);
...
// 從map集合取數(shù)據(jù)
FactoryGroupedClasses factoryClass = factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
if (factoryClass == null) {
String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
//初始化生成工廠類的類
factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
factoryClasses.put(qualifiedGroupName, factoryClass);
}
在循環(huán)結(jié)束后拆火,我們這么調(diào)用
// Generate code
for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
// 生成工廠類
factoryClass.generateCode(elementUtils, filer);
}
// 打個標(biāo)記 這里要注意
factoryClasses.clear();
官方j(luò)avadoc定義處理過程如下:
注解處理過程是一個有序的循環(huán)過程。在每次循環(huán)中涂圆,一個處理器可能被要求去處理那些在上一次循環(huán)中產(chǎn)生的源文件和類文件中的注解们镜。第一次循環(huán)的輸入是運行此工具的初始輸入。
就是說润歉,processor會在執(zhí)行完process()后模狭,生成工廠類,然后再執(zhí)行process()以檢查生成的文件是否包含@Factory標(biāo)記踩衩,第三次執(zhí)行發(fā)現(xiàn)為none嚼鹉,返回贩汉。因此,上面的邏輯是process()會被調(diào)用三次锚赤。
因此匹舞,如果不對數(shù)據(jù)進(jìn)行clear,會重復(fù)生成第一次生成的文件并且報錯宴树。
另外策菜,需要注意的是上面提到一個叫做JavaWriter的類晶疼,這是apache的工具類酒贬。只不過在工程中我把它復(fù)制出來了。很多處理器翠霍、庫锭吨、工具都依賴于JavaWriter。目前已經(jīng)被JavaPoet取代寒匙。有空可以看看零如。
使用自定義好的注解處理器
使用Maven編譯。把a(bǔ)nnotation.jar和processor.jar放到builpath中锄弱,javac會自動檢查和讀取javax.annotation.processing.Processor中的內(nèi)容考蕾,并且注冊MyProcessor作為注解處理器。
為什么要分別打包会宪?
在開發(fā)過程中肖卧,第三方開放著僅僅需要processor產(chǎn)生需要的代碼,而并不希望它跟隨源代碼一起打包掸鹅。因此塞帐,一起打包不合適!
如果你是一個Android的開發(fā)者巍沙,你肯定聽說過65k個方法的限制(即在一個.dex文件中葵姥,只能尋址65000個方法)。如果你在FactoryProcessor中使用guava句携,并且把注解和處理器打包在一個包中榔幸,這樣的話,Android APK安裝包中不只是包含F(xiàn)actoryProcessor的代碼矮嫉,而也包含了整個guava的代碼牡辽。Guava有大約20000個方法。所以分開注解和處理器是非常有意義的敞临。
補(bǔ)充Animal的實現(xiàn)類:
@Factory(id = "bird", type = Animal.class)
public class Bird implements Animal {
@Override
public void doSomething() {
System.out.println("fly");
}
}
寫main()方法入口:
public static void main(String[] args) {
new AnimalFactory().create("dog").doSomething();
}
打開AnimalFactory态辛,
生成的代碼符合我們的預(yù)期要求。成功挺尿!
說說題外話
如果你是一個Android的開發(fā)者奏黑,你應(yīng)該非常熟悉一個叫做ButterKnife的注解處理器炊邦。在ButterKnife中,你使用@InjectView注解Android的View熟史。ButterKnifeProcessor生成一個MyActivity$$ViewInjector馁害,但是在ButterKnife你不需要手動調(diào)用new MyActivity$$ViewInjector()實例化一個ButterKnife注入的對象,而是使用Butterknife.inject(activity)蹂匹。ButterKnife內(nèi)部使用反射機(jī)制來實例化MyActivity$$ViewInjector()對象:
try {
Class<?> injector = Class.forName(clsName + "$$ViewInjector");
} catch (ClassNotFoundException e) {
...
}
上一篇文章我們提到反射影響性能碘菜,使用注解處理來生成本地代碼,會不會導(dǎo)致很多的反射性能的問題限寞?
的確忍啸,反射機(jī)制的性能確實是一個問題。然而它并不需要手動去創(chuàng)建對象履植,確實提高了開發(fā)者的開發(fā)速度计雌。ButterKnife中有一個哈希表HashMap來緩存實例化過的對象。所以MyActivity$$ViewInjector只是使用反射機(jī)制實例化一次玫霎,第二次需要MyActivity$$ViewInjector的時候凿滤,就直接沖哈希表中獲得。
FragmentArgs非常類似于ButterKnife庶近。它使用反射機(jī)制來創(chuàng)建對象翁脆,而不需要開發(fā)者手動來做這些。FragmentArgs在處理注解的時候生成一個特別的查找表類(其實就是一種哈希表)鼻种,所以整個FragmentArgs庫只是在第一次使用的時候反番,執(zhí)行一次反射調(diào)用(一旦整個Class.forName()的Fragemnt的參數(shù)對象被創(chuàng)建),后面的都是本地代碼運行了普舆。
而如果你使用過Realm的話恬口,你也能發(fā)現(xiàn)類似的細(xì)節(jié)。
總結(jié)
注解處理器是一個強(qiáng)大的工具沼侣,為第三方開發(fā)者提供了巨大的便捷性祖能。我也想提醒的是,注解處理器可以做到比我上面提到例子復(fù)雜很多的事情蛾洛。
另外养铸,如果你決定在其他類使用ElementUtils, TypeUtils和Messager,你就必須把他們作為參數(shù)傳進(jìn)去轧膘〕可以使用Dagger(一個依賴注入庫)來解決這個問題。這在上面也有提到谎碍。
對了鳞滨,我也找到一個使用gradle構(gòu)建注解處理器的例子。需要的朋友也可以參考一下蟆淀。地址如下:
http://blog.csdn.net/ucxiii/article/details/52025005
那么拯啦,關(guān)于注解的模塊到此就講完了澡匪。如果有疑問的小伙伴可以在評論區(qū)下面留言。
(以上內(nèi)容參考總結(jié)自很多文章褒链,感謝互聯(lián)網(wǎng)給了我一個學(xué)習(xí)的平臺~)